news 2026/6/12 18:04:00

JVM垃圾收集算法与收集器深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM垃圾收集算法与收集器深度解析

前面我们吃透了 JVM 内存模型、对象创建机制、逃逸分析、内存分配规则,所有知识点最终都会汇聚到一个核心核心模块:垃圾收集(GC)

线上服务的GC卡顿、STW停顿、接口超时、FullGC频繁、OOM内存溢出,全部源于对 GC 底层算法、收集器特性、底层三色标记、卡表屏障机制理解不到位。

今天这篇文章,我们系统性打通 JVM 垃圾收集全套底层体系:分代收集理论、三大基础GC算法、四大经典收集器、三色标记底层实现、写屏障/读屏障/记忆集与卡表、亿级流量电商 ParNew+CMS 生产调优实战

全篇层层递进、从理论到底层、从原理到生产落地,看完彻底掌握 JVM GC 核心,搞定面试高阶提问与线上性能调优。


一、JVM 分代收集理论(所有GC的基石)

JVM 不使用统一的垃圾回收算法管理所有内存,而是基于对象存活生命周期不同,将对象内存分为几块,如年轻代和老年代,提出分代收集理论,针对性使用不同算法回收,实现性能最大化。

1.1 分代收集三大核心假说

  1. 弱分代假说:绝大多数对象都是朝生夕灭、短期存活对象。

  2. 强分代假说:存活越久的对象,越难被回收,生命周期更长。

  3. 跨代引用假说:跨代引用相对于同代引用极少存在。

基于三大假说,JVM 将堆内存划分为年轻代 + 老年代,针对性适配三种经典垃圾回收算法。

年轻代:对象消亡快、存活少 → 使用高效复制算法

老年代:对象存活久、存活多、内存空间大 → 使用清除/整理算法

1.1 标记-复制算法(年轻代核心算法)

1.1.1 核心原理

将内存区域划分为大小相等的两块内存,同一时间只使用其中一块。垃圾回收时:

  1. 标记当前使用区域中所有存活对象;

  2. 将所有存活对象完整复制到另一块空闲内存区域;

  3. 清空当前整块内存区域,完成垃圾回收;

  4. 交换两块内存的角色,下次GC轮换执行。

1.1.2 优缺点分析

优点

  • 无内存碎片,内存永远规整;

  • 只需复制存活对象,回收效率极高;

  • 适合存活对象少、垃圾对象多的场景。

缺点

  • 内存利用率极低,永久浪费 50% 内存空间;

  • 存活对象多的场景,复制开销极大、性能暴跌。

1.1.3 适用场景

HotSpot 年轻代专属算法,适配 Eden+Survivor 内存模型,通过 8:1:1 比例优化,规避50%内存浪费问题。

1.2 标记-清除算法(最基础算法)

1.2.1 核心原理

分为两个阶段:标记阶段 + 清除阶段

  1. 标记阶段:从GC Roots遍历所有可达对象,标记所有存活对象;

  2. 清除阶段:遍历整个内存区域,回收所有未标记的垃圾对象内存。

1.2.2 优缺点分析

优点

  • 算法逻辑简单、实现难度低;

  • 无需内存复制、无内存冗余浪费。

缺点

  • 产生大量内存碎片,导致大对象分配失败触发FullGC;

  • 两次全内存遍历,内存越大,GC耗时越长。

1.2.3 适用场景

早期经典算法,现代JVM不再单独使用,仅作为CMS回收算法的基础底层逻辑。

1.3 标记-整理算法(老年代核心算法)

1.3.1 核心原理

在标记清除基础上优化,分为标记 + 整理 + 清除三步:

  1. 标记所有存活对象;

  2. 将所有存活对象向内存一端统一平移整理

  3. 清空末端全部垃圾内存,保证内存连续规整。

1.3.2 优缺点分析

优点

  • 无内存碎片,内存空间规整;

  • 内存利用率100%,无空间浪费。

缺点

  • 需要移动大量存活对象,STW停顿时间最长;

  • 整体性能开销大,吞吐量较低。

1.3.3 适用场景

老年代专属算法,适配存活对象多、对象生命周期长、不适合复制的内存场景,Serial Old、G1 整理阶段核心算法。


二、主流垃圾收集器深度详解

算法是理论,收集器是落地实现。不同收集器封装了不同GC算法、适配不同业务场景,是生产调优的核心。常见的垃圾收集器有以下几种:

2.1 Serial 收集器(串行收集器)

2.1.1 简介

最古老、最基础的单线程垃圾收集器,全程单线程执行GC,用户线程全部暂停。

SerialOld收集器是Serial收集器的老年代版本,它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与ParallelScavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案

2.1.2 底层算法
  • 年轻代:标记-复制

  • 老年代:标记-整理(Serial Old)

2.1.3 优缺点

优点:简单高效(与其他收集器的单线程相比)、无线程切换开销、内存占用极小、适合客户端、低配置机器。

缺点:全程STW、单线程回收、高并发场景卡顿严重,不适合服务端生产

2.2 Parallel Scavenge 收集器(并行吞吐量收集器)

2.2.1 简介

Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

ParallelOld收集器是ParallelScavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑ParallelScavenge收集器和ParallelOld收集器(JDK8默认的年轻代和老年代收集器)。

作为JDK8默认年轻代收集器,多线程并行回收,核心目标是最大化系统吞吐量

吞吐量 = 运行用户代码时间 / (用户代码时间 + GC时间)

2.2.2 底层算法

年轻代:标记-复制算法,多线程并行执行。

老年代:标记-整理算法,多线程并行执行。

2.2.3 优缺点

优点:吞吐量极高、后台线程高效回收、适合后台批量任务、非实时业务。

缺点:不关注停顿时间,高峰期STW卡顿明显,不适合高并发、低延迟交易系统。

2.3 ParNew + CMS 收集器(低延迟经典组合)

2.3.1 ParNew 收集器简介

Serial收集器的多线程升级版,唯一可以配合CMS的年轻代收集器,多线程并行回收年轻代。设置参数为:-XX:+UseParNewGC

2.3.2 算法

标记-复制算法,多线程并行执行。

2.3.3 优缺点

优点:回收速度快、停顿短、适配低延迟场景、完美兼容CMS。

缺点:单纯年轻代收集器,依赖老年代CMS配合,无法独立工作。

2.3.4 CMS 收集器简介(并发低延迟收集器)

CMS(Concurrent Mark Sweep)并发标记清除收集器是一种以获取最短回收停顿时间为目标的收集器主打低延迟、低停顿,是互联网高并发项目经典老年代收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

2.3.5 CMS 完整回收流程(核心重点)

CMS 主要分为四大阶段,两大阶段STW、两大阶段并发,最大化减少停顿:

  1. 初始标记(STW):仅标记GC Roots直接关联对象,速度极快,短暂停顿。

  2. 并发标记(并发执行)并发标记阶段就是从GCRoots的直接关联对象开始遍历整个对象图的过程。用户线程与GC线程同时运行,遍历所有存活对象,耗时最长、无停顿。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。

  3. 重新标记(STW)修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。

  4. 并发清除(并发执行)开启用户线程,同时GC线程开始对未标记的区域做清扫。无STW,不阻塞业务线程。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)

  5. 并发重置重置本次GC过程中的标记数据。

2.3.6 底层算法

整体基于标记-清除算法,无内存整理,并发回收。

2.3.7 CMS 优缺点

优点

  • 大部分阶段并发执行,用户线程不阻塞;

  • STW停顿时间极短,接口延迟低、体验好;

  • 高并发互联网系统首选经典组合。

缺点

  • 基于清除算法,产生大量内存碎片(可通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理);

  • 并发阶段占用CPU资源,降低吞吐量;

  • 存在并发浮动垃圾(在并发标记和并发清理阶段又产生垃圾),无法回收本轮新产生垃圾;

  • 内存使用率过高(上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发fullgc)会触发Concurrent Mode Failure,退化为Serial Old单线程FullGC。

2.3.8 CMS 核心调优参数
1. -XX:+UseConcMarkSweepGC:启用cms 2. -XX:ConcGCThreads:并发的GC线程数 3. -XX:+UseCMSCompactAtFullCollection :FullGC之后做压缩整理(减少碎片) 4. -XX:CMSFullGCsBeforeCompaction :多少次FullGC之后压缩一次 ,默认是0 ,代表每次FullGC后都会压缩一次 5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92 ,这是百分比) 6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值) ,如果不指定, JVM仅在第一次使用设定值 ,后续则会自动调整 7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc ,降低CMS GC标记阶段(也会对年轻代一起做标记 ,如果 minor gc就干掉了很多对垃圾对象 ,标记阶段就会减少一些标记时间)时的开销 ,一般CMS的GC耗时 80%都在标记阶段 8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行 ,缩短STW 9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行 ,缩短STW;

2.4 亿级流量电商系统 ParNew+CMS 实战JVM参数调优

2.4.1 业务场景

亿级流量电商、秒杀、订单、支付核心服务,要求:极低GC卡顿、无频繁FullGC、接口响应稳定、高并发不抖动

服务器配置:8核16G 生产高配服务器

2.4.2 生产最优参数(可直接上线)

# 亿级电商交易系统 ParNew+CMS 生产终极参数 -Xms10G -Xmx10G -Xmn4G -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M # 开启经典低延迟组合 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC # 老年代75%触发并发GC,提前预热 -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly # 并行重新标记,降低停顿 -XX:+CMSParallelRemarkEnabled # 碎片整理策略 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3 # 关闭显式GC、避免业务System.gc抖动 -XX:+DisableExplicitGC # 打印GC日志 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/data/logs/jvm/cms-gc.log

2.4.3 核心参数解析
  • 固定堆10G:防止动态扩容缩容,避免内存抖动;

  • 年轻代4G:容纳秒杀瞬时海量临时对象,大幅降低MinorGC频率;

  • 75%触发CMS:提前并发回收,避免内存堆满触发降级FullGC;

  • 定时碎片整理:解决CMS内存碎片问题,防止大对象分配失败;

  • 禁用显式GC:杜绝业务代码误写System.gc导致服务卡顿。

2.4.4 上线优化效果
  • GC停顿时间控制在10ms以内,用户无感知;

  • 无频繁FullGC、无CMS降级;

  • 秒杀高峰期接口无超时、无服务抖动;

  • 长期运行无内存碎片堆积、无OOM。


三、垃圾收集底层核心算法实现(高阶底层)

上面的GC算法、收集器,全部依赖底层三色标记算法、读写屏障、记忆集、卡表支撑,这是解决并发漏标、跨代引用的底层核心,也是大厂面试高阶重难点。

3.1 三色标记算法(并发GC核心原理)

3.1.1 简介

三色标记是JVM并发标记阶段的对象遍历标记算法,解决用户线程与GC线程并发执行时的对象存活判定问题,解决漏标问题,是CMS、G1并发回收的底层基石。

3.1.2 三色定义

三色标记算法是把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:

  • 白色表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段,仍然是白色的对象,即代表不可达。

  • 灰色表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。

  • 黑色表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象)指向某个白色对象。

3.1.3 完整执行过程
  1. 初始所有对象为白色;

  2. 从GC Roots开始遍历,根对象标记为灰色;

  3. 遍历灰色对象引用,子对象由白变灰,父对象变黑;

  4. 循环遍历直至无灰色对象;

  5. 最终白色对象即为垃圾对象,可回收。

3.1.4 优缺点与并发问题

优点:支持并发标记、无需全程STW、大幅降低GC停顿。

核心问题

  • 多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。

另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

  • 并发运行时会出现对象漏标,导致存活对象被误回收。

漏标两大条件

  1. 灰色对象删除白色对象引用;

  2. 黑色对象新增白色对象引用。

解决方案:通过读写屏障+重新标记修正漏标问题。

3.2 读写屏障机制(解决并发漏标核心)

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案:增量更新(IncrementalUpdate) 和原始快照(SnapshotAtTheBeginningSATB) 。

增量更新就是当黑色对象插入新的指向白色对象的引用关系时就将这个新插入的引用记录下来, 等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了

原始快照就是当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来, 在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃)

以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

3.2.1 写屏障

对象引用赋值、修改时触发的拦截屏障,核心作用:

  • 记录黑色对象新增的白色引用;

  • 防止并发标记期间漏标存活对象;

  • 为重新标记阶段提供修正数据源。

3.2.2 读屏障

对象引用读取访问时触发的拦截屏障,主要用于G1、ZGC等新式收集器,实时修正对象标记状态,保证并发读取准确性。

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下

  • CMS:写屏障 + 增量更新
  • G1 ,Shenandoah:写屏障 + SATB
  • ZGC:读屏障

工程实现中,读写屏障还有其他功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每款垃圾回收器都有自己的想法。
为什么G1用SATB?CMS用增量更新?
我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描, G1因为很多对象都位于不同的region, CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

3.3 记忆集与卡表(解决跨代引用核心)

3.3.1 核心背景

分代回收中存在大量老年代引用年轻代的跨代引用,若每次MinorGC都全量扫描老年代,开销极大、性能极低。为此,在年轻代可以引入记录集(RememberSet)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。事实上并不只是年轻代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集(PartialGC行为的垃圾收集器,典型的如G1、ZGCShenandoah收集器,都会面临相同的问题。

3.3.2 记忆集(Remembered Set)

是一种抽象数据结构,作用是记录「非年轻代对象指向年轻代对象」的所有跨代引用,让MinorGC无需遍历整个老年代,只需扫描记忆集即可。

3.3.3 卡表(Card Table)

hotspot使用一种叫做“卡表”(Cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系, 可以类比为Java语言中HashMap与Map的关系。

JVM将老年代内存划分为一个个512字节的卡页,每个卡页对应一个卡表标记位:

  • 脏卡:卡页内存在跨代引用;

  • 干净卡:无跨代引用,无需扫描。

一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0. GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。

卡表的维护
卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1。
Hotspot使用写屏障维护卡表状态。

3.3.4 核心作用

大幅缩小GC扫描范围,以空间换时间,极致提升分代回收效率,是现代JVM分代GC的底层核心优化。


四、全文核心总结

  1. 分代收集基于三大假说,年轻代用复制算法、老年代用清除/整理算法,各司其职。

  2. 三大基础算法:复制高效无碎片、清除简单有碎片、整理无碎片但耗时。

  3. 经典收集器:ParNew+CMS主打低延迟,Parallel主打吞吐量,Serial适合低配置场景。

  4. CMS四阶段并发回收,是互联网亿级流量系统经典方案,需重点优化碎片与触发阈值。

  5. 三色标记是并发GC核心,读写屏障解决漏标问题,卡表记忆集解决跨代扫描开销。

  6. 底层算法+屏障卡表机制,共同支撑JVM高并发、低卡顿的垃圾回收能力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 17:56:55

如何在macOS上完美显示桌面歌词:LyricsX完整使用指南

如何在macOS上完美显示桌面歌词:LyricsX完整使用指南 【免费下载链接】LyricsX 🎶 Ultimate lyrics app for macOS. 项目地址: https://gitcode.com/gh_mirrors/ly/LyricsX 你是否曾经在听歌时想要跟着唱,却总是记不住歌词&#xff1f…

作者头像 李华
网站建设 2026/6/12 17:54:19

Rust Qt Binding Generator:如何快速实现Rust与Qt/QML的无缝集成

Rust Qt Binding Generator:如何快速实现Rust与Qt/QML的无缝集成 【免费下载链接】rust-qt-binding-generator Generate bindings to use Rust code in Qt and QML 项目地址: https://gitcode.com/gh_mirrors/ru/rust-qt-binding-generator Rust Qt Binding …

作者头像 李华
网站建设 2026/6/12 17:53:54

3PEAK思瑞浦 TPR8201-EV1R-S EMSOP8 特殊功能电路

特性 优异匹配:TPR82xxA:在-40C至125C范围内匹配度为0.05%TPR82xx:在-40C至125C范围内匹配度为0.1% 匹配温度漂移: TPR82xxA:1ppm/C(最大)TPR82xx:5ppm/C (最大) 工作温度范围:-40C至125C

作者头像 李华
网站建设 2026/6/12 17:51:54

D3keyHelper暗黑3助手:如何用5分钟配置你的专属自动化战斗系统

D3keyHelper暗黑3助手:如何用5分钟配置你的专属自动化战斗系统 【免费下载链接】D3keyHelper D3KeyHelper是一个有图形界面,可自定义配置的暗黑3鼠标宏工具。 项目地址: https://gitcode.com/gh_mirrors/d3/D3keyHelper 你是否厌倦了在暗黑破坏神…

作者头像 李华
网站建设 2026/6/12 17:49:03

MQX RTOS深度解析:从内核机制到工业级嵌入式开发实战

1. 项目概述:为什么嵌入式系统离不开RTOS在嵌入式开发这个行当里摸爬滚打了十几年,我见过太多项目从“裸奔”(无操作系统)的简单逻辑,一步步演变成需要同时处理网络通信、用户交互、传感器数据采集和复杂控制算法的“多…

作者头像 李华