分代收集理论:年轻代、老年代、元空间
好,咱们来聊聊分代收集理论。说白了,这就是JVM能高效管理内存的核心策略之一,也是我们做GC抑制优化时必须掌握的理论基础。
分代收集的底层逻辑
为什么要把堆内存分成几代?我个人的理解是这样的:大部分对象的生命周期都很短,用完就“死”了;而少部分对象会活很久。你想想,如果每次GC都去扫描整个堆,那得多浪费?这就是典型的「二八原则」。把对象按年龄分区管理,用不同的策略处理,效率自然就上来了。
核心观点:分代收集的核心就是「弱分代假说」——大多数对象朝生夕死,熬过几次GC的对象倾向于长期存活。
基于这个假说,JVM把堆分成了三个逻辑区域:年轻代(Young Generation)、老年代(Old Generation),还有JDK 8之后的元空间(Metaspace)。它们各有各的使命。
年轻代:对象的摇篮
年轻代是所有新对象诞生的地方。它又分了三块:一个Eden区和两个Survivor区(通常是S0、S1)。
为什么这么设计?我有个习惯:每次新建对象,JVM都会把它丢进Eden。Eden满了怎么办?触发Minor GC(也叫Young GC)。
Minor GC的过程其实挺有意思的:
- 扫描Eden和正在用的那个Survivor区
- 把还活着的对象复制到另一个空的Survivor区
- 清理掉Eden和之前的Survivor区
每次熬过GC,对象的年龄就加1。当年龄达到阈值(默认15,可通过-XX:MaxTenuringThreshold设置),或者Survivor区装不下了,这些对象就会被「晋升」到老年代。
我的经验:我在项目中遇到过因为Survivor区太小,导致对象过早晋升老年代的问题。启动时大量临时对象直接进了老年代,触发Full GC,启动耗时一下子飙上去。这时候调大SurvivorRatio往往能解决问题。
老年代:持久对象的家
老年代用来存放那些长生命周期对象。我之前的项目里,缓存对象、长期持久的线程池、连接池,基本都会落在这。
老年代的GC叫Major GC或Full GC。这可比Minor GC重得多,因为它通常需要整个堆都停一停(Stop-the-World)。触发条件包括:
- 老年代空间不足
- 晋升对象大小超过剩余空间
- 显式调用
System.gc()(虽然不太建议)
你看,如果因为启动时大量对象过早晋升,老年代塞得太满,Full GC一来,启动耗时直接就崩了。这就是我们要做GC抑制优化的原因之一。
元空间:方法区的进化
说到元空间,就不得不提它的前任——永久代(PermGen)。JDK 7及以前,类的元数据放在PermGen里,大小固定,很容易就OOM了。我记得有一次线上应用因为动态类加载太多,直接报java.lang.OutOfMemoryError: PermGen space,搞得我被迫重启。
从JDK 8开始,永久代被移除,改成了元空间。这东西最大的变化就是:不再使用堆内存,而是用本地内存。这意味着你不需要再手动设置-XX:MaxPermSize了,默认情况下只要系统内存够用,它能自动扩展。
避坑指南:我曾经遇到过因为元空间设置了过小的-XX:MaxMetaspaceSize,导致应用频繁Full GC。实际上,在GC抑制优化的启动场景下,我建议保持元空间的自动扩展能力,不要主动设上限,除非你对类加载量有的把握。
| 分区 | 存储内容 | GC类型 | 特点 |
|---|---|---|---|
| 年轻代 | 短生命周期对象 | Minor GC | 频率高、速度快、STW短 |
| 老年代 | 长生命周期对象 | Major GC / Full GC | 频率低、速度慢、STW长 |
| 元空间 | 类元数据、常量池 | 无专门GC(依赖Full GC) | 使用本地内存、自动扩展 |
关键指标:在GC抑制优化中,重点关注年轻代的大小配置和晋升速率。如果启动期内触发Full GC,多半是晋升过快或老年代不足。
这里再补充一点常见的配置参数,方便你理解:
// 年轻代大小
-XX:NewSize=256m
-XX:MaxNewSize=256m
// 老年代与年轻代比例(默认2,即老年代:年轻代=2:1)
-XX:NewRatio=2
// Survivor区比例(默认8,即Eden:S0:S1=8:1:1)
-XX:SurvivorRatio=8
// 晋升年龄阈值(默认15)
-XX:MaxTenuringThreshold=15
整体流程示意图
好了,说了这么多,我们用一张图把整个分代收集的流程串起来。你重点看对象从出生到晋升、再到被清理的路径。
你看这张图就很清楚:对象一出生就在Eden。Minor GC发生时,存活的对象要么去另一个Survivor,要么直接晋升到老年代。老年代如果也满了,就会触发耗时的Full GC。元空间因为是本地内存,一般不参与常规GC,但类加载过多也会间接引发GC。
优化提示:如果你发现应用在启动阶段频繁Full GC,可以用-Xlog:gc*参数打印GC日志,重点看晋升速率和对象年龄。我一般会从调整SurvivorRatio和MaxTenuringThreshold入手。