【jvm】GC与垃圾回收算法

GC

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

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

对象是否存活

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

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

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

因此这个GC Roots很重要,包括如下几种:spa

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

4种引用

不管哪一种算法都须要判断引用,jdk中存在着4种引用线程

  1. 强引用(Strong Reference)3d

    Object object = new Object();
    复制代码

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

  2. 软引用(Soft Reference)cdn

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

    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

标记整理算法

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

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

缺点:效率不高,不只要标记存活对象还要整理全部存活对象的引用地址

分代收集算法

分代收集算法根据对象存活的生命周期不一样将内存划分为不一样的区域,通常是把堆分红新生代和老年代,这样就能够根据各个年代的特色采用最合适的收集算法

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

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

参考:深刻理解java虚拟机第二版

更多阅读:chenmingyu.top/categories/

相关文章
相关标签/搜索