内存都要回收,为什么JVM偏要把堆分成新生代和老年代

发布时间:2026/9/9 3:24:09
内存都要回收,为什么JVM偏要把堆分成新生代和老年代 「Java 进阶之路」系列 Day34写在前面上一篇讲了标记清除、标记整理、复制算法这三种基础回收算法结尾留了个疑问现代JVM从来不会只用一种算法从头到尾扫一遍堆而是把堆拆成新生代、老年代分开处理。这篇就讲清楚分代收集这套思想到底是怎么落地的以及CMS、G1、ZGC这几款常见收集器分别是怎么在这套思想上做取舍的。一、是什么分代收集把堆拆成几块分别处理分代收集的核心假设是弱分代假说绝大多数对象都是朝生夕死的只有极少数对象能活得很久。基于这个假设JVM把堆分成新生代Young Generation和老年代Old Generation对不同区域采用不同的回收算法和回收频率。堆老年代新生代Minor GC存活对象下次Minor GC存活多次存活年龄够了老年代Eden区Survivor fromSurvivor to新生代内部又按8:1:1的比例分成一块Eden区和两块Survivor区from、to。新对象几乎都优先在Eden区分配一次Minor GC发生时Eden区和当前使用的Survivor区里的存活对象会被复制到另一块空闲的Survivor区然后把原来那两块整体清空——这正是上一篇讲的复制算法因为新生代存活率低复制成本极低。对象每熬过一次Minor GC年龄就加一年龄达到一定阈值默认15可以通过-XX:MaxTenuringThreshold调整之后就会被晋升到老年代。老年代空间大、对象存活率高更适合标记整理或标记清除对应的回收动作叫Major GC或者叫Full GC含义上会更宽泛一些通常指连老年代、方法区一起回收的动作。二、为什么要这样设计把大多数时候很快和少数时候要彻底分开如果堆不分代每次垃圾回收都要扫描整个堆——包括那些已经存活了很久、大概率还会继续存活的对象——这些扫描大部分都是白费功夫。分代收集的价值在于用最匹配对象生命周期特征的算法去处理对应区域把频繁但便宜的回收和稀少但昂贵的回收分开新生代对象死得快、存活率低 → 用复制算法做Minor GC回收频繁但每次都很快复制的对象少老年代对象活得久、存活率高 → 用标记整理做Major GC回收次数少但每次代价更高要扫描和整理的存活对象多这样设计之后用户线程大部分时间只会被频繁但短暂的Minor GC打断真正开销大的Major GC很少发生——这正是分代收集相比不分代、每次都全堆扫描能大幅降低平均停顿时间的原因。不过这套设计本身也带来一个新问题老年代的对象可能引用新生代的对象比如一个老对象把某个新建的对象设为自己的字段Minor GC只扫新生代的话怎么知道这个新对象被老年代引用着、不能被误判为垃圾JVM用**记忆集Remembered Set 卡表Card Table**解决——把老年代划分成一个个卡只要某张卡里有对象引用了新生代就把这张卡标记为脏Minor GC时只需要扫描这些被标记的卡而不用扫描整个老年代去找跨代引用这样既解决了跨代引用的问题又没有牺牲Minor GC只处理新生代这个效率优势。三、怎么用CMS、G1、ZGC分别怎么取舍分代收集是思想框架具体落地成什么样的收集器取决于对停顿时间和吞吐量这两个目标怎么取舍。CMS老年代第一款以低停顿为目标的收集器CMSConcurrent Mark Sweep专注老年代核心思路是把标记过程和用户线程并发执行尽量减少stop-the-world独占CPU的时间初始标记 短暂停顿并发标记 与用户线程同时跑重新标记 短暂停顿并发清除 与用户线程同时跑只有初始标记标记GC Roots直接关联的对象和重新标记修正并发标记期间用户线程造成的变动这两步需要停顿且耗时都很短耗时最长的并发标记、并发清除都能和用户线程一起跑。代价是用的是标记清除算法天生有内存碎片问题而且和用户线程并发运行会占用CPU资源吞吐量会下降此外CMS已经在JDK9标记为过时deprecatedJDK14中被彻底移除生产环境基本被G1取代。G1把堆拆成很多小Region哪块垃圾最多先回收哪块G1Garbage First不再严格区分连续的新生代、老年代物理空间而是把整个堆拆成很多大小相等的小Region每个Region可以被动态划分成Eden、Survivor或Old的角色。回收时G1会优先挑选回收收益最高垃圾最多的那些Region来回收——这也是Garbage First这个名字的来源。堆划分成很多Region每个Region独立标记存活对象按每个Region能回收的垃圾量排序优先回收垃圾最多的那批Region因为每次只挑一部分Region回收不需要整个老年代一起处理G1可以让用户配置一个期望的最大停顿时间目标-XX:MaxGCPauseMillisG1会尽量按这个目标去控制每次回收处理的Region数量。跨Region的引用问题G1同样是用记忆集来解决只是粒度从老年代整体细化到了每个Region。整体上G1兼顾了停顿时间和吞吐量是JDK9之后的默认收集器。ZGC把停顿时间压到几乎和堆大小无关G1的停顿时间虽然可控但堆越大标记、整理这些阶段能并发的部分之外仍有一些和存活对象数量相关的停顿。ZGC的目标更激进不管堆有多大从几百MB到几TB停顿时间都要控制在几毫秒以内。它做到这一点的关键手段是染色指针Colored Pointer和读屏障Load Barrier——把对象的部分标记信息直接存在指针本身的多余比特位里配合读屏障在用户线程访问对象引用的同时完成对象的转移、重定位把原本需要停顿处理的工作尽可能挪到和用户线程并发执行的阶段去做只在少数几个必须同步的时间点上有极短的停顿。代价是染色指针对指针位数有限制、需要额外的CPU和内存带宽开销但换来的低延迟对于电商大促、金融交易这类对响应时间敏感的场景价值很高。一句话总结这条演进路线CMS解决了老年代能不能并发G1解决了停顿时间能不能可控、可预期ZGC解决了停顿时间能不能几乎不受堆大小影响。四、面试追问Q1什么是分代收集为什么要分代分代收集基于弱分代假说——大多数对象朝生夕死只有少数能长期存活所以把堆拆成新生代、老年代分别处理新生代存活率低用复制算法做频繁但快速的Minor GC老年代存活率高用标记整理做次数少但代价更高的Major GC。这样能把大部分回收工作控制在开销很小的Minor GC里减少真正昂贵的全堆扫描的发生频率。Q2Minor GC时怎么处理老年代对象引用新生代对象的情况用记忆集和卡表。JVM把老年代划分成一个个卡只要某张卡里的对象引用了新生代对象就把这张卡标记为脏。Minor GC时只需要扫描被标记为脏的卡而不用扫描整个老年代既能正确识别这些跨代引用避免误回收又不会牺牲Minor GC只处理新生代带来的效率优势。Q3CMS收集器最大的问题是什么因为CMS的清除阶段用的是标记清除算法不会整理内存所以会产生大量不连续的内存碎片可能导致明明总的空闲内存够、但因为没有足够大的连续空间而提前触发一次Full GC。另外并发标记、并发清除阶段会和用户线程抢占CPU资源导致吞吐量下降且CMS已在JDK14中被移除。Q4G1是怎么做到停顿时间可控的G1把整个堆拆成很多大小相等的Region不再要求新生代、老年代各自是连续空间。每次回收时优先挑选垃圾最多、回收收益最高的那些Region来处理而不需要一次性处理整个老年代。用户可以配置期望的最大停顿时间目标G1会据此动态调整每次回收涉及的Region数量从而把停顿时间控制在目标范围内。Q5ZGC相比G1的核心突破是什么ZGC的目标是让停顿时间几乎不随堆大小增长不管堆是几百MB还是几TB停顿都能控制在几毫秒以内。它靠染色指针把部分标记信息编码进指针本身配合读屏障让对象的转移、重定位等原本需要停顿完成的工作也能和用户线程并发执行只在极少数必须同步的点上有极短停顿适合对响应时间非常敏感的大内存场景。下一篇预告Day35 讲对象到底什么时候会被判定为垃圾——可达性分析算法的原理以及强引用、软引用、弱引用、虚引用这四种引用类型分别解决了什么问题。