【JVM】垃圾回收机制、算法以及垃圾回收器
当JVM监测到堆内存不足时、手动调用System.gc()时、对象数量或内存使用达到阈值时会进行垃圾回收。
一、垃圾回收机制
1、minorgc后若放不下怎么办
- 年龄阈值判断:对象年龄达到
MaxTenuringThreshold(默认15)的对象直接晋升老年代 - 动态年龄计算:如果某个年龄段的对象总大小超过Survivor区的50%,该年龄段及以上的所有对象都晋升老年代
- 空间分配担保:当上述筛选后仍放不下时,检查老年代空间:
- 老年代剩余空间 > 所有存活对象 → 全部晋升老年代
- 老年代剩余空间 > 历史晋升平均值 → 尝试冒险晋升
- 老年代空间不足 → 触发Full GC
2、大对象直接放老年代
防止大对象导致新生代空间不足频繁minorgc,且复制移动对象也需要时间。
如何判断是否是大对象根据垃圾回收器决定Serial/ParNew、CMS默认只放新生代,可以参数配置。G1默认Region大小的一半是大对象。
3、垃圾判断方法
1、引用计数法
从根对象(静态属性引用的对象、方法区常量引用的对象、虚拟机栈中的对象、本地方法栈中的对象)。
三色标记法(可达性分析的具体实现):
- 白色:未被访问过的对象(最终被回收)
- 灰色:已被访问,但子引用未被完全遍历
- 黑色:已被访问,且所有子引用都被遍历
2、可达性分析法
为每个对象维护一个引用计数器,记录被引用的次数。当引用数为0时即可回收。会出现循环引用问题。需要额外存储计数信息。
二、垃圾回收算法
1. 标记-清除算法
标记-清除算法是垃圾回收的基础算法。它的工作分为两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
这种方法的主要问题是效率不高,因为标记和清除两个过程都需要遍历内存。更严重的是会产生大量不连续的内存碎片,当程序需要分配较大对象时,可能因为找不到足够的连续内存而触发另一次垃圾回收。
为什么低效:因为他需要访问内存两次,一次标记,一次去清除;其他两个是标记的时候直接复制了然后直接清除,只用访问一次内存。
2. 复制算法
为了解决标记-清除算法的碎片问题,复制算法被提出。它将可用内存按容量分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
这种算法的优点是解决了内存碎片问题,实现简单,运行高效。但代价是可用内存缩小为原来的一半,空间利用率较低。复制算法适用于对象存活率较低的场景,比如新生代的垃圾回收。
3. 标记-整理算法
老年代中对象的存活率通常较高,复制算法在这种情况下效率会很低。标记-整理算法应运而生,其标记过程与标记-清除算法一样,但后续步骤不同:它不是直接清理可回收对象,而是让所有存活的对象都向内存的一端移动,然后直接清理掉边界以外的内存。
这种方法既避免了内存碎片问题,又不需要像复制算法那样牺牲一半的内存空间。但它的缺点是移动存活对象需要更新所有引用,增加了开销,且移动过程中需要暂停用户线程(Stop The World)。
复制算法:对象相对位置不变,更新计算简单
标记-整理:对象完全重排,每个对象偏移不同,更新复杂
三、垃圾回收器详解
一、Serial回收器

Serial收集器是最基础的单线程垃圾回收器,采用串行执行方式。它包含两个部分:Serial用于新生代的垃圾回收,采用复制算法;Serial Old用于老年代的垃圾回收,采用标记-压缩算法。
在进行垃圾回收时,Serial回收器会完全暂停所有用户线程(Stop-The-World),这种"全世界暂停"的方式虽然简单可靠,但会导致应用程序出现明显的卡顿。由于是单线程执行,它无法充分利用多核处理器的优势。
Serial回收器适合内存较小的客户端应用或个人电脑环境,其设计简单、开销小,在小堆内存场景下表现稳定。
二、Parallel回收器

Parallel收集器(也称吞吐量优先收集器)是Serial收集器的多线程并行版本。它同样包含两个部分:Parallel Scavenge用于新生代,采用复制算法;Parallel Old用于老年代,采用标记-整理算法。
与Serial回收器类似,Parallel在进行垃圾回收时也需要暂停所有用户线程(STW),但它的优势在于使用多线程并行执行垃圾回收任务,从而大幅缩短了垃圾回收的停顿时间。Parallel收集器的设计目标是达到更高的吞吐量,适合后台运算较多、对响应时间不敏感的应用场景。
Parallel回收器在中等规模的内存配置下表现良好,能够有效利用多核CPU资源提升垃圾回收效率。
三、CMS回收器

CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的并发垃圾回收器使用标记清除法,专门用于老年代的垃圾回收。它允许在垃圾回收的大部分阶段与用户线程并发执行,显著减少了应用程序的停顿时间。
CMS执行流程
- 初始标记:标记GC Roots直接关联的对象,这个过程需要短暂STW。
- 并发标记:从初始标记的对象开始,并发地遍历整个对象图,此阶段与用户线程并发执行。
- 重新标记:修正并发标记期间因用户线程执行而产生的引用变化,需要再次STW。
- 并发清理:并发地清理标记的垃圾对象,与用户线程一起运行。
CMS的浮动垃圾问题
重新标记阶段虽然能解决"原本的垃圾被新引用"的问题(避免误删),但无法处理"原本被引用的对象变成垃圾"的情况。这些在并发标记阶段被标记为存活、但实际上已经成为垃圾的对象,会留到下一次GC才能被回收,这就是所谓的"浮动垃圾"问题。
CMS还面临内存碎片问题,长时间运行后可能因为碎片过多而触发Full GC。尽管如此,CMS在对响应时间敏感的服务端应用中仍有其价值。
四、G1回收器
G1最大的特点是引入分区的思路,弱化了分代的概念。

G1(Garbage-First)垃圾收集器从JDK 9开始成为默认收集器,它同时管理新生代和老年代。G1将堆内存划分为多个大小相等的Region(通常1MB-32MB),每个Region可以动态地扮演Eden、Survivor、Old或Humongous角色,其中Humongous区域专门用于存储大对象。
G1在各个Region内部采用复制算法进行垃圾回收,其核心设计目标是在指定的时间内获得最高的垃圾回收效率,通过智能的Region选择策略实现可控的暂停时间。
G1的三个核心阶段
新生代回收(Young Collection):初始状态下所有Region都处于空闲状态。G1会挑选一些空闲Region作为Eden区存储新对象,当Eden区需要垃圾回收时,会选择一个空闲Region作为Survivor区,使用复制算法转移存活对象。此阶段需要STW暂停。
并发标记(Concurrent Mark):当老年代占用超过阈值(默认45%)时,触发并发标记周期。这个阶段的大部分工作与用户线程并发执行,只有在初始标记和最终标记阶段需要短暂的STW暂停。
混合收集(Mixed Collection):并发标记完成后,G1开始混合收集阶段,同时回收部分新生代和老年代Region。G1不会一次性回收所有老年代Region,而是根据预设的暂停时间目标,优先回收那些存活对象少、回收价值高的Region。
G1的优势
G1通过Remembered Set(RSet)记录跨Region的引用关系,避免了全堆扫描。其分区设计和智能选择机制使得它能够在大堆内存场景下提供可预测的暂停时间,同时保持良好的吞吐量表现。G1的设计避免了传统标记-整理算法在离散Region上执行的高成本问题,更适合现代大规模服务端应用的需求。