GC是解决内存溢出(OOM)、GC频繁、系统卡顿的核心突破口。很多开发者对GC的理解停留在“自动回收无用对象”的表层,不懂“哪些对象是垃圾”“如何高效回收”“不同回收器适配什么场景”,导致遇到GC相关问题时只能盲目调整JVM参数,无法从根源解决问题。
本文结合JDK8及以上主流版本(生产环境首选),从GC核心前提、垃圾判定算法、核心回收算法、垃圾收集器、实战调优、面试高频问题六个维度,彻底拆解GC底层逻辑,衔接上一篇运行时数据区内容,帮你构建完整的JVM内存与GC知识体系——既能应对大厂面试,也能快速解决生产环境中的GC实战难题。
核心前提:GC的核心目标是“识别并回收无用对象,释放内存空间,避免内存泄漏和OOM”,且GC过程由JVM自动执行,开发者无需手动触发,但需理解其原理并进行合理调优,减少GC对系统性能的影响(尤其是Full GC的STW影响)。
一、核心基础:哪些对象是“垃圾”?(垃圾判定算法)
GC的第一步的是“识别垃圾”——即判断堆内存中哪些对象已经不再被使用,属于可回收的无用对象。JVM中主流的垃圾判定算法有两种,各有优劣,实际应用中结合使用。
1. 引用计数法(废弃算法)
这是最基础、最简单的垃圾判定算法,核心逻辑:为每个对象分配一个“引用计数器”,记录对象被引用的次数。
① 核心规则:
- 当对象被创建时,引用计数器初始化为1;
- 当有一个新的引用指向该对象时,计数器加1;
- 当引用失效(如引用变量被赋值为null、超出作用域)时,计数器减1;
- 当计数器的值为0时,判定该对象为垃圾,可被GC回收。
② 优点:实现简单、效率高,实时性强(对象成为垃圾时可立即被识别)。
③ 致命缺陷:无法解决“循环引用”问题——两个对象相互引用,计数器均为1,但两者均无其他外部引用,实际已无用,却无法被判定为垃圾,导致内存泄漏。
④ 现状:JVM(HotSpot)未采用该算法,仅用于部分简单的虚拟机(如Android Dalvik早期版本)。
2. 可达性分析算法(JVM主流算法)
为解决引用计数法的循环引用问题,HotSpot虚拟机采用“可达性分析算法”作为垃圾判定的核心算法,也是我们必须掌握的重点。
① 核心逻辑:以“根对象(GC Roots)”为起点,向下遍历所有引用链路,若某个对象无法通过任何根对象的引用链路到达(即“不可达”),则判定该对象为垃圾,可被GC回收。
② 核心概念:GC Roots(根对象)—— 不会被GC回收的对象,作为遍历的起点,常见的GC Roots包括4类(面试高频):
- 虚拟机栈中局部变量表引用的对象(如方法中的局部变量、参数);
- 本地方法栈中Native方法引用的对象;
- 元空间中类静态变量引用的对象(如static修饰的变量引用的对象);
- 活跃线程对象(正在执行的线程,不会被回收)。
③ 优势:完美解决循环引用问题,即使两个对象相互引用,若均无法到达GC Roots,仍会被判定为垃圾。
④ 补充:对象被判定为“不可达”后,并非立即被回收——JVM会对其进行两次标记,若两次标记后仍为不可达,才会被真正回收(后续讲解“对象finalize()方法”时详细说明)。
3. 补充:Java中的4种引用类型(影响垃圾判定)
可达性分析算法的核心是“引用”,Java中引用分为4种类型,不同类型的引用对垃圾判定和回收时机的影响不同,也是面试高频考点:
① 强引用(默认):最常见的引用类型(如Object obj = new Object()),只要强引用存在,对象就不会被GC回收,即使内存不足,JVM也会抛出OOM,不会回收强引用对象。
② 软引用(SoftReference):用于描述“有用但非必需”的对象,当内存充足时,不会回收软引用对象;当内存不足(即将OOM)时,会回收软引用对象,避免OOM。常用于缓存场景(如图片缓存)。
③ 弱引用(WeakReference):用于描述“非必需”的对象,无论内存是否充足,只要GC执行,就会回收弱引用对象(生命周期比软引用短)。常用于临时数据存储,避免内存泄漏。
④ 虚引用(PhantomReference):最弱的引用类型,无法通过虚引用获取对象实例,仅用于“监听对象被GC回收的时机”,必须结合ReferenceQueue使用,实际开发中极少用到(如JVM底层的直接内存回收)。
二、核心核心:垃圾回收算法(GC如何回收垃圾)
当JVM通过可达性分析算法判定出垃圾对象后,就需要通过“垃圾回收算法”将其回收,释放内存空间。JVM中核心的垃圾回收算法有4种,不同算法适用于不同的内存区域(年轻代、老年代),实际GC过程中会组合使用。
1. 复制算法(Copying Algorithm)—— 年轻代首选
复制算法是年轻代(Eden区、Survivor区)的核心回收算法,结合上一篇堆内存的布局(Eden:From Survivor:To Survivor = 8:1:1),核心逻辑是“将存活对象复制到新的内存区域,然后清空原区域的垃圾”。
① 核心步骤(以年轻代Minor GC为例):
1. 新生对象优先分配到Eden区,当Eden区满时,触发Minor GC;
2. 遍历Eden区和From Survivor区,将存活对象复制到To Survivor区(复制过程中会清理垃圾对象);
3. 清空Eden区和From Survivor区的所有对象(垃圾被彻底回收);
4. 交换From Survivor和To Survivor的角色(下次GC时,To Survivor变为From Survivor,反之亦然);
5. 若存活对象在Survivor区来回转移超过默认次数(15次,可通过参数调整),则进入老年代。
② 优点:
- 回收效率高:无需区分垃圾和存活对象,只需复制存活对象,清空原区域即可;
- 内存无碎片:复制后存活对象在新区域连续存储,避免内存碎片。
③ 缺点:
- 内存利用率低:需要预留一部分内存(如To Survivor区)作为“复制目的地”,始终有一部分内存处于空闲状态;
- 不适合存活对象多的区域:若区域内存活对象多(如老年代),复制操作会占用大量时间,效率低下。
④ 适用场景:年轻代(存活对象少、垃圾多,复制成本低)。
2. 标记-清除算法(Mark-Sweep Algorithm)—— 老年代辅助
标记-清除算法是最基础的垃圾回收算法,核心逻辑分为“标记”和“清除”两个步骤,无需复制对象,直接回收垃圾。
① 核心步骤:
1. 标记阶段:通过可达性分析算法,标记所有可达的存活对象(未被标记的即为垃圾对象);
2. 清除阶段:遍历内存区域,直接回收所有未被标记的垃圾对象,释放内存空间。
② 优点:
- 内存利用率高:无需预留空闲区域,所有内存均可被使用;
- 适合存活对象多的区域:无需复制存活对象,仅标记和清除垃圾,效率相对较高。
③ 缺点:
- 回收效率低:需要遍历两次内存区域(标记一次、清除一次),耗时较长;
- 产生内存碎片:回收后,存活对象分散在内存各个角落,形成大量不连续的内存碎片,后续创建大对象时,可能因无法找到连续内存而触发Full GC。
④ 适用场景:老年代(存活对象多、垃圾少,标记成本低,可接受内存碎片),通常作为老年代回收的辅助算法。
3. 标记-整理算法(Mark-Compact Algorithm)—— 老年代首选
标记-整理算法是为解决标记-清除算法的“内存碎片”问题而设计的,是老年代的核心回收算法,核心逻辑在“标记-清除”的基础上,增加了“整理”步骤。
① 核心步骤:
1. 标记阶段:与标记-清除算法一致,标记所有可达的存活对象;
2. 整理阶段:将所有存活对象向内存区域的一端移动,集中存储;
3. 清除阶段:清空存活对象另一端的所有垃圾对象,释放内存空间。
② 优点:
- 无内存碎片:存活对象连续存储,后续创建大对象时可快速找到连续内存;
- 内存利用率高:无需预留空闲区域,与标记-清除算法一致。
③ 缺点:回收效率低于标记-清除算法——增加了“整理”步骤,需要移动存活对象,消耗额外的时间(尤其是存活对象较多时)。
④ 适用场景:老年代(存活对象多、垃圾少,可接受整理耗时,避免内存碎片),是HotSpot虚拟机老年代的主流回收算法。
4. 分代回收算法(Generational Collection Algorithm)—— JVM实际使用的组合算法
分代回收算法并非独立的算法,而是结合“复制算法”“标记-整理算法”,根据堆内存的分代(年轻代、老年代)特点,针对性选择回收算法,是JVM实际使用的垃圾回收方式(上一篇堆内存分代已铺垫)。
① 核心逻辑:根据对象的“生命周期”,将堆内存分为年轻代和老年代,不同代采用不同的回收算法,兼顾回收效率和内存利用率。
② 分代回收策略(结合JDK8堆布局):
- 年轻代:采用“复制算法”,因为年轻代对象生命周期短、存活对象少、垃圾多,复制算法效率高、无碎片;
- 老年代:采用“标记-整理算法”(主流)或“标记-清除算法”(辅助),因为老年代对象生命周期长、存活对象多、垃圾少,标记-整理算法可避免内存碎片,适合长期使用;
- 元空间:采用“标记-清除算法”,回收无用的类元数据(如未被引用的类、类加载器),避免元空间OOM。
③ 核心优势:针对性优化,兼顾回收效率和内存利用率,是JVM GC的核心实现方式,也是我们理解GC过程的关键。
三、实战重点:JVM垃圾收集器(GC算法的具体实现)
垃圾回收算法是“理论”,而垃圾收集器(Garbage Collector)是“实际实现”——JVM提供了多种垃圾收集器,不同收集器适配不同的业务场景(如高吞吐量、低延迟),开发者需根据业务需求选择合适的收集器,这也是GC调优的核心。
重点说明:本文聚焦JDK8及以上主流收集器(生产环境常用),淘汰JDK9及以上废弃的收集器(如CMS),重点讲解4种核心收集器,以及它们的组合使用方式。
1. Serial GC(串行收集器)—— 单线程收集器(入门级)
Serial GC是最基础、最简单的垃圾收集器,核心特点是“单线程执行GC”,即GC执行时,会暂停所有用户线程(STW,Stop The World),直到GC完成。
① 核心实现:
- 年轻代:采用“复制算法”,单线程执行Minor GC;
- 老年代:采用“标记-整理算法”,单线程执行Full GC。
② 优点:实现简单、内存占用小、无线程切换开销,适合单线程环境或小型应用(如桌面应用)。
③ 缺点:STW时间长,尤其是堆内存较大时,GC执行会导致系统卡顿,无法满足高并发、低延迟的生产环境需求。
④ 启用参数:-XX:+UseSerialGC(JDK8默认不启用,适合小型应用)。
2. Parallel GC(并行收集器)—— 高吞吐量收集器(生产常用)
Parallel GC是Serial GC的多线程版本,核心特点是“多线程执行GC”,减少STW时间,追求“高吞吐量”(吞吐量=用户线程执行时间/(用户线程执行时间+GC时间)),是JDK8默认的垃圾收集器。
① 核心实现:
- 年轻代:采用“复制算法”,多线程执行Minor GC;
- 老年代:采用“标记-整理算法”,多线程执行Full GC;
- 支持“自适应调节策略”:JVM会根据系统运行状态,自动调整GC线程数、堆内存大小等参数,无需手动调优。
② 优点:多线程GC,STW时间比Serial GC短,吞吐量高,适合CPU核心数多、对吞吐量要求高的生产环境(如后台服务、数据处理)。
③ 缺点:仍有STW时间,无法满足低延迟需求(如接口响应时间要求毫秒级)。
④ 启用参数:-XX:+UseParallelGC(JDK8默认启用);-XX:ParallelGCThreads=N(设置GC线程数,默认等于CPU核心数)。
3. G1 GC(Garbage-First)—— 低延迟收集器(高并发首选)
G1 GC是JDK9及以上的默认垃圾收集器,也是JDK8中推荐用于高并发、低延迟场景的收集器,核心特点是“分区域回收”“可预测的STW时间”,兼顾吞吐量和低延迟。
① 核心实现(区别于传统分代):
- 堆内存不再分为固定的年轻代和老年代,而是划分为多个大小相等的“区域(Region)”,每个区域可动态扮演Eden区、Survivor区、老年代区;
- 采用“标记-整理算法”(区域内)+“复制算法”(区域间),优先回收垃圾最多的区域(Garbage-First),减少GC时间;
- 支持“可预测的STW时间”:开发者可通过参数设置GC的最大STW时间(如100ms),JVM会根据该目标调整回收策略,确保延迟可控。
② 优点:
- 低延迟:STW时间可预测、可控,适合高并发、低延迟场景(如电商、接口服务);
- 内存利用率高:无明显内存碎片,支持动态调整区域角色,适配不同对象生命周期;
- 适合大堆内存:堆内存越大,G1的优势越明显(如堆内存16G以上)。
③ 缺点:实现复杂,内存占用比Parallel GC高,吞吐量略低于Parallel GC。
④ 启用参数:-XX:+UseG1GC(JDK8需手动启用,JDK9及以上默认启用);-XX:MaxGCPauseMillis=N(设置最大STW时间,如100)。
4. ZGC(Z Garbage Collector)—— 超低延迟收集器(高性能场景)
ZGC是JDK11引入的高性能垃圾收集器,核心特点是“超低延迟”“支持超大堆内存”,STW时间可控制在毫秒级以内,适合对延迟要求极高的场景(如金融、实时计算)。
① 核心优势:
- 超低延迟:STW时间不超过10ms,即使堆内存达到TB级,STW时间也能保持稳定;
- 支持超大堆:最大支持4TB堆内存,适合大型分布式应用、大数据场景;
- 无内存碎片:采用“标记-复制-整理”结合的算法,确保内存连续。
② 缺点:吞吐量低于G1和Parallel GC,JDK8不支持(需JDK11及以上),生产环境使用需谨慎(部分场景仍需验证)。
③ 启用参数:-XX:+UseZGC(JDK11及以上支持)。
5. 收集器选择建议(生产实战)
结合业务场景选择合适的收集器,避免盲目追求“高性能”,匹配业务需求才是关键:
- 小型应用/桌面应用:Serial GC(简单、内存占用小);
- 后台服务/数据处理(追求高吞吐量):Parallel GC(JDK8默认,性价比高);
- 高并发接口/电商(追求低延迟):G1 GC(JDK8可用,推荐);
- 金融/实时计算(超低延迟、超大堆):ZGC(JDK11及以上,需验证兼容性)。
四、实战落地:GC异常排查与调优(生产必备)
日常开发中,GC相关的常见问题有3类:GC频繁(Minor GC每秒多次)、Full GC频繁(每次Full GC STW时间过长)、OOM(内存溢出)。结合上一篇运行时数据区的异常排查方法,这里重点讲解GC相关的排查与调优技巧。
1. GC异常排查核心工具(与运行时数据区通用)
① jstat:查看GC统计信息,核心命令(高频使用):
- jstat -gc 进程ID 1000 10:每隔1000ms,输出10次GC统计信息,包括年轻代、老年代的GC次数、GC时间、内存使用情况;
- jstat -gcutil 进程ID:查看GC利用率(如年轻代GC利用率、老年代GC利用率),快速判断GC是否频繁。
② jmap:导出堆快照,分析对象分布,排查内存泄漏(GC无法回收的对象):
- jmap -dump:format=b,file=heapdump.hprof 进程ID:导出堆快照;
- 用VisualVM/JProfiler打开快照,分析“内存占用最多的对象”“对象引用链路”,定位内存泄漏原因(如静态集合持有对象、资源未关闭)。
③ jconsole/VisualVM:可视化工具,实时查看GC情况、内存使用趋势,快速定位GC异常。
④ GC日志:开启GC日志,详细记录GC过程(时间、类型、内存变化、STW时间),是排查GC问题的核心依据。
2. 常见GC异常排查与解决方案(实战重点)
① 异常1:Minor GC频繁(每秒多次,导致系统卡顿)
- 排查原因:年轻代内存分配过小,新生对象快速占满Eden区,频繁触发Minor GC;或存在大量短期存活对象,导致Minor GC频繁。
- 解决方案:增大年轻代内存(调整-Xmn参数,如-Xmn4g);优化代码,减少短期存活对象的创建(如避免在循环中创建临时对象)。
② 异常2:Full GC频繁(每天多次,STW时间过长)
- 排查原因:老年代内存不足,存活对象过多(如缓存对象未及时清理);大对象直接进入老年代,快速占满老年代;内存泄漏,无用对象无法被GC回收,积累到老年代。
- 解决方案:
1. 增大老年代内存(调整-Xmx、-Xms,如-Xms8g -Xmx8g,合理分配年轻代和老年代比例);
2. 优化缓存策略,及时清理无用缓存(如设置缓存过期时间);
3. 避免大对象直接进入老年代(调整-XX:PretenureSizeThreshold参数,设置大对象阈值,如10M,超过阈值的对象优先分配到年轻代);
4. 排查内存泄漏,修复代码(如释放静态集合的引用、关闭未关闭的资源)。
③ 异常3:OOM(java.lang.OutOfMemoryError: Java heap space)
- 排查原因:堆内存分配过小;内存泄漏(核心原因);大对象过多,堆内存无法容纳。
- 解决方案:
1. 临时解决:增大堆内存(调整-Xmx参数);
2. 根本解决:导出堆快照,分析内存泄漏原因,修复代码;减少大对象创建,拆分大对象。
3. GC调优核心参数(实战常用,JDK8)
结合收集器和内存布局,整理常用调优参数,直接套用即可:
① 堆内存参数(基础):
-Xms:堆初始内存大小(建议与-Xmx一致,避免JVM频繁调整内存);
-Xmx:堆最大内存大小(根据服务器内存设置,如服务器16G内存,设置为8G);
-Xmn:年轻代内存大小(建议占堆内存的1/3~1/2,如-Xmn4g);
-XX:SurvivorRatio:年轻代中Eden区与Survivor区的比例(默认8:1,无需修改,特殊场景可调整)。
② 收集器参数(核心):
-XX:+UseParallelGC:启用Parallel GC(JDK8默认);
-XX:+UseG1GC:启用G1 GC(高并发低延迟场景);
-XX:ParallelGCThreads:GC线程数(默认等于CPU核心数,如8核CPU设置为8);
-XX:MaxGCPauseMillis:G1 GC最大STW时间(如设置为100ms)。
③ 其他常用参数:
-XX:+PrintGCDetails:打印详细GC日志(排查问题必备);
-XX:+HeapDumpOnOutOfMemoryError:OOM时自动导出堆快照(快速排查内存泄漏);
-XX:PretenureSizeThreshold:大对象阈值(如10485760,即10M,超过该值的对象直接进入老年代);
-XX:MaxTenuringThreshold:Survivor区对象进入老年代的默认次数(默认15,可调整)。
五、面试高频问题(高级开发必背)
结合本文内容,整理8个大厂高频GC面试题,结合底层逻辑给出精准答案,面试直接套用,同时衔接上一篇运行时数据区的知识点:
1. JVM垃圾回收的核心作用是什么?仅作用于哪些内存区域?
答:核心作用是识别并回收无用对象,释放内存空间,避免内存泄漏和OOM;仅作用于线程共享区的堆内存和元空间,线程私有区无需GC。
2. JVM如何判定一个对象是垃圾?主流算法是什么?
答:主流算法是可达性分析算法;以GC Roots为起点,遍历引用链路,不可达的对象判定为垃圾;辅助算法是引用计数法,因无法解决循环引用被废弃。
3. 什么是GC Roots?常见的GC Roots有哪些?
答:GC Roots是不会被GC回收的根对象,作为可达性分析的起点;常见的有4类:虚拟机栈局部变量引用的对象、本地方法栈Native方法引用的对象、元空间类静态变量引用的对象、活跃线程对象。
4. Java中的4种引用类型有哪些?各自的特点是什么?
答:① 强引用:默认类型,强引用存在时对象不会被回收,OOM时也不回收;② 软引用:有用非必需,内存不足时回收;③ 弱引用:非必需,GC时立即回收;④ 虚引用:最弱,仅用于监听对象回收时机,需结合ReferenceQueue。
5. JVM核心的垃圾回收算法有哪些?各自的适用场景是什么?
答:① 复制算法:适合年轻代,效率高、无碎片,内存利用率低;② 标记-清除算法:适合老年代辅助,内存利用率高、有碎片;③ 标记-整理算法:适合老年代,无碎片、内存利用率高,效率略低;④ 分代回收算法:JVM实际使用,结合前三种算法,按分代选择。
6. Parallel GC、G1 GC的核心区别是什么?各自适合什么场景?
答:① Parallel GC:多线程GC,追求高吞吐量,STW时间较长,适合后台服务、数据处理;② G1 GC:分区域回收,可预测STW时间,兼顾吞吐量和低延迟,适合高并发、低延迟场景(如电商)。
7. Full GC和Minor GC的区别是什么?触发条件有哪些?
答:① 区别:Minor GC作用于年轻代,回收年轻代垃圾,STW时间短、频率高;Full GC作用于老年代+年轻代,回收全堆垃圾,STW时间长、频率低;② 触发条件:Minor GC触发条件是Eden区满;Full GC触发条件是老年代满、元空间满、手动调用System.gc()(不推荐)。
8. 如何排查GC频繁和内存泄漏问题?核心工具和步骤是什么?
答:① 排查工具:jstat(查看GC统计)、jmap(导出堆快照)、VisualVM(可视化分析)、GC日志;② 排查步骤:1. 用jstat查看GC频率和时间,确认异常类型;2. 导出堆快照,分析内存占用最多的对象;3. 查看对象引用链路,定位内存泄漏原因(如静态集合持有对象);4. 优化代码或调整JVM参数。
评论