深入剖析JVM垃圾回收:从GC Root到收集器,一篇吃透面试核心

深入剖析JVM垃圾回收:从GC Root到收集器,一篇吃透面试核心 在Java开发中垃圾回收GC是JVM自动内存管理的核心机制也是面试中的高频重难点。很多开发者只知其然不知其所以然——比如“GC到底回收什么”“不同垃圾收集算法有何优劣”“为什么G1能替代CMS”今天这篇文章我们从基础到进阶系统拆解JVM垃圾回收的全流程结合案例与底层原理帮你彻底搞懂这一核心知识点轻松应对面试与工作中的内存问题。一、前置基础GC到底回收哪些对象JVM垃圾回收的核心目标是回收“不可达”的对象——也就是无法通过任何路径关联到“GC Root”的对象可以理解为“游离在关系网之外的对象”。在判断对象是否可达前我们首先要明确哪些对象能成为GC Root这是GC的“起点”也是面试中必问的基础点。1.1 常见的GC Root对象4类核心记牢不踩坑静态变量Static Variables属于类级别的变量而非实例对象生命周期贯穿整个程序运行期间。比如一个类中定义的private static User user new User();这个user引用的对象就是GC Root只要类未被卸载它就不会被回收。活动线程Active Threads正在运行的线程本身就是GC Root。因为线程是程序执行的控制流只要线程还在运行它引用的所有对象都需要被保留否则会导致程序运行异常。栈帧中的局部变量和输入参数方法调用时栈帧会存储局部变量和方法参数这些引用的对象也是GC Root。它们的生命周期与方法一致方法执行结束栈帧出栈这些引用就会失效。JNI引用JNI References通过Java Native InterfaceJNI与本地代码C/C交互时传递的对象。这些对象的生命周期由本地代码管理JVM会将其视为GC Root避免被误回收。搞懂GC Root后JVM通过“可达性分析算法”判断对象是否可达从GC Root出发沿引用链遍历能被遍历到的对象是“存活对象”无法被遍历到的就是“死亡对象”也是GC要回收的目标。二、核心概念Stop-the-World与吞吐量在介绍具体的垃圾收集算法前必须先搞懂两个高频概念——Stop-the-World和吞吐量这是理解GC优化的关键。2.1 Stop-the-WorldSTWGC的“暂停代价”Stop-the-World简称STW指JVM在执行GC时会暂停所有应用程序线程仅保留GC所需线程运行。这是所有GC算法都无法避免的过程——因为如果GC线程和应用线程同时运行对象的引用关系会不断变化导致GC标记出现错误。我们常说的“GC优化”本质上就是减少STW的停顿时间让系统达到“高吞吐、低停顿”的效果——比如电商系统、接口服务过长的STW会导致接口超时、用户体验下降。2.2 吞吐量GC效率的“衡量标准”吞吐量指CPU用于执行用户代码的时间与CPU总执行时间的比值计算公式如下$$吞吐量 执行用户代码时间 /执行用户代码时间 GC时间$$吞吐量越高说明GC占用的CPU时间越少垃圾回收效率越高。举个例子虚拟机总共运行100分钟其中GC耗时1分钟那么吞吐量就是99%——这是一个非常优秀的指标如果GC耗时10分钟吞吐量就只有90%说明GC已经影响到了用户程序的运行。三、垃圾收集算法没有最优只有最合适当JVM区分出存活对象和死亡对象后就会执行垃圾回收操作。目前JVM有4种核心垃圾收集算法每种算法都有其优劣没有绝对的“最优解”只有适配不同场景的“最合适解”。3.1 标记-清除算法Mark-Sweep最基础也最“朴素”标记-清除算法是所有GC算法的基础后续的算法都是在它的基础上优化而来。它的核心流程分为两个阶段正如其名标记阶段通过可达性分析算法标记出所有可达的存活对象清除阶段对堆内存从头到尾进行线性遍历将未被标记即死亡的对象回收释放内存空间。看似简单但它的缺点非常明显也是它无法单独作为主流算法的原因效率问题需要进行两次全堆遍历标记一次、清除一次堆内存越大效率越低空间问题清除后会产生大量不连续的内存碎片。JVM需要维护一个空闲内存列表增加额外开销更关键的是当分配大对象如数组时很难找到连续的内存空间可能导致提前触发GC。3.2 复制算法Copying高效无碎片代价是“浪费内存”为了解决标记-清除算法的碎片问题复制算法应运而生。它的核心思想是“空间换时间”流程如下将堆内存划分为两个大小相等的区域From空间使用中和To空间空闲对象分配阶段所有新创建的对象都分配在From空间GC触发时将From空间中所有GC Root可达的对象全部复制到To空间清空From空间然后互换From和To的名称下一次GC继续重复上述流程。优点很突出实现简单回收效率高且不会产生内存碎片但缺点也同样明显内存浪费严重将内存一分为二相当于只有一半的内存可用代价过高如果不想浪费一半内存就需要额外的空间作为“分配担保”应对所有对象100%存活的极端情况因此老年代一般不使用这种算法。存活率高时效率下降如果对象存活率很高比如老年代需要复制所有存活对象且重置所有引用地址耗时会大幅增加。【应用场景】年轻代的Minor GC。因为年轻代的对象“朝生夕死”存活率极低通常低于10%复制算法的优势能充分发挥。HotSpot JVM将年轻代分为1个Eden区和2个Survivor区From和To默认比例为8:1:1有效缓解了内存浪费的问题。3.3 标记-压缩算法Mark-Compact平衡效率与空间标记-压缩算法也叫标记-整理算法是标记-清除算法的改进版核心是解决标记-清除的碎片问题同时避免复制算法的内存浪费。它的流程分为两步标记阶段与标记-清除算法一致标记所有存活对象压缩阶段不直接清除死亡对象而是将所有存活对象向堆内存的一端移动然后直接清除边界以外的所有内存死亡对象空闲空间。优点既解决了标记-清除的碎片问题又避免了复制算法的内存浪费是一种“平衡型”算法缺点如果存活对象过多压缩阶段需要执行大量的复制操作会导致算法效率下降。【应用场景】老年代。老年代对象存活率高标记-压缩算法能在保证内存利用率的同时避免碎片问题通常与标记-清除算法混合使用。3.4 分代收集算法Generational-Collection主流JVM的“最优解”前面三种算法各有优劣有没有一种能兼顾效率、空间、利用率的算法答案是没有但分代收集算法可以“取长补短”——它不是一种新算法而是将复制算法、标记-清除、标记-压缩算法结合起来根据对象的生命周期分区域使用不同算法。先看三个核心指标的对比仅作基础参考实际受场景影响内存效率复制算法 标记-清除算法 标记-压缩算法内存整齐度复制算法 标记-压缩算法 标记-清除算法内存利用率标记-压缩算法 标记-清除算法 复制算法。分代收集算法的核心逻辑将堆内存分为年轻代和老年代根据两个区域的对象特点选择最合适的算法年轻代Young Generation区域小、对象存活率低朝生夕死适合使用复制算法——效率高、无碎片且通过Eden2个Survivor的设计缓解了内存浪费问题。老年代Old Generation区域大、对象存活率高存活时间长适合使用标记-清除算法效率高或标记-清除与标记-压缩的混合算法平衡碎片与效率。这也是目前所有主流JVM如HotSpot的默认垃圾回收思路——分代收集因地制宜。四、面试高频Java中的4种引用类型Java中的引用类型直接影响对象的回收时机是面试中的高频考点几乎必考。我们先定义一个基础的User类方便后续案例演示public class User { public int id; public String name; public User(int id, String name) { this.id id; this.name name; } Override public String toString() { return [id id , name name ] ; } }4.1 强引用最常用永不回收强引用是Java中最常见的引用类型也是默认的引用方式。只要强引用存在垃圾收集器永远不会回收被引用的对象——即使内存不足JVM也会抛出OOM异常而不是回收强引用对象。示例代码User user new User(1, zhangsan);【案例验证】即使将user置为null只要还有其他强引用如user1指向该对象对象就不会被回收public class StrongReferenceTest { public static void main(String[] args) { User user new User(1, zhangsan); User user1 user; // 强引用关联 user null; // 解除user的强引用 System.gc(); // 强制GC try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(user1); // 输出[id1, namezhangsan]对象未被回收 } }4.2 软引用内存不足时回收软引用通过SoftReference类实现它的特点是内存充足时不会回收软引用关联的对象当系统即将发生OOM内存溢出时才会将这些对象列入回收范围进行二次回收——如果回收后仍没有足够内存才会抛出OOM。示例代码SoftReferenceUser userSoftRef new SoftReference(new User(1, zhangsan));【应用场景】内存敏感的高速缓存比如EHCache本地缓存、Netty异步网络通信框架中的缓存——既保证了缓存的可用性又避免了内存溢出。【案例验证】设置JVM参数-Xms10m -Xmx10m限制堆内存为10M模拟内存不足场景public class SoftReferenceTest { public static void main(String[] args) { // 建立软引用 SoftReferenceUser userSoftRef new SoftReference(new User(1, zhangsan)); System.out.println(userSoftRef.get()); // 内存充足输出对象信息 try { // 分配7M内存导致内存紧张10M堆内存新生代占1/3老年代占2/37M无法容纳 byte[] b new byte[1024 * 1024 * 7]; } catch (Throwable e) { e.printStackTrace(); } finally { // 内存不足软引用对象被回收 System.out.println(userSoftRef.get()); // 输出null } } }4.3 弱引用GC时立即回收弱引用通过WeakReference类实现它的生命周期比软引用更短无论内存是否充足只要垃圾收集器开始工作就会回收只被弱引用关联的对象——也就是说弱引用的对象只能存活到下一次GC。示例代码WeakReferenceUser userWeakRef new WeakReference(new User(1, zhangsan));【案例验证】调用System.gc()后弱引用对象会被立即回收public class WeakReferenceTest { public static void main(String[] args) { WeakReferenceUser userWeakRef new WeakReference(new User(1, zhangsan)); System.out.println(userWeakRef.get()); // 输出[id1, namezhangsan] System.gc(); // 触发GC try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(userWeakRef.get()); // 输出null对象已被回收 } }4.4 虚引用对象回收的“监视器”虚引用也叫幽灵引用、幻影引用通过PhantomReference类实现是最特殊的一种引用——无法通过虚引用获取对象实例它的唯一作用是当对象被GC回收时会收到一个系统通知用于执行清理操作或监控对象回收状态。示例代码ReferenceQueue phantomQueue new ReferenceQueue(); PhantomReferenceUser obj new PhantomReference(new User(1, tom), phantomQueue);【案例验证】通过isEnqueued()方法判断虚引用对象是否被回收public class PhantomReferenceTest { public static void main(String[] args) { User obj new User(1, zhangsan); ReferenceQueuelt;Objectgt; queue new ReferenceQueue(); PhantomReferencelt;Objectgt; phantomRef new PhantomReference(obj, queue); obj null; // 解除强引用 boolean isCollected false; while (!isCollected) { System.gc(); // 建议GC执行回收 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 判断虚引用是否被回收被回收则入队 isCollected phantomRef.isEnqueued(); } System.out.println(虚引用是否被回收 isCollected); // 输出true } }注意虚引用的回收具有不确定性需要等待GC完成因此程序中需要加入延迟确保GC有足够时间执行。五、垃圾收集器算法的“具体实现”如果说垃圾收集算法是“内存回收的方法论”那么垃圾收集器就是“方法论的具体实现”。从JDK1.3到JDK17垃圾收集器不断迭代核心目标都是“缩短STW时间、提高吞吐量”。下面我们梳理主流收集器的特点、应用场景以及面试中的重点。5.1 GC收集器发展史快速梳理面试加分1999年JDK1.3.1Serial GC发布第一款JVM垃圾收集器串行执行STW时间长2002年JDK1.4.2Parallel GC、CMS GC发布引入多线程回收和并发回收JDK6Parallel GC成为HotSpot默认收集器2012年JDK1.7u4G1 GC可用开启分区域回收新时代2017年JDK9G1成为默认收集器替代CMS2018年JDK11引入Epsilon无操作回收器、ZGC低延迟实验版2019年JDK12增强G1引入Shenandoah GC低延迟实验版2020年JDK14删除CMS收集器ZGC支持MacOS和WindowsJDK17Oracle JDK默认G1OpenJDK默认Shenandoah。5.2 主流垃圾收集器详解面试重点1Serial / Serial Old 收集器最古老最稳定Serial是新生代收集器Serial Old是老年代收集器两者都是串行执行单线程回收回收过程中会STW。算法Serial新生代用复制算法Serial Old老年代用标记-压缩算法特点实现简单、稳定STW时间长适合单CPU环境JVM参数-XX:UseSerialGC启用串行收集器应用场景客户端程序如桌面应用对延迟要求不高。2ParNew 收集器Serial的多线程版本ParNew是新生代收集器本质是Serial的多线程版本——除了使用多线程回收其余行为算法、STW、对象分配规则等与Serial完全一致甚至共用部分代码。算法新生代用复制算法老年代需搭配Serial Old串行特点多线程回收效率比Serial高STW时间比Serial短JVM参数-XX:UseParNewGC启用ParNew、-XX:ParallelGCThreads限制回收线程数应用场景服务端程序适合多CPU环境常与CMS搭配使用。3Parallel / Parallel Old 收集器吞吐量优先Parallel是新生代收集器Parallel Old是老年代收集器两者都是多线程回收核心目标是“提高吞吐量”——可以通过参数动态调整回收策略适配系统运行状态。算法Parallel新生代用复制算法Parallel Old老年代用标记-压缩算法特点吞吐量优先支持自适应调节JVM动态调整回收参数STW时间比ParNew略长JVM参数-XX:UseParallelGC启用Parallel、-XX:UseParallelOldGC启用Parallel Old老年代并行回收应用场景吞吐率优先的服务端程序如计算密集型应用对延迟要求不高。4CMS 收集器低延迟优先CMSConcurrent Mark Sweep是老年代收集器核心目标是“缩短STW时间”适合对响应时间敏感的服务如电商、接口服务。它基于“标记-清除”算法实现运行过程分为4个阶段初始标记STW标记GC Root直接关联的对象速度快并发标记与应用线程并发执行进行GC Root追踪耗时较长重新标记STW修正并发标记期间因应用线程运行导致的标记偏差停顿时间短并发清除与应用线程并发执行清除死亡对象。优点并发回收、低停顿响应时间快缺点产生内存碎片、并发阶段会降低吞吐量且无法等到老年代内存用尽再回收需提前触发否则会导致并发回收失败JVM参数-XX:UseConcMarkSweepGC启用CMS、-XX:UseCMSCompactAtFullCollectionFull GC后整理碎片现状JDK14已删除被G1替代。5G1 收集器目前的主流选择G1Garbage-First是HotSpot开发团队为替代CMS设计的收集器目前是JDK9的默认收集器兼顾低延迟和高吞吐量支持大堆内存如几十G、上百G。它的核心特点的是“分区域回收”和“可预测的停顿时间”。G1的核心特性并行与并发多CPU环境下用多个线程缩短STW时间部分GC动作可与应用线程并发执行分代收集保留新生代和老年代概念但不再是物理连续的区域而是由多个大小相等的“Region”区域组成空间整合整体基于标记-压缩算法局部基于复制算法不会产生内存碎片可预测的停顿用户可指定“在M毫秒内GC停顿时间不超过N毫秒”G1会根据Region的垃圾堆积情况优先回收垃圾最多的区域Garbage-First。G1的内存布局G1将堆内存划分为多个大小相等的Region默认不超过2048个每个Region大小堆总大小/2048可通过-XX:G1HeapRegionSize指定。Region分为4种类型EdenE新生代区域存放新创建的对象SurvivorS新生代区域存放Minor GC后存活的对象OldO老年代区域存放存活时间长的对象HumongousH存放巨型对象大小超过Region的50%直接分配在连续的Region中。G1的回收流程初始标记STW标记GC Root直接关联的对象修改TAMS值确保并发标记时应用线程能正常创建对象并发标记与应用线程并发执行进行可达性分析标记存活对象最终标记STW合并Remembered Set Logs记录Region间的引用关系修正标记偏差筛选回收STW对每个Region的回收价值回收内存量/耗时排序根据用户指定的停顿时间优先回收价值最高的Region用复制算法将存活对象移动到空Region。关键JVM参数-XX:UseG1GC启用G1收集器-XX:G1HeapRegionSizesize指定Region大小1MB~32MB必须是2的幂次方-XX:MaxGCPauseMillistime指定GC最大停顿时间如200ms。6ZGC低延迟的“未来之选”ZGC是JDK11引入的低延迟垃圾收集器JDK15正式转正专注于“最小化停顿时间”毫秒级支持超大堆内存上TB但会牺牲一点吞吐量。核心特点低延迟停顿时间不超过10ms、支持超大堆、并发回收关键技术染色指针将对象标记信息存储在对象指针中而非对象头G1标记在对象头应用场景对低延迟、超大内存有极高要求的服务如金融、高频交易系统选择建议JDK15若需低延迟选ZGC若需平衡吞吐量和延迟选G1。5.3 垃圾收集器选择策略面试必背客户端程序如桌面应用Serial Serial Old吞吐率优先计算密集型服务Parallel Scavenge Parallel Old响应时间优先接口、电商服务ParNew CMS已淘汰推荐G1大堆内存、平衡延迟与吞吐量G1JDK9默认超大堆、极低延迟ZGCJDK15。六、底层补充三色标记算法与常见问题前面提到CMS和G1采用“并发标记”而实现并发标记的核心算法就是“三色标记算法”——它能在不暂停应用线程的情况下完成对象标记减少STW时间。6.1 三色标记算法原理三色标记算法用三种颜色区分对象的标记状态核心是“异步标记”流程如下初始状态所有对象都是白色未被标记线程访问初始标记将GC Root直接关联的对象标记为灰色放入灰色队列GC Root标记为黑色已访问且引用的对象已全部访问并发标记从灰色队列中取出对象将其引用的对象标记为灰色并放入队列当前对象标记为黑色标记结束所有黑色对象为存活对象白色对象为死亡对象可回收。6.2 三色标记的弊端与解决方案并发标记时应用线程仍在运行对象的引用关系可能发生变化会产生两个问题1浮动垃圾标记为存活实际已死亡并发标记期间应用线程断开了GC Root与某个已标记为黑色的对象的引用导致该对象成为垃圾但由于已被标记为黑色不会被本次GC回收——这种垃圾称为“浮动垃圾”。解决方案无需特殊处理浮动垃圾会在下次GC中被回收。2漏标/错杀标记为死亡实际仍存活并发标记期间黑色对象新增了对白色对象的引用同时灰色对象断开了对该白色对象的引用——导致该白色对象被标记为死亡进而被回收引发程序空指针异常。要避免这种问题只需打破两个条件之一黑色对象新增引用、灰色对象断开引用对应两种解决方案增量更新CMS采用破坏“黑色对象新增引用”。在赋值操作前添加“写屏障”记录黑色对象新增的引用关系重新标记阶段STW以该黑色对象为根重新扫描一次避免漏标。原始快照SATBG1采用破坏“灰色对象断开引用”。在赋值操作前添加“写屏障”记录灰色对象断开的引用原始快照标记时默认该引用的对象为存活即使是垃圾也当作浮动垃圾下次GC回收。七、总结掌握GC核心应对面试与工作JVM垃圾回收的核心逻辑的是通过GC Root判断对象可达性用合适的算法回收死亡对象通过收集器实现高效回收最终达到“高吞吐、低停顿”的目标。面试中重点关注这几个点GC Root的4种类型、4种引用类型的区别、4种垃圾收集算法的优劣、G1和ZGC的核心特点、三色标记的弊端与解决方案。最后记住一句话没有最好的垃圾收集算法也没有最好的垃圾收集器只有最合适的——结合业务场景通过测试和调优才能让GC发挥最佳性能。