HotSpot虚拟机垃圾回收算法及收集器

目录

一、对象引用

二、堆区和方法区回收

1. 堆区回收

2. 方法区回收 

三、垃圾回收算法

1. 算法总结

2. 算法相关细节 

四、垃圾收集器 

1. 新生代收集器

2. 老年代收集器

3. 混合式收集器G1

4. 低延迟收集器

五、参考资料


一、对象引用

        判定对象是否存活和引用离不开关系。JDK 1.2之后,有四种引用:强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),其引用强度依次减弱(强软弱虚)

        若下代码所示,是四种引用的使用方式。

package com.cmmon.instance.gc;

import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.Test;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

/**
 * @author tcm
 * @version 1.0.0
 * @description JDK1.2的4种引用
 * @date 2023/5/24 17:25
 **/
public class ReferenceTest {

    /**
     * 强引用:new的对象,只要有引用链,则GC永远不会回收
     */
    @Test
    public void testStronglyReference() {
        // new创建对象,是强引用
        Map map = new HashMap<>();
        map.put("name", "stronglyReference");

        System.out.println(JSON.toJSONString(map.keySet()));
    }

    /**
     * 软引用:内存不足时,GC会回收SoftReference所引用的对象
     * 适用场景:缓存
     */
    @Test
    public void testSoftReference() throws InterruptedException {
        // 软引用
        SoftReference> softReference = new SoftReference>(new HashMap<>());

        Map map = softReference.get();
        if (map == null) {
            // 下面代码,这样做,仍然可能为null(概率很小)
            softReference = new SoftReference>(new HashMap<>());
        }
        map = softReference.get();

        map.put("name", "test");
        System.out.println(JSON.toJSONString(map.keySet()));
    }

    /**
     * 弱引用:内存是否足够都要回收,则只能生存到下一次GC回收WeakReference所引用的对象
     * 适用场景:Debug、内存监视工具等程序中(一般要求即要观察到对象,又不能影响该对象正常的GC过程)
     */
    @Test
    public void testWeakReference(){
        WeakReference> weakReference = new WeakReference>(new HashMap<>());
        Map map = weakReference.get();

        if (map == null) {
            // 下面代码,这样做,仍然可能为null(概率很小)
            weakReference = new WeakReference>(new HashMap<>());
        }
        map = weakReference.get();

        map.put("name", "test");
        System.out.println(JSON.toJSONString(map.keySet()));
    }

    /**
     * 虚引用:无法通过虚引用来获取一个对象实例,目的回收时通知系统
     * 以下代码报错:java.lang.NullPointerException
     */
    @Test
    public void testPhantomReference(){
        PhantomReference> phantomReference = new PhantomReference>(new HashMap<>(), new ReferenceQueue());
        Map map = phantomReference.get();

        if (map == null) {
            phantomReference = new PhantomReference>(new HashMap<>(), new ReferenceQueue());
        }
        map = phantomReference.get();
        map.put("name", "test"); // 报空指针异常
        System.out.println(JSON.toJSONString(map.keySet()));
    }

}

二、堆区和方法区回收

1. 堆区回收

        对象“死去”,说明没有任何引用指向该对象,即:对象没有被任何引用使用。那么,如何判定一个对象是否存活呢?对象存活算法有两种:引用计数算法、可达性分析算法(目前使用),如下图所示。

        目前HotSpot虚拟机采用可达性分析算法来判定对象是否存活,某对象没有任何引用链(Reference Chain) ,则对象“死去”,如下图所示。

HotSpot虚拟机垃圾回收算法及收集器_第1张图片

        根据上述,哪些内容可作为GC Roots?如下图所示。

HotSpot虚拟机垃圾回收算法及收集器_第2张图片

        同时,准确且完整的GC Roots至关重要。目前所有收集器在进行枚举GC Roots对象时,都需要暂停用户线程来获取整个堆区原始快照(SATB:Snapshot At The Beginning),以达到GC Roots的准确性;不同的收集器,存在跨代和跨区引用问题(记忆集解决),以达到完整。详细见收集器章节。

        一个对象最多经历两次标记,如下图所示。其中,finalize()方法在Object中,只能被调用一次,第二次调用时则失效,所以:拯救对象再次能活,可以在finalize()方法再次引用(不推荐使用),测试代码如下。

package com.cmmon.instance.gc;

/**
 * @author tcm
 * @version 1.0.0
 * @description TODO
 * @date 2023/4/19 9:53
 **/
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] space1 = new byte[4 * _1MB];

    public void isAlive() {
        System.out.println("是的,我还活着!");
    }

    /**
     * 系统只能调用一次,推荐不使用该方法
     * @throws Throwable
     */
    @Override
    protected 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();

        // 第一次自救,则成功
        SAVE_HOOK = null;
        /*
            注意:若使用-XX:+DisableExplicitGC禁止人工触发GC,
                 则不会触发Full GC,更不会进入finalize()方法,SAVE_HOOK = null时,则已经死了
         */
        System.gc();
        Thread.sleep(1000 * 6);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("我已经死了!");
        }

        // 第二次自救,则失败
//        SAVE_HOOK = null;
//        System.gc();
//        Thread.sleep(5000);
//        if (SAVE_HOOK != null) {
//            SAVE_HOOK.isAlive();
//        } else {
//            System.out.println("2我已经死了!");
//        }
    }

    /*
    执行结果:
        [GC (System.gc()) [PSYoungGen: 4592K->1173K(37888K)] 4592K->1181K(123904K), 0.0283448 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]
        [Full GC (System.gc()) [PSYoungGen: 1173K->0K(37888K)] [ParOldGen: 8K->1042K(86016K)] 1181K->1042K(123904K), [Metaspace: 3060K->3060K(1056768K)], 0.0072510 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
        进入finalize()方法
        是的,我还活着!
        [GC (System.gc()) [PSYoungGen: 1310K->64K(37888K)] 2353K->1106K(123904K), 0.0005835 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
        [Full GC (System.gc()) [PSYoungGen: 64K->0K(37888K)] [ParOldGen: 1042K->1039K(86016K)] 1106K->1039K(123904K), [Metaspace: 3061K->3061K(1056768K)], 0.0057221 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
        2我已经死了!
        Heap
         PSYoungGen      total 37888K, used 1638K [0x00000000d6380000, 0x00000000d8d80000, 0x0000000100000000)
          eden space 32768K, 5% used [0x00000000d6380000,0x00000000d6519b40,0x00000000d8380000)
          from space 5120K, 0% used [0x00000000d8880000,0x00000000d8880000,0x00000000d8d80000)
          to   space 5120K, 0% used [0x00000000d8380000,0x00000000d8380000,0x00000000d8880000)
         ParOldGen       total 86016K, used 1039K [0x0000000082a00000, 0x0000000087e00000, 0x00000000d6380000)
          object space 86016K, 1% used [0x0000000082a00000,0x0000000082b03fe0,0x0000000087e00000)
         Metaspace       used 3068K, capacity 4486K, committed 4864K, reserved 1056768K
          class space    used 324K, capacity 386K, committed 512K, reserved 1048576K
     */

}

2. 方法区回收 

        方法区回收主要是废弃的常量、不再使用的类型。需要主要的是:JDK7版本方法区内的常量池、静态变量移至堆中;JDK8使用元空间(Metaspace)代替永久代,主要存储类信息。

HotSpot虚拟机垃圾回收算法及收集器_第3张图片

三、垃圾回收算法

1. 算法总结

        如下表所示,垃圾回收算法总结。

算法

特点

标记-清除

(Mark-Sweep)

1.标记存活的对象,统一回收所有未被标记的对象

2.最基础的收集算法;

3.缺点:内存碎片化,大对象分配时没有足够连续空间,则提前触发GC

              对象数量增加时,标记和清除两个过程执行效率低

4.适用:老年代;

5.采用该算法收集器:CMS。

标记-复制

(Mark-Copy)

1.“半区复制”:内存按大小相等分两块

              (使用一块;存活对象复制到另一块);

2.“Appel式回收”:新生代分为一个Eden区、两个Survivor区

   (每次可用一个Eden区、一个Survivor区;存活对象复制到另一个Survivor区)

3.缺点:“半区复制”内存浪费大,缩小一半

              “Appel式回收”,默认90%可用(Eden:Survivor=8:1)

4.适用:新生代;

5.采用该算法收集器:Serial、ParNew。

标记-整理

(Mark-Compact)

1.标记后不直接清理,而是所有存活对象向内存空间一端移动(整理)后,再清除

2.整理时,需要移动对象,则必须暂停用户线程才能进行

 (注意:最新的ZGC、Shenandoah收集器使用读屏障解决该问题);

3.缺点:移动对象时,需暂停用户线程(Stop the World);效率低

4.适用:老年代;

5.采用该算法收集器:Parallel Old。

        需要注意的是,分代收集是上面三种算法的混合使用。分代收集存在跨代引用问题,用记忆集(Remembered Set)解决。如下图所示,是分代收集假说及注意问题。

HotSpot虚拟机垃圾回收算法及收集器_第4张图片

2. 算法相关细节 

        算法的实现细节,如:GC Root枚举、安全点、安全区域、记忆集、写屏障、三色标记等,如下如图所示。 

HotSpot虚拟机垃圾回收算法及收集器_第5张图片

        其中,从GC Roots扫描整个堆的对象图时,按照“是否访问过” 用三种颜色标记。标记整个堆的对象是很耗时,用户线程停顿时,导致用户体验差。那么,与用户线程并发来进行可达性分析时会出现问题,如下图所示。

HotSpot虚拟机垃圾回收算法及收集器_第6张图片

        如下图所示,并发时的解决方案。

四、垃圾收集器 

        下图所示是HotSpot虚拟机的垃圾收集器,图中连线说明可搭配使用。其中JDK9废弃Serial + CMS、ParNew + Serial Old。

HotSpot虚拟机垃圾回收算法及收集器_第7张图片

        垃圾收集器三大指标:内存占用(Footprint)、吞吐量(ThroughPut)、延迟(Latency),一款优秀的收集器最多同时达成两项。而理想中的收集器其收集速率与内存分配速率相当,而不是整堆清除

        虚拟机吞吐量(ThroughPut) =  CPU运行用户代码时间 / ( CPU运行用户代码时间 + CPU运行垃圾回收时间 )。停顿时间短适合用户交互或保证服务响应的程序,提升交互体验;吞吐量高适合运算量大的服务,提高利用资源效率。

        各类收集器并发情况,如下图所示。Shenandoah、ZGC两款只有初始标记、最终标记这部分是固定的,与堆容量、堆的对象数量没有关系。

HotSpot虚拟机垃圾回收算法及收集器_第8张图片

1. 新生代收集器

新生代收集器

特点

Serial

1.JDK1.3之前新生代收集器;

2.单线程收集,整个GC过程必须暂停用户线程;

3.算法:标记-复制;

4.缺点:整个GC过程必须暂停用户线程。

ParNew

1.JDK5新生代收集器;

2.多线程收集,整个GC过程必须暂停用户线程;

3.算法:标记-复制;

4.缺点:整个GC过程必须暂停用户线程;

5.与Serial比较:多线程,其他都一样;

6.只与CMS老年代收集器联合使用

Parallel Scavenge

1.JDK7新生代收集器;

2.多线程收集,整个GC过程必须暂停用户线程

3.算法:标记-复制;

4.缺点:整个GC过程必须暂停用户线程;

5.Parallel Scavenge关注点是达到可控的吞吐量;

   CMS关注点尽可能缩短停顿时间。

2. 老年代收集器

HotSpot虚拟机垃圾回收算法及收集器_第9张图片

3. 混合式收集器G1

HotSpot虚拟机垃圾回收算法及收集器_第10张图片

4. 低延迟收集器

        低延迟收集:Shenandoah(OpenJDK支持)、ZGC(OracleJDK12支持),这两款收集器抛弃了分代收集、记忆集等概念。

五、参考资料

JVM垃圾收集器(很全很全) - 简书

关于垃圾收集算法与垃圾收集器ParNew与CMS_parnew收集器_秋天的一亩三分地的博客-CSDN博客

详解ZGC垃圾收集器-CSDN博客

69.G1垃圾回收的详细过程 -了解_simpleGq的博客-CSDN博客

G1垃圾回收器_赵军林的博客-CSDN博客

垃圾收集算法——分代收集算法(Generational Collection)。_分代收集算法根据各个年代的特点采用最适当的收集算法_孤芳不自賞的博客-CSDN博客https://www.cnblogs.com/yufengzhang/p/10571081.html 

为对象分配内存——TLAB_usetlab_chengqiuming的博客-CSDN博客

JVM调优常用参数_printreferencegc_point-break的博客-CSDN博客

你可能感兴趣的:(JVM,垃圾回收算法,JVM,垃圾收集器,G1,ZGC)