垃圾回收机制

990664862 发布于 27 天前 83 次阅读


Java 堆内存是垃圾回收的核心区域,JVM 把堆划分为新生代(Young)、老年代(Old/Tenured),外加堆外的元空间(Metaspace),不同区域存放不同生命周期对象,使用不同回收算法,提升 GC 效率。

一、整体内存划分(堆)

二、新生代 Young Generation

1. 作用

存放刚创建、生命周期短的对象(局部变量、临时集合、临时字符串等)。

绝大多数对象很快就会死亡,所以新生代回收频率最高、速度最快。

2. 分区比例(默认 Eden:S0:S1 = 8:1:1)

  1. Eden 伊甸区所有新对象优先分配在这里。Eden 占新生代 80% 空间。Eden 满了触发 Minor GC(新生代垃圾回收)。
  2. 两个 Survivor 幸存区(S0 / S1)同一时间只有一块是活跃区,另一块是空的复制区。Minor GC 流程:
    1. 标记 Eden + 活跃 Survivor 存活对象;
    2. 把存活对象全部复制到空的 Survivor;
    3. 清空 Eden 和原来的 Survivor;
    4. 交换两块 Survivor 的角色。

3. 对象晋升到老年代规则

对象每熬过一次 Minor GC,年龄 age +1

  • 默认年龄达到 15,直接晋升到老年代;
  • 若 Survivor 中同年龄对象总和 ≥ Survivor 一半空间,等于该年龄及以上对象全部提前晋升;
  • 大对象(超长数组、大字符串)直接跳过新生代,分配到老年代,避免 Eden 复制开销。

4. Minor GC 特点

  • 只回收新生代;
  • 使用复制算法,无内存碎片;
  • 会 Stop-The-World(STW,暂停所有用户线程),但停顿很短。
  • 绝大多数Eden里的对象都是垃圾,一次Minor GC后里绝大部分直接丢弃,根本不会复制

三、老年代 Old / Tenured Generation

1. 存放对象

  1. 新生代熬过多次 GC、年龄达标晋升的长期存活对象
  2. 程序全局长生命周期对象(静态变量、缓存、数据库连接池);
  3. 直接分配的超大对象。

2. 回收机制

老年代空间不足时触发 Major GC / Full GC

  • Major GC:仅老年代回收;
  • Full GC:整堆(新生代 + 老年代)全部回收,STW 时间极长,生产环境要尽量避免。

3. 使用算法

老年代对象存活率高,不适合复制算法,主流两种:

  1. 标记 - 清除:标记死亡对象,直接清空,会产生内存碎片;流程两步
    1. 标记阶段从 GC Roots 遍历,把所有有引用、存活的对象打上标记。
    2. 清除阶段遍历整个堆,把没有标记的垃圾对象直接回收,释放对应内存。示意图逻辑[存活][垃圾][存活][垃圾][垃圾][存活]
      标记完成后,直接删掉垃圾:
      [存活]   [存活]       [存活]
    优点:不需要复制对象,不额外占用内存空间,实现简单。❌ 致命缺点:
    1. 产生大量内存碎片回收后空闲内存是分散的小块,当需要分配大对象时,哪怕总空闲内存足够,但没有连续空间,会直接触发 Full GC。
    2. 两次遍历堆,效率一般。
  2. 标记 - 整理:标记存活对象,全部向一侧压缩,消除碎片(CMS、G1、ZGC 常用)。流程三步
    1. 标记阶段:和标记清除完全一样,标记所有存活对象。
    2. 整理(压缩)阶段将所有存活对象向内存一端统一移动、紧密排列。
    3. 清理边界外全部垃圾,只留一块连续大空闲内存。示意图逻辑[存活][垃圾][存活][垃圾][垃圾][存活]
      标记后全部向左压缩:
      [存活][存活][存活]               大片连续空闲
    ✅ 优点:
    1. 无内存碎片,空闲内存是一整块连续空间,分配大对象无压力;
    2. 内存分配速度快(直接指针碰撞)。
    ❌ 缺点:需要移动所有存活对象,修改对象引用地址,STW 停顿时间更长,开销比标记清除大。

4. 拓展:指针碰撞与空闲列表

指针碰撞(Bump-The-Pointer):适用于无碎片的连续存储空间,如新生代的eden和标记-整理后的老年代

原理

堆里维护一个分配指针(allocation pointer),指向当前空闲区域的起始位置。

  1. 新建对象需要内存时,直接拿指针当前地址分配;
  2. 指针向后滑动对象所占内存大小;
  3. 下一个新对象直接分配在指针新位置。

空闲列表(Free-List):适用于内存存在大量碎片(标记 - 清除后的堆),空闲内存是零散小块

原理

1. JVM 维护一张列表,记录每一块空闲内存的地址、大小;

2. 创建对象时遍历列表,找一块能放下的空闲块(类似操作系统内 存分配),并将该内存块从free list中移除

3. 需要链表查找、拆分空闲块,性能远低于指针碰撞。

四、元空间 Metaspace(非堆,不参与堆 GC)

以前 JDK7 及以下叫永久代 PermGen,JDK8 彻底移除,换成元空间。

  1. 不在堆内,使用本地直接内存;
  2. 存放:类字节码、常量池、方法信息、注解;
  3. 不会像永久代一样 OOM(java.lang.OutOfMemoryError,内存溢出),默认自动扩容;
  4. 仅在类卸载时回收,和新生代 / 老年代无关。

五、补充关键名词

  1. Minor GC:新生代回收
  2. Major GC:老年代回收
  3. Full GC:新生代 + 老年代 + 元空间全部回收,代价最大
  4. STW(Stop The World):GC 时暂停业务线程,用户程序卡死
  5. 对象年龄 Age:记录对象经历过多少次 Minor GC
  6. 晋升 Promotion:对象从新生代移动到老年代

六、完整对象生命周期示例

  1. new 对象 → 分配 Eden;
  2. Eden 满,触发 Minor GC,存活对象复制到 S0;age=1;
  3. 再次 Eden 满,Minor GC,存活对象复制到 S1;age=2;
  4. 重复 15 次 GC,age=15 → 晋升到老年代;
  5. 老年代空间不足 → Major GC或Full GC,清理长期存活对象。

七、为什么要分代?

降低 STW 耗时:大部分 GC 只是 Minor GC,停顿很短,Full GC 极少触发。

分治回收:短生命周期对象集中在新生代,快速清理;长生命周期对象少,老年代低频回收;

算法适配:新生代存活率低用复制算法,效率高;老年代存活率高用标记整理,节省内存;

此作者没有提供个人介绍。
最后更新于 2026-07-04