筑基#内存模型

37 阅读9分钟

java筑基#注解

java筑基#多线程编程

java筑基#序列化

java筑基#内存模型

java筑基#泛型

一.图解java从编译到运行

我们可以看到当我们编译一个java文件到执行引擎运行大概会经过下图所示的流程,我们知道jvm是c++写的,所以解释执行是c++种有个解释器来解释字节码,由于它是经过一次翻译,所以它会比较慢,而JIT编译器是如果你的代码会执行超过5000次以上就会认为这段代码是热点代码,它会直接帮助我们翻译成汇编,它的速度比较快,但是它可能提前编译,所以加载时间稍长。

二.类加载器

1.类加载器的定义

根据指定全限定名将class文件加载到jvm内存中,转为Class对象。

2.类加载器的分类

  • 启动类加载器

Bootstrap类加载器(c++实现):负责将存放在/java_home/lib或-xbootclasspath参数指定的路径的类库加载到内存中

  • 其他类加载器
  1. Extension类加载器(java实现):负责/java_home/lib/ext目录或java.ext.dirs系统变量指定路径的所有类库;
  2. Application应用类加载器,用来加载用户类(classpath)路径上的指定类库,我们可以直接使用,如果我们没有自定义类加载器默认就使用这个加载器

3.双亲委派机制

如果一个类加载器收到类加载请求,并不直接去加载,会先去父类去加载,如果父类没有加载过,才会使用自己的类加载器。每个类加载器都是这样的,它的流程是bootstrap->Extension->Aplpication->用户自定义

  • 为什么使用双亲委托机制?

如果没有双亲委托就无法保证内存中类的唯一性,也保证不了核心API的安全性,并且内存中会出现多份同样的字节码,如何破解双亲呢?重写类加载器,并重写findclass和loadclass方法。

三.jvm运行时数据区

1.内存结构图

2.共享区

  • 方法区

每一个jvm虚拟机只有一个方法区,生命周期与jvm一致,能够被所有线程共享;存储类信息,静态变量,常量;

  • 堆区

每一个jvm虚拟机只有一个堆区,生命周期与jvm一致,能够被所有线程共享;存储对象和数组

3.独占区

  • 虚拟器栈

每个Java虚拟机线程都有一个私有的Java虚拟机栈,在线程创建时创建;Java虚拟机栈存储栈帧,那什么是栈帧?栈帧用于存储数据和部分结果,以及执行动态链接,方法的返回值和调度异常。你可以理解为方法的执行,它是压栈执行的。

我们可以用-Xss 来设置栈的大小,默认一般是1024KB,当然这个跟平台有关,并且如果一直出不了栈,如四递归,就会可能导致栈溢出即:stack overflow error。

  • 栈帧执行流程

我们在看看栈帧内部执行的结构,即方法执行的结构:当执行某个操作时局部变量表会被压入到操作数栈中,程序计数器会记录字节码所在的位置行号,如果这个时候有其他线程执行,当其执行完成后就会回到该位置继续执行,当执行完成后,如果有返回值会被压入到操作数栈返回。

  • 本地方法栈

每个Java虚拟机线程都有可能存在一个私有的本地方法栈,如果线程没有调用本地方法(native)则可能不存在本地方法栈。

  • 程序计数器(指令内存地址偏移量)

线程在运行时都会抢CPU的资源进行运行,如果线程在运行时来回切换,为了保证切换回来后知道执行在哪个位置,需要程序计数器来记住当前执行的位置,每个栈帧的刚进入的时候都是从0开始的偏移量,字节码指令大小不一样,偏移量不一样。

4.代码执行流程

1.jvm申请内存 主要是申请方法区和栈内存

2.初始化运行时数据区

3.类加载

4.执行方法

5.创建对象

四.jvm对象分配和垃圾回收机制

1.堆内存地址分配

堆地址可以分为以上四个区,那这四个区分别分配什么对象,以及他们的运作流程是咋样的呢?我们接着继续看。

2.对象创建与分配过程

1.对象的创建过程

1.对象分配内存的方式

指针碰撞:

当内存空间连续没有碎片可以分配对象大小时,我们叫指针碰撞,这种效率高。

空闲列表:

内存空间不是连续,我们需要对内存碎片进行记录,并且进行分配给对象。

2.并发安全问题解决

CAS失败重试:

多线程分配对象时,我们会通过cas来判断对象是否创建来解决并发问题。

本地线程分配缓冲:

Thread Local Allocation Buffer的简称,这种是每个线程都在eden区分配自己线程的对象,这种速度更快,但是占用内存会比较大。

3.内存初始化

它是给对象的一些变量初始化,比如int类型赋初始0等。

4.对象头的设置

5.对象的初始化

根据构造方法区创建对象。

2.对象的放置区域

1.一般对象

这种是new出来直接放到eden区的。

2.大对象

这种大对象是直接放到老年代即:Tenured区,或者是经过15次的minorGc的未被回收的对象。minorGc会在后面介绍。

3.栈对象

逃逸分析与jit,当有热点数据时(方法的次数达到一定的次数)就会触发jit编译,然后就会形成逃逸分析,判断对象是否可以逃出此方法(在栈内创建不会被其他线程使用),若可以就会在栈内创建对象。

3.垃圾回收机制

1.回收算法简介

  • 复制算法

它将可用内存一分为二,每次只用一块,当这一块内存不够用时,便触发 GC,将当前存活对象复制(Copy)到另一块上,以此往复。例如年轻代中的MinorGc机制,它的效率高,因为java中大部分对象可以直接回收,所以存活的对象比较少,那复制的数据量就会比较小,但是它也会造成一定程度的内存浪费。

  • 标记算法

在标记清除的基础上,追加了碎片的散落问题,在清除之后进行了碎片的整理,但副作用是增了了GC的时间。例如老年代垃圾回收,它的好处是提供了连续的内存。如上图复制算法,当对象被复制到内存2时,那么内存1内存如果没法直接使用,就有可能成为内存碎片,那么我们就需要用来标记这些碎片,对其整理。

  • 根可达性分析算法

如果对象没有被GCRoot持有或引用,这些对象就有可能被回收。在jvm中GcRoot有静态变量、常量池、线程栈变量、jni指针,除此之外还有Exception对象、类加载器、同步锁(synchronized)、内部对象,本地缓存、回调、临时对象等

2.jvm中的垃圾回收

1.引用关系类型

强引用

new出来的对象就是强引用,jvm宁愿oom也不会回收此类对象。

软引用

SoftReference 包裹的对象,如果仅有软引用的情况下,除非内存不足,不然垃圾回收器不会回收。

弱引用

WeakReference包裹的对象,垃圾回收时会回收此类对象。

虚引用

PhantomReferences随时可以被垃圾回收器回收,可以验证垃圾回收器是否正常工作。

2.MinorGc

第一次MinorGc:当eden区满了之后,就会执行MinorGc并且将存活的对象放置S0区;

第二次MinorGc:当eden区再次满了,就会将eden和S0区存活的对象放置S1,并且S0和S1交换,这个时候S0中的对象就是经过 两次MinorGc未被回收的对象了。

3.FullGc

我们把eden、s0、s1区成为年轻代(G0),当我们的对象经过15(参数可调,最大15) 次GC后依旧未被回收,它就会进入老年代即G1(Tenured)区,如果是一个大对象创建后也会进入老年代,如果G0和G1都满了,就会触发FullGc,如果FullGC触发后依旧无法为对象分配内存,就会触发oom即:内存溢出。

4.class对象是否能被回收?

可以但是条件比较苛刻,1.class创建的对象都被回收;2.对应的类加载器被回收;3.class对象未被引用,且无法反射获取;

5.finalize方法总结

对象被回收时会执行此方法;它的优先级比较低,并且它只会调用一次,不会重复执行。

public class FinalizeTest {

    public static FinalizeTest instance;
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("i'm going to die");
        instance=this;//重新赋值

    }

    public void alive(){
        System.out.println("i'm alive");
    }
    @Test
    public void testFinalize() throws InterruptedException {
        instance=new FinalizeTest();
        instance=null;
        System.gc();
        Thread.sleep(200);//休眠线程,finalize 执行,如果休眠 finalize 可能执行不了
        if(instance!=null){
            instance.alive();
        }else{
            System.out.println("i'm dead1");
        }
        instance=null;
        System.gc();//再次GC finalize不会再执行,只会执行一次
        Thread.sleep(200);
        if(instance!=null){
            instance.alive();
        }else{
            System.out.println("i'm dead2");
        }
    }
}

日志打印:

i'm going to die
i'm alive
i'm dead2

五.总结

我们经过前面四节的学习,主要掌握了以下知识点:

1.java加载到jvm的流程,jit编译和字节码解释器的区别;

2.类加载机制和双亲委派机制;

3.jvm运行时内存结构分为独占、共享区,也分别堆独占和共享区进行了介绍,我们也总结了独占区虚拟机栈帧中方法执行流程;

4.jvm对象的分配,我们从jvm数据初始化开始,以及对象分配时的内存分配情况,如指针碰撞以及空闲列表两种方式以及多线程创建的cas判断原则,我们也讲了对象的具体接口,比如对象头,让我们了解到一个对象不仅仅包含对象数据还包含类指针、字节对象、对象头(同步 hash、锁等信息),后面我们又对对象放置的对区域做了简单介绍;

5.除了对象分配之外,我们学到了垃圾回收机制,对象的引用类型等知识点,在jvm中我们一般都使用根可达分析来处理垃圾回收。