ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【JVM】一篇通关JVM垃圾回收

2026/9/26 7:12:21 拓冰建站 浏览量
【JVM】一篇通关JVM垃圾回收

目录

  • 1. 如何判断对象可以回收
    • 1-1. 引用计数法
    • 1-2. 可达性分析算法
    • 1-3. 四种引用
      • 强引用
      • 软引用
      • 弱引用
      • 虚引用
      • 终结器引用
  • 2. 垃圾回收算法
    • 2-1. 标记清除
    • 2-2. 标记整理
    • 2-3. 复制
    • 2-4. 总结
  • 3. 分代垃圾回收
    • 3-1. 回收流程
    • 3-2. VM参数分析
      • VM参数
      • 大对象处理策略
      • 线程内存溢出
  • 4. 垃圾回收器
    • 4-1. 串行
    • 4-2. 吞吐量优先
    • 4-3. 响应时间优先
    • 4-4. G1
      • G1垃圾回收阶段
      • Full GC
      • Young Collection 跨代引用
      • remark
  • 5. 垃圾回收调优
    • 5-1 调优领域
    • 5-2 确定目标
    • 5-3 最快的 GC
    • 5-4 新生代调优

1. 如何判断对象可以回收

1-1. 引用计数法

引用计数法

  • 只要一个对象被其他变量所引用,那么就让这个对象的计数+1
  • 如果其他变量不再引用,让这个对象的计数 -1
  • 让这个对象的引用计数为 0时,则说明没有变量引用了,就可以作为一个垃圾进行回收

引用计数法弊端

  • 循环引用问题,A对象与B对象循环引用,他们的引用计数始终为1,不能作为一个垃圾进行回收
  • 出现内存泄漏

在这里插入图片描述

1-2. 可达性分析算法

根对象:一些肯定不能作为垃圾的对象

可达性分析算法

  • 在垃圾回收之前,会对堆内存所有的对象进行扫描

  • 查看每一个对象是不是被根对象直接或间接引用

  • 如果是,则不能被垃圾回收

  • 如果不是,则可以被垃圾回收

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看是否能够沿着GC Root对象 (堆中对象) 为起点的引用链找到该对象,找不到,表示堆中对象可以回收

  • 哪些对象可以作为GC Root

    • 活动线程中局部变量所引用的堆中对象可以作为根对象

1-3. 四种引用

实线代表强引用

平时用的引用都是强引用,例如:赋值运算
在这里插入图片描述

强引用

  • 所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
  • Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收 obj = null; //手动置null后会被回收

软引用

  • 只有【软引用】引用该对象时,在垃圾回收后,内存仍不足 则会回收软引用对象
  • 可以配合【引用队列】来释放软引用自身,因为软引用自身也占用内存
  • 在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

★应用场景

  • 非核心业务资源(比如:图片)被强引用特别多时,有可能报OOM异常,因为强引用是不会被回收的,内存满直接抛异常
  • 那么就可以用【软引用】来指向这些资源,当内存不足时,回收这些资源。
  • 以后如果再使用图片资源,重新读取一遍。
  • 这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
  • 在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

弱引用

  • 只有【弱引用】引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
  • 可以配合【引用队列】来释放弱引用自身,因为弱引用自身也占用内存
  • 在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

虚引用

  • 必须配合【引用队列】使用
  • 例如 ByteBuffer 对象不再【强引用】时, ByteBuffer 对象本身可以被垃圾回收,但是占用的直接内存 是属于操作系统的,无法被回收。
  • 那么就可以将【虚引用】放入【引用队列】, 由 Reference Handler 线程调用虚引用相关方法释放【直接内存】
  • 如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存
  • 总结:【虚引用】引用的对象被垃圾回收时,【虚引用】被放入【引用队列】,从而由一个线程可以调用【虚引用】对象中的方法执行一系列操作。
  • 在 JDK1.2 之后,用 PhantomReference 类来表示

终结器引用

  • Object 类中有 finallize() 方法
  • 当一个类重写了Object 类中有 finallize() 方法,并且该对象没有被【强引用】时,就可以进行垃圾回收
  • 第一次垃圾回收时,将【终结器引用】放入【引用队列】,并且由一个优先级很低的Finalizer线程去寻找【终结器引用】的对象,找到后执行该对象的 finallize() 方法。
  • 直到第二次垃圾回收时,才将该对象进行垃圾回收。

软引用使用:

public class Demo1 {public static void main(String[] args) {final int _4M = 4*1024*1024;//list是强引用,byte数组是软引用List<SoftReference<byte[]>> list = new ArrayList<>();SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);}
}

软引用配合引用队列使用:

public static void main(String[] args) throws IOException {///使用引用队列,用于移除引用为空的软引用ReferenceQueue<byte[]> queue=new ReferenceQueue<>();List<SoftReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 5; i++) {//关联了引用队列,当软引用所关联的byte数组被回收时,软引用自己就会加入到引用队列queue中去SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}//获取队列中第一个软引用Reference<? extends byte[]> poll = queue.poll();//遍历引用队列,如果有软引用,则移除while(poll!=null){list.remove(poll);poll=queue.poll();}System.out.println("=============");System.out.println("循环结束:" + list.size());for (SoftReference<byte[]> ref : list) {System.out.println(ref.get());}
}

弱引用使用:

弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference

public static void main(String[] args) {//list是强引用,byte数组是弱引用List<WeakReference<byte[]>> list=new ArrayList<>();for (int i = 0; i < 5; i++) {WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB]);list.add(ref);for (WeakReference<byte[]> w : list) {System.out.print(w.get()+" ");}System.out.println();}System.out.println("循环结束:"+list.size());
}

2. 垃圾回收算法

2-1. 标记清除

在这里插入图片描述
标记清除

在垃圾回收的过程中

  • 标记:确定哪些对象是可回收对象,
  • 清除:标记好之后,清除可回收对象的内存,并不是将内存空间字节清零,而是记录内存起始地址。

注意:这里的清除并不是将内存空间字节清零,而是记录这段内存的起始地址,下次分配内存的时候,会直接覆盖这段内存。

优点: 速度快

缺点: 容易产生内存碎片。一旦分配较大内存的对象,由于内存不连续,导致无法分配,最后就会造成内存溢出问题

2-2. 标记整理

在这里插入图片描述

标记整理

  • 先采用标记算法确定可回收对象
  • 然后整理剩余的对象内存,将可用的对象内存移动到一起,使内存更加紧凑,连续的空间就更多。

优点:不会有内存碎片

缺点:速度慢

2-3. 复制

在这里插入图片描述
在这里插入图片描述

复制算法

  • 将内存分为等大小的两个区域,FROM和TO(TO中为空)。
  • 将被GC Root引用的对象从FROM放入TO中,然后回收不被GC Root引用的对象。
  • 回收完之后交换FROM和TO两个区域。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

优点:不会有内存碎片

缺点:会占用双倍的内存空间。速度慢

2-4. 总结

  • JVM会根据不同的情况来采用这3种算法
  • 不会只使用一种算法

3. 分代垃圾回收

堆内存分为新生代和老年代,新生代有划分为伊甸园,幸存区To,幸存区From。

新生代

  • 主要存放刚创建的对象

老年代

  • 主要存放长时间使用的对象

新生代和老年代会进行不同的垃圾回收算法

在这里插入图片描述

3-1. 回收流程

新创建的对象分配在伊甸园

在这里插入图片描述

如果新生代空间不足时,触发 Minor GC(minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行)回收伊甸园和 幸存区from 中的垃圾,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from 和 to,并让幸存区from中的对象寿命加1

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit) 不同的垃圾回收器阈值会不同

在这里插入图片描述

如果老年代空间不足,会先尝试触发Minor GC,回收新生代的垃圾。如果之后空间仍不足,那么触发 Full GC,回收新生代和老年代的垃圾,stop the world的时间更长

在这里插入图片描述

如果 Full GC之后,空间仍不足。则触发OutOfMemory - Java heap space0000000

3-2. VM参数分析

VM参数

含义参数
堆初始大小-Xms
堆最大大小-Xmx 或 -XX:MaxHeapSize=size
新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGCXX:+ScavengeBeforeFullGC

在这里插入图片描述

大对象处理策略

遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

public class Main {private static final int _512KB = 512 * 1024;private static final int _1MB = 1024 * 1024;private static final int _6MB = 6 * 1024 * 1024;private static final int _7MB = 7 * 1024 * 1024;private static final int _8MB = 8 * 1024 * 1024;// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGCpublic static void main(String[] args) throws InterruptedException {ArrayList<byte[]> list=new ArrayList<>();list.add(new byte[_8MB]);}}

在这里插入图片描述

线程内存溢出

某个线程的内存溢出了而抛异常(out of memory),不会影响其他的线程结束运行。这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,其他进程依然正常运行。

public class Main {private static final int _512KB = 512 * 1024;private static final int _1MB = 1024 * 1024;private static final int _6MB = 6 * 1024 * 1024;private static final int _7MB = 7 * 1024 * 1024;private static final int _8MB = 8 * 1024 * 1024;// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGCpublic static void main(String[] args) throws InterruptedException {new Thread(() -> {ArrayList<byte[]> list = new ArrayList<>();list.add(new byte[_8MB]);list.add(new byte[_8MB]);}).start();//主线程还是会正常执行System.out.println("sleep....");Thread.sleep(1000L);}
}

在这里插入图片描述

4. 垃圾回收器

串行垃圾回收器

  • 是单线程
  • 适合堆内存较小,个人电脑

吞吐量优先垃圾回收器

  • 是多线程
  • 适合堆内存较大,多核cpu
  • 吞吐量优先,即让单位时间内,STW(Stop the World)的时间最短
  • 例如:1h内发生了2次垃圾回收,0.2 + 0.2 = 0.4 总耗时0.4

响应时间优先垃圾回收器

  • 是多线程
  • 适合堆内存较大,多核cpu
  • 响应时间优先,即尽可能让单次STW的时间最短
  • 例如:1h内发生了5次垃圾回收,0.1 + 0.1 +0.1 + 0.1 + 0.1 = 0.5 总耗时0.4,单次STW 0.1

4-1. 串行

开启串行垃圾回收器参数:

XX:+UseSerialGC = Serial + SerialOld

Serial 收集器:

  • 单线程收集器。采用复制算法。工作在新生代

Serial Old收集器:

  • 单线程收集器。采用标记-整理算法。工作在老年代

在这里插入图片描述

安全点:即发生垃圾回收时,让所有线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。

阻塞:因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

4-2. 吞吐量优先

开启吞吐量优先回收器:

JDK1.8默认使用的垃圾回收器

在这里插入图片描述
在这里插入图片描述

Parallel 收集器:

  • 定义:与吞吐量关系密切,故也称为吞吐量优先收集器
  • 特点:并行的,工作于新生代,采用复制算法

Parallel Old 收集器:

  • 定义:是Parallel 收集器的老年代版本
  • 特点:并行的,工作在老年代,采用标记-整理算法

4-3. 响应时间优先

在这里插入图片描述

  • 初始标记:标记GC Roots能直接到的对象。速度很快,存在Stop The World
  • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。存在Stop The World
  • 并发清理:对标记的对象进行清除回收

CMS收集器:

  • 定义:Concurrent Mark Sweep(并发,标记,清除)
  • 特点:基于标记-清除算法的垃圾回收器。是并发的。工作在老年代。

ParNew 收集器:

  • 定义:ParNew收集器其实就是Serial收集器的多线程版本
  • 特点:工作在新生代,基于复制算法的垃圾回收器。

4-4. G1

Garbage First

  • JDK 9以后默认使用,而且替代了CMS 回收器

适用场景:

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是 标记+整理 算法,两个Region之间是 复制 算法

开启G1参数:

  • JDK8 并不是默认开启的,所以需要参数开启

在这里插入图片描述

G1垃圾回收阶段

Young Collection: 对新生代的垃圾回收
Young Collection + Concurrent Mark: 对新生代的垃圾回收 同时执行并发标记
Mixed Collection: 对新生代老年代进行垃圾回收
在这里插入图片描述

Young Collection

  • G1会把堆内存划分成大小相等的一个个区域,每个区域都可以独立作为伊甸园、幸存区、老年代
  • 白色:空闲区域、绿色E: 伊甸园、幸存区S
  • 存在STW

在这里插入图片描述

  • 当伊甸园内存不足时,触发新生代垃圾回收
  • 将存活的对象以复制算法放入到幸存区S

在这里插入图片描述

  • 当幸存区的内存不足时,并且幸存区对象的年龄超过阈值,触发新生代垃圾回收,将幸存区的一部分对象晋升老年代

Young Collection + CM

  • CM:并发标记
  • 在 Young GC 时会对 GC Root 进行初始标记
  • 当老年代占用堆内存的比例达到阈值时,对进行并发标记(不会STW),阈值可以根据用户来进行设定
  • 默认45%,当老年代占堆内存的45%则会进行并发标记
    在这里插入图片描述

Mixed Collection

会对E S O 进行全面的回收

  • 最终标记(Remark)会STW,最终标记是因为并发标记时其他线程还在运行,所以有可能产生新的垃圾,所有要暂停其他线程进行最终标记。
  • 拷贝存活(Evacuation)会STW,拷贝存活就是在最终标记完后,将存活的对象拷贝到指定的区域
  • -XX:MaxGCPauseMills:xxx :用于指定G1垃圾回收时最长的停顿时间

为什么有的老年代被拷贝了,有的没拷贝?

  • 因为指定了最大停顿时间,如果对所有老年代都进行回收(复制算法),耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
    在这里插入图片描述

Full GC

  • SerialGC

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
      • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
      • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC
  • G1

    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足(老年代所占内存超过阈值)
      • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
      • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC

如果触发了Full GC,打印日志中会显示Full GC (开启GC参数的前提下)

Young Collection 跨代引用

新生代回收的跨代引用(老年代引用新生代)问题

  • 卡表:老年代被划为一个个卡表
  • Remembered Set:Remembered Set 存在于E(新生代)中,用于保存新生代对象对应的脏卡
  • 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过post-write barried + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

在这里插入图片描述

remark

黑色:已被处理,有引用在引用,即存活对象

灰色:正在处理中的对象

白色:还未处理的对象

在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark

  • 之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态
  • 在并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它

在这里插入图片描述

JDK 8u20 字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

案例

String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串(底层是char[])放入一个队列
  • 当新生代回收时,G1并发检查是否有重复的字符串
  • 如果字符串的值一样,就让他们引用同一个字符串对象
  • 注意,其与String.intern的区别:
    • intern关注的是字符串对象
    • 字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark 默认启用

JDK 8u60 回收巨型对象

  • JDK 8u60 回收巨型对象一个对象大于 region 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑回收巨型对象
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0的巨型对象就可以在新生代垃圾回收时处理掉

在这里插入图片描述

JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

5. 垃圾回收调优

5-1 调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • IO
  • GC

5-2 确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1,ZGC (低延迟,响应时间优先)
  • ParallelGC
  • Zing

5-3 最快的 GC

查看Full GC前后的内存占用,考虑以下几个问题:

  • 数据是不是太多?
    • resultSet = statement.executeQuery("select * from 大表")
  • 数据表示是否太臃肿?
    • 对象图
    • 对象大小
  • 是否存在内存泄漏?

5-4 新生代调优

  • 新生代的特点:

    • 所有的new操作分配内存都是非常廉价的
      • TLAB thread-local allocation buffer(可防止多个线程创建对象时的干扰)
    • 死亡对象回收零代价
    • 大部分对象用过即死(朝生夕死)
    • MInor GC 所用时间远小于Full GC
  • 新生代内存越大越好么?

    • 不是

      • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
      • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长
    • 新生代内存设置为能容纳 [并发量x(请求-响应)] 的数据为宜

    • 幸存区大到能保留【当前活跃对象+需要晋升对象】

    • 晋升阈值配置得当,让长时间存活对象尽快晋升

    • 吞吐量 = 程序运行时间 / 程序运行时间 + 垃圾回收时间