
垃圾回收
为什么要垃圾回收?
JVM 堆内存空间有限,需要把无用对象回收掉,相比于 C++ 手动回收,java 提供了垃圾回收机制。
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存爆掉。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
垃圾回收过程
JVM 先标记垃圾,再回收垃圾

如何标记垃圾?
引用计数法
给对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
问题:无法解决循环引用问题
可达性分析法
是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
但其实一个对象被标记之后也不是一定会被回收,会有一个缓刑截断,这时因为 finalize 机制的存在可能会导致对象复活:
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
即使在可达性分析算法中不可达的对象,其实也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
判定一个对象是否可回收,至少要经历两次标记过程:
- 第一次标记:如果对象objA到GC Roots没有引用链,则进行第一次标记。进行筛选,判断此对象是否有必要执行finalize()方法如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
- finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
jdk9 开始 finalize 方法逐渐被弃用: 参见 Weixin Official Accounts Platform
什么是 GC Roots:
所谓的 GC Roots,就是一组必须活跃的引用,不是对象,它们是程序运行时的起点,是一切引用链的源头:
- 虚拟机栈中的引用(方法的参数、局部变量等)
- 本地方法栈中 JNI 的引用
- 类静态变量
- 运行时常量池中的常量(String 或 Class 类型)
引用类型
- 强引用,默认引用就是强引用,具备强引用的对象不会被回收
- 软引用,(只有软引用及低于次级别的引用存在时)内存不足时可被回收,因此可以用作缓存
- 弱引用,(只有弱引用及低于次级别的引用存在时)会在下次垃圾回收是被回收(ThreadlocalMap key 就是弱引用)
- 虚引用,任何时候都可能会被回收,用的不多,主要用来跟踪对象被垃圾回收的活动
如何清理垃圾?
- 标记清除算法:标记完后直接删除,一个一个删除效率不高,且会导致内存碎片化问题
- 标记复制算法:把空间分成两份,把存活对象复制到另一个区域,效率高解决了内存碎片话问题,但是空间浪费,适合新生代
- 标记整理:把存活对象移动到头部,性能低但解决了碎片问题,适合老年代回收算法
分代回收算法
分代回收算法只是一种思想,注意和具体的垃圾回收器区分,jdk 的垃圾回收器采用了这种分代的思想,但最新的 ZGC 就不符合分代思想
具体过程参考这个图:
Minor GC、 Major GC、Mixed GC、Full GC 秦哦呃
- Partial GC:并不收集整个 GC 堆的模式
- Young GC:只收集 young gen 的 GC
- Old GC:只收集 old gen 的 GC。只有 CMS 的 concurrent collection 是这个模式
- Mixed GC:收集整个 young gen 以及部分 old gen 的 GC。只有 G1 有这个模式
- Full GC:收集整个堆,包括 young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
垃圾回收器
名称 | 说明 | 收集模式 | 分代适用类型 |
---|---|---|---|
Serial | 单线程串行收集器 | 串行收集器 | 新生代 |
Serial Old | Serial 单线程收集器老年代版本 | 串行收集器 | 老年代 |
Parallel Scavenge | 并行吞吐量优先收集器 | 并行收集器 | 新生代 |
Parallel Old | Parallel Scavenge 并行收集器老年代版本 | 并行收集器 | 老年代 |
ParNew | 多线程并行 Serial 收集器 | 并行收集器 | 新生代 |
CMS (Concurrent Mark Sweep) | 并行最短停顿时间收集器 | 并发收集器 | 老年代 |
G1 | 面向局部收集和基于 Region 内存布局的新型低延时收集器 | 并发/并行收集器 | 新生代/老年代 |
ZGC | 不分代,延迟低,但吞吐量做了取舍 | 并行收集器 | 不分代 |
分代回收器
⭐Serial + Serial Old
1.0 - 1.3 时期默认
早期采用 Serial 回收新生代,Serial Old 回收老年代,是串行垃圾回收器,stw 时间长,性能低。
⭐Parallel Scavenge + Parallel Old
1.4 - 1.8 时期默认
Parallel Scavenge(新生代)+ Parallel Old(老年代),目标是达到一个可控制的吞吐量。通过-XX: MaxGCPauseMillis 来设置收集器尽可能在多长时间内完成内存回收,通过-XX: GCTimeRatio 来控制吞吐量(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。
⭐ParNew + CMS
Jdk 1.4.2 被引入,作为可选项使用。
ParNew 其实就是多线程版本的 Serial 回收器,目标是尽可能缩短垃圾收集时用户线程的停顿时间。用于新生代垃圾回收
CMS 用作老年代垃圾回收,配合 ParNew 使用,基于三色标记 ==标记清除==算法,关注最短停顿时间,四个步骤:
- 初始标记标记和 root 直接相连的对象(标记的是存活对象)==需要停顿==
- 并发标记 GC 和用户线程并发,记录所有可达对象==不需要停顿==
- 重新标记由于并发原因,第二部可能有漏标(本来没有引用,并发过程中有了),这里进行修正==需要停顿==
- 并发清除 ==不需要停顿==
CMS 采用的是三色标记法来进行垃圾标记:
优点:
- 并发收集,停顿时间短
缺点: - 碎片化
- CPU 资源敏感
- 无法处理浮动垃圾(remark 只负责漏标,错标不管(初始时或者,并发的时候死了))对于CMS产生浮动垃圾和G1的一些理解
因为以上缺点,这个组合并没有被作为默认垃圾回收器过,在 Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。
⭐经久不衰的垃圾回收器——G1
G1 名为 Garbage-First,但因为和
G2
很像,我老是读成 G two 😄
G1 诞生于 Jdk 1.7, jdk 9 成为默认垃圾回收器,他的特点是可以配置停顿时间,虽然他还保留了分代的概念,但是他是分区进行回收的,会把堆空间分配成多个小块,而且还专门增加了一个用于存放大对象的区域。
他的原理和 CMS 很像,不过最后回收时会根据用户设置的停顿时间,选择一定数量的最具回收价值的区域进行回收,把存活对象复制到新区域。
G1 中存在三种 GC 模式,分别是 Young GC、Mixed GC 和 Full GC。
- 当 Eden 区的内存空间无法支持新对象的内存分配时,G1 会触发 Young GC。
- 当老年代中垃圾的比例达到一定值时会触发 mixed gc,回收年轻代和部分老年代的区域
- Mixed gc 时发现老年代空间还是不足的话,就需要触发 full gc
借助 -XX:MaxGCPauseMillis
来设置期望的停顿时间(默认 200ms),1 会根据这个值来计算出一个合理的 Young GC 的回收时间,然后根据这个时间来制定 Young GC 的回收计划。
这个视频中给出了 G1 垃圾回收器的一些调优策略,可以参考:
【IT老齐042】生产环境JVM与垃圾回收GC的一些配置建议_哔哩哔哩_bilibili
不分代回收器
⭐追求极致低延迟的垃圾回收器——ZGC
ZGC 不分代,暂停时间很短,而且暂停时间和堆内存大小无关,采用着色指针和读屏障来实现对象转移过程中用户线程仍然可以读取到对象。(具体细节有点复杂) ZGC 延迟低,但吞吐量会下降,目前应用不是很多,调参经验比较少,之前看到过美团技术团队的文章讲到了利用 ZGC 来降低延迟。
ZGC 适用于低延迟引用,但是还是不够完善和通用,因此现在默认的垃圾回收器还是 G1.
原理和使用可以参考美团技术文章:新一代垃圾回收器ZGC的探索与实践 - 美团技术团队
注意:java 21 中为 ZGC 增加了分代回收功能,并在 Java 23 中默认使用分代回收,未来将弃用不分代的 ZGC
参考资料