【基础篇】十四、GC算法

本文详细介绍了Java垃圾回收的实现思路,重点讨论了StopTheWorld(STW)现象,以及SWT对用户体验的影响。通过对比标记清除、复制和标记整理等GC算法,阐述了分代GC如何通过将堆分为年轻代和老年代来优化性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、实现思路

Java实现垃圾回收的步骤:

  • 根据GC Root对象可达性分析,将内存中对象标记为存活的、可回收的
  • 处理可回收的对象,释放空间在这里插入图片描述

2、SWT

GC是在一个单独的线程,但不管JVM用哪种算法,都会存在一个阶段需要停止所有的用户线程,称Stop The World(STW),SWT大,用户用起来自然卡。

在这里插入图片描述

感受下SWT:

public class StopTheWorld {

    public static void main(String[] args) {
        /**
         * 启动用户线程和GC线程
         * 查看不同阶段用户线程的执行时间
         */
        new PrintTimeThread().start();
        new ClearThread().start();
    }
}

/**
 * 模拟用户代码,这里直接打印这段代码的执行耗时
 */
class PrintTimeThread extends Thread {
    @SneakyThrows  //lombok的try..catch
    @Override
    public void run() {
        long begin = System.currentTimeMillis();
        while (true) {
            long now = System.currentTimeMillis();
            System.out.println(now - begin);
            begin = now;
            Thread.sleep(100);
        }
    }
}

/**
 * 模拟GC线程
 */
class ClearThread extends Thread {
    @SneakyThrows
    @Override
    public void run() {
        List<byte[]> list = new LinkedList<>();
        while (true) {
            //存80个100M后就删除里面byte对象的强引用,垃圾回收释放
            if(list.size() >= 80){
                list.clear();
            }
            list.add(new byte[1024 * 1024 * 100]);
            Thread.sleep(100);
        }
    }
}

添加JVM参数,使用分代回收的垃圾回收器,输出GC详细信息,并限制堆最大10G:

-XX:+UseSerialGC -Xmx10g -verbose:gc

运行发现用户线程本来100ms左右的事儿,有时候会被拖到2000ms以上:

在这里插入图片描述

3、GC算法

对象回收算法的评价标准:

  • 吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间),值越大,性能越高
  • 最大暂停时间:SWT的最大值

在这里插入图片描述

  • 堆的使用效率:如复制算法只用一半空间

在这里插入图片描述

以上三个指标,不可兼得。各个算法各有长处,对应着不同的适用场景。

4、标记清除算法Mark Sweep GC

实现:

  • 从GC Root List开始,遍历引用链,找到可达对象,并标记
  • 清除没标记的对象

在这里插入图片描述

优点:

  • 实现简单,只需给对象维护个标记位

缺点:

  • 导致内存碎片化:从原本连续的内存空间,摘掉一些被回收的,得到一些碎片。如下回收了4+3+2,却连个5字节的对象都创建不了

在这里插入图片描述

  • 分配速度慢:由于内存碎片化,需要维护一个空闲链表记录可用空间,新对象来了每次都得往后遍历,找出一块合适大小的地儿安置

在这里插入图片描述

5、复制算法Copying GC

实现:

  • 堆内存一分为二,一半叫From,一半叫To
  • 新对象来了往From安置
  • GC时,把From的存活对象Copy到To

在这里插入图片描述

  • 清掉From,From和To名字互换,原来的To做为新的From安置新new的对象

完整例子:

  • 开始状态:

在这里插入图片描述

  • GC开始,把GC Root对象和可达的对象搬到To空间
    在这里插入图片描述

  • 清掉From空间,并把原来的To改为From空间
    在这里插入图片描述

一句话:将存活的对象搬运到另一块空间,清理掉当前空间,互换名字

优点:

  • 解决了内存碎片化:往To搬的时候,按连续地址往过码
  • 吞吐相比下面的标记整理算法要高:只需遍历一次存活对象。但不如标记-清除算法,因为后者不用给对象搬家

缺点:

  • 堆内存使用率低:安置新对象只能用50%的堆空间,另一半得留着To

5、标记整理算法Mark Compact GC

也称标记压缩,用来解决标记清除算法的内存碎片化缺点。

实现:

  • 从GC Root开始,遍历标记可达对象
  • 将可达的存活对象移动到堆的一端,清掉非存活的

在这里插入图片描述
优点:

  • 无内存碎片化问题:比标记清除多了一步整理
  • 堆内存利用率比复制算法高

缺点:

  • 理解阶段性能不高,得看整理阶段的实现算法

6、分代算法Generational GC

组合使用了上面的几种算法,被主流使用。分代即把内存分为年轻代和老年代(JDK8时,堆被分成两份,默认年轻代 : 老年代 = 1:2):

在这里插入图片描述

关于这几块空间的大小设置:

在这里插入图片描述

Demo:

public class Gc {
    @SneakyThrows
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int count = 0;
        while (true) {
            System.in.read();
            System.out.println(++count);
            list.add(new Byte[1024 * 1024]);
        }
    }
}

对应JVM的参数:

-XX:+UseSerialGC -Xms60m -Xmn20m -Xmx60m -XX:SurvivorRatio=3 -XX:+PrintGDetails

粗略计算:老年代60m - 20m = 40m,Eden除以随便一块s区 = 3,则Eden:s0:s1 = 12:4:4,使用阿尔萨斯执行memory验证:

在这里插入图片描述

7、分代的整体流程

堆区分年轻和老年代的思想 + 复制算法。详细流程:

  • 新new的对象,安置到堆的年轻代的伊甸园区

在这里插入图片描述

  • 伊甸园区满了以后,触发GC,仅是年轻代的GC(Minor GC、Young GC)
  • 即把Eden和S0(S0这会儿还没对象)的存活对象放入S1(To),Eden和S0区被清空(复制算法)

在这里插入图片描述

  • 互换名,S0做为To,S1做为From,再安置新对象,直到Eden和From满

在这里插入图片描述

  • 再次触发Minor GC,Eden和From存活对象放入S0,其余清掉回收(每次GC能活下来的,记录年龄,+1)

在这里插入图片描述

  • 对象GC年龄到达阈值(最大15,对象头里放着,默认值和垃圾回收器有关),晋升到老年代。(一直活着就别在From和To之间来回搬了)

在这里插入图片描述

  • 老年代最后也满了,新new的对象进来,先Minor GC,还是不足,再Full GC,对整个堆进行垃圾回收,此时的STW时间就比Minor GC时的SWT长一些了

在这里插入图片描述

  • Full GC后,无法回收老年代对象,再往老年代放,就OOM

在这里插入图片描述
补充:如果现在新生代已经满了,Minor GC还是满,再来对象,尽管新生代有的对象没到达年龄阈值,也会被搬到老年代(大对象也可能会提前晋升到老年代)

8、为什么分代GC把堆内存分为年轻代和老年代?📕

在这里插入图片描述
从上面的GC分代流程就可以看到一个最核心的点:只给年轻代GC,STW时间更短了,这是明摆着的好处。

答案:

  • 分代GC下,可以只进行Minor GC,不用每次Full GC,STW时间短
  • 开发者可以通过调整年轻代和老年代的比例来适应不同的服务场景,提高性能(对象用完即丢的,生命短的多,可以调大年轻代,目的就是少STW,非STW的,也能Minor就别Full)
  • 年轻代和老年代可以选择使用不同的算法,年轻代通常用复制算法、老年代则用标记清除或者标记整理

在这里插入图片描述

补充:

  • 很多对象都是new完很快就可以回收,比如一个个Vo
  • 老年代存放一直用的对象,比如Spring容器里的一些Bean
  • JVM默认设置下,新生代空间远小于老年代

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-代号9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值