JVM解析(二) JVM 垃圾回收器

一. 概览

    定义

        JVM的垃圾回收器是Java内存管理中的一个重要组成部分。其主要任务是自动回收不再被程序使用的对象所占用的内存空间,以避免内存泄漏和内存溢出等问题。

    作用区域

        垃圾回收器主要作用于堆和方法区,对于线程私有的区域(程序计数器、虚拟机栈、本地方法栈)垃圾回收器不会进行回收。因为这些区域的内存,会随着线程的死亡而释放。

    判断对象是否可以回收

        引用计数器算法:每个对象中有个被其他对象引用的计数器,如果被一个对象引用了,那么计数器加一。如果在垃圾回收器回收过程中,发现对象不存在被引用的情况,那么该对象会被回收。但是有一个缺点,如果存在循环引用的情况(如以下代码),那么对象将无法被回收器回收。所以目前不推荐使用引用计数器方法来判断对象是否可以回收。

JvmGcDemoClass a = new JvmGcDemoClass();
JvmGcDemoClass b = new JvmGcDemoClass();
a.setReference(b);
b.setReference(a);

第一步:创建A对象,存储在堆空间中,但是a变量是存储在栈帧里面的局部变量表中,所以a的引用地址就是堆空间引用地址 
第二步:创建B对象,存储在堆空间中,但是b变量也是存储在栈帧里面的局部变量表中,所以b的引用地址就是堆空间引用地址 
第三步:A对象的属性object的引用地址指向了B对象的引用地址 
第四步:B对象的属性object的引用地址也执行了A对象的引用地址
第五步:局部变量表中的a变量引用地址置为null,直接将下图中的第一步去掉了
第六步:局部变量表中的b变量引用地址置为null,直接将下图中的第二步去掉了
这样就导致了堆空间中的循环相互引用的问题

                                

        可达性分析算法:从根对象(GC Root)开始,沿着引用链下去进行追溯。如果一个对象没有任何一个引用链,那么这个对象可以被回收。目前流行的解决方案。

            1. 可被当作GC Root的对象                  

(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。 
(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。 
(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。 
(4)最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。 

    · 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
    · 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
    · 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
    · 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

            2. 对象最终是否会被回收?(摘自深入理解 JVM )

                即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize() 方法。当对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

                如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queuc的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是如果一个对象在finalizeO 方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize() 方法是对象逃脱死亡命运的最后一次机公稍后GC将对F-QUCUC中的对象进行第二次小规模的标记,如果对象要在finalize()中成功扬救自己一只要重新与引用链上的任何- 一个对象建立关联即可,譬如把自己(this 关键字) 赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合: 如果对象这时候还没有逃脱,那基本上它就真的被回收了。从代码清单3-2 中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。代码清单3-2一次对象自我拯救的演示。

/*此代码演示了两点
 * 对象可以在GC时自我拯救
 * 这种自救只会有一次,因为一个对象的finalize方法只会被自动调用一次
 * */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK=null;
    public void isAlive(String prefix){
        System.out.println(prefix + "yes我还活着");
    }
    public void finalize() throws Throwable{
        super.finalize();
        System.out.println("执行finalize方法");
        FinalizeEscapeGC.SAVE_HOOK=this;//自救
    }
    public static void main(String[] args) throws InterruptedException{
        SAVE_HOOK=new FinalizeEscapeGC();
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("1 ");
        }else{
            System.out.println("1 no我死了");
        }
        //对象的第一次回收
        SAVE_HOOK=null;
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("2 ");
        }else{
            System.out.println("2 no我死了");
        }
        System.gc();
        //因为finalize方法的优先级很低所以暂停0.5秒等它
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("3 ");
        }else{
            System.out.println("3 no我死了");
        }
        //下面的代码和上面的一样,但是这次自救却失败了
        //对象的第一次回收
        SAVE_HOOK=null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("4 ");
        }else{
            System.out.println("4 no我死了");
        }
    }
}

二. 市面上的GC回收器

垃圾回收器 作用区域 回收算法 是否会中断其他线程
Serial 新生代 复制
ParNew 新生代 标记复制
Parallel Scavenge 新生代 标记复制
Serial Old 年老代 标记复制
Parallel Old 年老代 标记整理
CMS(Concurrent Mark Sweep) 年老代 标记清除
G1 新生代,年老代 标记整理 看情况

        G1垃圾回收算法,以标记整理作为主要算法,但是和其他的标记清除不太一样。G1并不是针对内存块,而是针对内存区域(Region)。G1不再将年老代、新生代进行分类存放,而是以标记的形式,给一块区域定义为年老代或者新生代。

        G1垃圾回收算法是否会中断其他线程,看情况,内部执行流程中一些会中断用户线程,一些不会。

            ​​​​​​​    ​​​​​​​    ​​​​​​​    ​​​​​​​    

三. 不同的GC对比

        1. Serial 回收器

            1. 执行流程

                

            2. 特点

                1. 单线程处理

                2. HotSpot中client模式下的默认新生代垃圾收集器

                3. 执行GC时会暂停其他所有线程 ParNew 回收器

    2. ParNew 垃圾回收器

        1. 执行流程

                

        2. 特点

                1. 多线程实现垃圾回收,默认线程数和CPU核心数一致

                2. 是Server模式下首选的垃圾回收器

                3. 是除了Serial回收器之外,能和CMS回收器搭配的回收器 Parallel Scavenge回收器

    3. Parallel Scavenge回收器

            1. 执行流程

                与ParNew 回收器类似,也是多线程垃圾回收器                

            2. 特点

                1. 吞吐量有限的处理器,以达到一个可控制的吞吐量为标准进行垃圾回收的处理器

                2. 自适应的调节策略(+UseAdaptiveSizePolicy), 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX: MaxGCPauseMillis参数(更关注最大停顿时间)或-XX: GCTimeRatio(更关注吞吐量) 参数给虚拟机设立一个优化目标, 那具体细节参数的调节工作就由虚拟机完成了。 Serial Old 回收器

        4. Serial 垃圾回收器

            1. 执行流程

                    

            2. 特点

                1. 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用

                2. 在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用 (Parallel Old 诞生以前)

                3. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 Parallel Old回收器

        5. Parallel Old 垃圾回收器

            1. 执行流程

                

            2. 特点

                1. Parallel Scavenge收集器的老年代版本

                2. 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 CMS 回收器

        6. CMS 垃圾回收器

            1. 执行流程

                    

            2. 特点

                1. 并发清除,可以和用户线程同步执行

                2. 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高

                3. 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

                4. 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 G1 垃圾回收器

        7.  G1垃圾回收器

            1. 执行流程

                

            2. 特点

                1. 空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。

                2. 可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

四. 内存分配与回收策略(留个坑,这两块内容比较多)

        · G1回收器与ParNew回收器的性能对比

        · JDK17默认的ZGC

参考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章