分代收集理论:年轻代、老年代、元空间

好,咱们来聊聊分代收集理论。说白了,这就是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的过程其实挺有意思的:

  1. 扫描Eden和正在用的那个Survivor区
  2. 把还活着的对象复制到另一个空的Survivor区
  3. 清理掉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

整体流程示意图

好了,说了这么多,我们用一张图把整个分代收集的流程串起来。你重点看对象从出生到晋升、再到被清理的路径。

JVM分代收集核心流程 年轻代 (Young Generation) Eden区 新对象优先分配 S0 存活 S1 空闲 Minor GC触发 老年代 (Old Generation) 老年代 年龄达标或晋升空间不足 晋升 元空间 (Metaspace) — 本地内存 类元数据、常量池、方法元信息 GC事件路径与耗时影响 1. 新对象 → Eden分配 → Eden满 → Minor GC (STW短, 几毫秒到几十毫秒) 2. 存活对象 → 复制到S0/S1 → 年龄++ → 年龄达标 → 晋升老年代 3. 老年代满 → Full GC (STW长, 几百毫秒到几秒) → 影响启动耗时!

你看这张图就很清楚:对象一出生就在Eden。Minor GC发生时,存活的对象要么去另一个Survivor,要么直接晋升到老年代。老年代如果也满了,就会触发耗时的Full GC。元空间因为是本地内存,一般不参与常规GC,但类加载过多也会间接引发GC。

优化提示:如果你发现应用在启动阶段频繁Full GC,可以用-Xlog:gc*参数打印GC日志,重点看晋升速率和对象年龄。我一般会从调整SurvivorRatio和MaxTenuringThreshold入手。