【基础篇】十四、GC算法

发布时间:2024年01月05日

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、标记整理算法

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

实现:

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

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

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

缺点:

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

6、分代算法Generational GC

组合使用了上面的几种算法,被主流使用。分代即把内存分为年轻代和老年代:

在这里插入图片描述

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

在这里插入图片描述

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的存活对象放入S1(To),Eden区被清空(复制算法)

在这里插入图片描述

  • 互换名,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还是满,再来对象,尽管新生代有的对象没到达年龄阈值,也会被搬到老年代

文章来源:https://blog.csdn.net/llg___/article/details/135386910
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。