Java内存回收机制总结

jopen 10年前

此处将引用《深入理解Java虚拟机——JVM高级特性与最佳实践》这本书的一些内容。

1、对象已死?

    垃圾回收是对堆中对象的管理,首先就要确定什么是垃圾,即什么情况下堆中的对象可以被回收。

    最常用的判定算法是引用计数算法,即每当有一个对象被其它对象所引用,则将对象的引用数+1,当对象的引用数为0时,则认为对象将不再被使用,可以回收。但引用计数算法有一个缺陷,即无法解决对象循环引用的问题。当对象相互引用时,将会给引用的双方的对象的引用计数+1,这样的话对象的引用计数将一直无法被清零,也即是说,GC(Garbage Collection)无法判定对象为可回收对象,该对象将一直占据在内存中无法被释放。

    Java中使用的判定算法为根搜索算法(GC Roots Tracing),这个算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,由该起始点出发向下搜索对象,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链连接,即此对象不可达时,则认为此对象不可用。

    Java中可以作为GC Roots的对象包括以下几种:

    a、虚拟机栈(栈帧中的本地变量表)中的引用的对象

    b、方法区中的类静态属性引用的对象

    c、方法区中的常量引用的对象

    d、本地方法栈中JNI的引用的对象

    个人对于以上几种GC Roots对象的理解是这样的:根搜索算法的判定条件是对象不可达,而不是引用计数归零,即是说当从某个引用出发去搜索对象时,某个对象无法再被搜索到,则判定该对象可以被回收,这样一来,一旦引用被重置为null,假设该引用所指向的对象只被该引用所索引,那么此时对象将不再被任何引用所索引,此时对象将处于可回收状态,而不会出现由于循环引用而使引用计数无法被清零的问题。具体情形如下:

    Java中对于引用的概念进行了扩充,以描述这样一类对象:当内存空间还足够时,则能保留在内存之中,如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。

    Java中的引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)四种,强度依次减弱。

    a、强引用:类似"Object obj = new Object();"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

    b、软引用:通过SoftReference类实现,SoftReference<Person> p = new SoftReference<Person>(new Person(“Rain”));内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。

    c、通过WeakReference类实现,eg : WeakReference<Person> p = new WeakReference<Person>(new Person(“Rain”));不管内存是否足够,系统垃圾回收时必定会回收。

    d、不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现。

    回顾完引用问题,来继续看对象什么时候被回收的问题,前面已经讲了当对象被判定不可达时,则可以被回收。但是否一定被回收呢?

    当对象被判定不可达时,此时虚拟机会对该对象进行标记并筛选,筛选的条件是此对象有没有必要执行finalize()方法。当对象没有覆盖 finalize()方法或是虚拟仙已经调用过finalize()方法,则虚拟机将这两种情况都视为没有必要执行。若是判定为没有必要执行,则直接回收对象。

    若对象被判定为有必要执行finalize()方法,虚拟机会把该对象放入一个F-Queue的队列中,稍后会由虚拟机建立的一条优先级的线程 Finalizer去执行。注意,此处的执行是指虚拟机会触发这个方法,但不保证会等待方法的执行直至结束(避免由于执行缓慢或是死循环而导致整个内存回收系统崩溃)。finalize()方法是对象逃脱被回收命运的最后一次机会,(若想要保留对象不被回收,可覆盖finalize()方法并在方法中添加到这个对象的引用。)在F-Queue队列开始执行后,过一会时间GC会对队列中的对象进行第二次标记,标记和筛选方法和第一次的相同,但有一点有所不同,即任何对象的finalize()方法将被会且只会被系统自动调用一次,只要执行过一次后,以后将不再执行,因此,在第二次标记后,依旧被判定为不可达的对象将被虚拟机直接回收。

    以上对对象的回收进行了回顾,而对于方法区的回收,JVM规范不要求虚拟机实现方法区的垃圾收集,虚拟机不一定会实现这方面的回收。在这里就简单的说一下永久代的回收内容和判定条件。

    永久代的垃圾收集主要包括废弃常量和无用的类,废弃常量的判断比较简单,只要检查该常量是否被引用就可以了。而类的“无用”判定则要同时满足三个条件:a、该类所有的实例已经被回收,即Java堆中不存在该类的任何实例;b、加载该类的ClassLoader已经被回收;c、该类对应的 java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义的ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

2、常用的垃圾收集算法

    a、标记——清除算法(Mark——Sweep)

    首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

    缺点:

    效率问题,标记和清除过程的效率不高;

    空间问题,标记清除之后会产生大量的非连续空间碎片,过多的碎片将导致程序在以后的运行中找不到足够大的连续内存空间来分配给较大的对象而不得不提前触发另一次垃圾收集事件。

    b、复制算法(Copying)

    为了解决标记清除算法的效率问题,复制算法将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。

    优点:

    实现简单,运行高效

    缺点:

    对内存空间的利用不高,可用内存变成一半,这代价过高

    现在的商业虚拟机基本都采用这种收集算法来回收新生代,由于新生代中的对象存活率不高,因此不需要按1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior,回收时将Eden和Survior中还存活的对象一次性地拷贝到另外一块Survior上,最后清理掉Eden和刚才用过的Survior空间。(HotSpot虚拟机默认Eden和Survior大小比例是 8:1)当然,Survior空间不可能一直够用,此时还需要老年代来进行分配担保(Handle Promotion)(主要是将新生代存活的大对象直接移到老年代,以减少对Survior内存空间的需求)。

    c、标记——整理算法(Mark——Compact)

    与标记清除算法的标记阶段相同,但标记后会将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。这种算法一般用于老年代的内存回收上,因为老年代中对象的存活时间都比较长,可能存在100%存活的极端情况,因此不能选择Copying算法来进行回收。

    d、分代收集算法(Generational Collection)

    这种算法只是根据对象的存活周期的不同将内存划分为几块,一般都划分为新生代和老年代,这样可以根据各个年代的特点采用最适当的收集算法。新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,因此选取复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用“标记——清除”或是“标记——整理”算法来进行回收。在之前的三种算法中已经有所描述。

3、内存分配与回收策略

    回顾完垃圾回收的判定和算法后,我们来看下内存分配和回收的策略。

    内存的回收分为两类:

    新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。

    老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次Minor GC。Major GC的速度一般会比Minor GC慢10倍以上,而且一次Full GC的执行是以整个程序完全停滞作为代价的,这对于很多实时或是高响应要求的应用来说是不可接受的,因此需要尽可能地减少Full GC的出现。

    虚拟机一般会采用以下几种内存分配策略来尽量减少Full GC出现的频率:

    a、对象优先在Eden分配

    b、大对象直接进行老年代(可通过参数设置直接进入老年代的对象的内存阈值)

    c、长期存活的对象将进入老年代(对Survior中的对象进行年龄计量,每经过一次Minor GC而不被回收,则对其年龄+1,当达到阈值时就晋升到老年代,阈值可以通过参数设置)

    d、动态对象年龄判定(当Survior空间中相同年龄所有对象大小的总和大于Survior空间的一半,则年龄大于或等于该年龄的对象就可以直接进入老年代)

    e、空间分配担保(在此前有提到过,老年代为Survior进行空间担保,当Survior空间不足时将对象转移进老年代。前提是老年代也有足够的空间来安置来自Survior的对象,但Survior中将要被移到老年代的对象的大小在完成内存回收之前是不可知的,只能取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出空间。当然,如果出现某次Minor GC存活后的对象突增,远远高于平均值,此时可能会导致担保失败,则将重新发起一次Full GC)

    好了,终于完成了关于Java内存回收机制的复习,除了《深入理解Java虚拟机》这本书外,还引用了以下页面的内容,这篇博客对垃圾回收机制的描述更加细致且易于理解。文章链接如下:

http://blog.jobbole.com/37273/