GC

Grabage Collection GC 垃圾收集,在了解了jvm的内存区域之后,要关心的问题就是垃圾收集了,因为我们的内存是有限的,程序在运行中会不断的产生新的对象占用内存空间,所以我们需要一个垃圾收集机制去回收内存

在java内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈三个区域随着线程的创建而创建,销毁而销毁,栈中的每个栈帧分配多少内存基本上在类结构确定下来是就抑制了,所以这几个区域不需要过多考虑回收的问题,方法结束或者线程结束,内存自然就回收了,而在堆和方法区这两块区域中,我们只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配合回收都是动态的,所以我们主要关注点在如果进行回收堆内存和方法区这两块区域的垃圾内存

对象是否存活

垃圾收集,我们首先要判断哪些对象是垃圾的对象

引用计数算法:每个对象都添加一个引用计数器,当有地方引用它的时候,计数器就加1,当引用失效的时候计数器就减1,这样通过这个引用计数器就可以知道当前对象是否被引用,但是这种方式的弊端就是无法解决循环引用的问题,假如a持有b的引用,b持有a的引用,两个对象的计数器都是1,但是a和b这两个对象只是被对方引用,假如这两个对象都是垃圾对象,但是由于计数器不为零,所以无法进行回收

可达性分析算法:当一个对象到GC Roots没有任何引用链相连的时候,就证明这个对象是不可达的对象

所以这个GC Roots很重要,包括以下几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象,常量引用的对象
  • 本地方法栈中JNI引用的对象

4种引用

无论哪种算法都需要判断引用,jdk中存在着4种引用

  1. 强引用(Strong Reference)

    Object object = new Object();

    当内存不够时,程序会抛出异常,也不会进行回收强引用指向的对象

  2. 软引用(Soft Reference)

    用来描述一些有用但非必须的对象,必要时可以进行垃圾回收

    SoftReference<Object> softReference = new SoftReference<>(object);

    当内存充足时,垃圾收集器不会回收弱引用指向的对象,当内存不足时,垃圾收集器才会回收软引用指向的对象

  3. 弱引用(Weak Reference)

    描述非必须对象

    WeakReference<Object> weakReference = new WeakReference<>(object);

    每次垃圾收集时被回收

  4. 虚引用(Phantom Reference)

    这个引用类型强度最低,一个对象是否有虚引用对其生存周期没有任何影响

    PhantomReference<Object> phantomReference = new PhantomReference<>(object, new ReferenceQueue<>());

    每次垃圾收集时被回收

    虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中

二次标记

在发现不可达对象后,这对象也不是一定会回收,一个对象被回收,至少要经历两次标记过程

第一次:当对象不可达时被第一次标记

第二次:如果未可达对象,没有覆盖finalize()方法不需要执行finalize()方法,或者已经执行过finalize()方法了,此时进行第二次标记

如果未可达对象有必要执行finalize()方法,则会被放入一个F-Queue的队列中,后续jvm会创建一个低优先级的线程去执行它,只要未可达对象在finalize()方法里重新将自己赋予给某个类的对象或者对象的属性,就可以避免被垃圾回收

验证:

/**
 * @author: chenmingyu
 * @date: 2019/9/18 18:19
 * @description:
 */
public class FinalizeTest {

    private static FinalizeTest FINALIZE_TEST;

    public void test(){
        System.out.println("当前存活");
    }

    @Override
    protected void finalize() throws Throwable {
        FINALIZE_TEST = this;
        System.out.println("执行finalize方法");
    }

    public static void main(String[] args) throws Exception {

        FINALIZE_TEST = new FinalizeTest();
        FINALIZE_TEST = null;
        System.gc();
        TimeUnit.SECONDS.sleep(2L);
        if(FINALIZE_TEST != null){
            FINALIZE_TEST.test();
        } else {
            System.out.println("已死亡");
        }

        FINALIZE_TEST = null;
        System.gc();
        TimeUnit.SECONDS.sleep(2L);
        if(FINALIZE_TEST != null){
            FINALIZE_TEST.test();
        } else {
            System.out.println("已死亡");
        }
    }
}

输出:

当一次手动调用gc时,FINALIZE_TEST对象被第一次标记,但是在执行finalize()方法时,重新将自己赋给了静态变量,这样这个对象就有重新有了强引用,避免了被回收

第二次手动调用gc时,FINALIZE_TEST对象被第一次标记,不在执行finalize()方法,因为finalize()方法只会被系统自动调用一次,所以之后不会再执行finalize()方法,进行了二次标记,然后对象被垃圾收集器回收

经过两次标记之后,对象基本上就会被回收了

可以自己将上面重写finalize()方法去掉,自己试一下效果

方法区回收

对方法区的回收主要是对无用类的回收

无用类的条件:

  1. 该类的所有实例都已经被回收
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类

方法区的垃圾回收性价比低,所以java虚拟机规范中要求虚拟机可以不在方法区实现垃圾收集

垃圾收集算法

标记清除算法

算法分为两个部分:标记和清除

标记阶段:首先按照可达性分析,将GC Roots可达的对象进行标记,未被标记的对象就是需要回收的对象

清除阶段:在标记完成后统一回收所有未被标记的对象

这种算法适用于垃圾比较少的区域,比如老年代

缺点:标记和清除过程效率都不高,回收后会产生大量不连续的内存碎片,空间碎片太多可能导致后续分配大对象时,无法找到足够的连续内存而触发领另一次GC

复制算法

复制算法将内存空间分为大小相同的两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除掉正在使用的内存中所有对象,交换两块内存的角色,周而复始

优点:使用复制算法解决了标记清除算法的效率问题,分配内存时不用在考虑内存碎片的问题,按顺序分配内存,运行效率高

缺点:内存可用率缩小为原来的一半,如果对象存活率较高时,效率将会变低

这种算法适用于新生代

有研究表明,新生代中的对象有98%都是朝生夕死的,所以不需要按照1:1的比例来划分内存,而是将内存分为一块较大的内存区域叫Eden区和两块较小的内存区域叫Survivor区,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor

HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间,如果老年代的内存不够用,就会触发一次fullGC

标记整理算法

标记整理算法可以分为三个阶段,第一标记阶段,第二整理阶段,第三清除

实现过程是首先进行标记,将存活的对象标记出来,在内存中把存活的对象往一端移动,直接回收边界以外的内存,所以不会产生内存碎片,提高了内存的利用率,这种算法适用于老年代

缺点:效率不高,不仅要标记存活对象还要整理所有存活对象的引用地址

分代收集算法

分代收集算法根据对象存活的生命周期不同将内存划分为不同的区域,一般是把堆分成新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法

新生代:新生代中每次垃圾收集都有大量垃圾对象需要回收,只有少量的对象存活,所以选择复制算法是最高效的,只需要移动少量的对象即可

老年代:老年代中对象存活率高,没有额外的空间对它进行分配担保,所以可以采用标记清除或者标记整理算法