MinorGC的完整流程与复制算法深度解析

MinorGC的完整流程与复制算法深度解析


前言

作为一个Java程序员,我们每天都在写 new Object(),但你是否想过:这个对象到底放在哪里?什么时候被回收?

JVM的内存管理号称"自动化",但自动化的背后是一套精密的机制。今天,我们就来深入剖析JVM中最频繁发生的垃圾回收------MinorGC,看看对象是如何在新生代中"出生"、"搬家"、"晋升"的。

这是JVM核心机制系列的第一篇,后续还会深入动态年龄判定、空间分配担保等"潜规则"。


一、JVM堆内存的分代结构

在深入MinorGC之前,我们需要先了解JVM堆内存的布局。HotSpot VM将堆内存划分为两个主要区域:

复制代码
堆内存布局:
┌─────────────────────────────────────┬─────────────────┐
│             新生代                   │     老年代      │
├──────────┬──────────┬──────────────┤                 │
│  Eden    │ Survivor │   Survivor   │       Old       │
│          │   (From) │    (To)      │                 │
└──────────┴──────────┴──────────────┴─────────────────┘

1.1 为什么需要分代?

JVM的设计者通过大量统计发现,Java对象具有一个显著特征:绝大多数对象都是"朝生夕死"的

举个例子:

java 复制代码
public void method() {
    Object obj = new Object();  // 对象出生
    // ... 一些操作
}  // 方法结束,obj失去引用,变成垃圾

像这样在方法内部创建的临时对象,往往很快就可以被回收。而那些长期存活的对象(如缓存对象、单例对象、Session对象等)则会一直存在。

基于这个特点,JVM将堆内存分代:

  • 新生代:存放"短命"对象,GC频繁,但每次回收快
  • 老年代:存放"长命"对象,GC较少,但每次回收慢

1.2 新生代的"三驾马车"

新生代内部又细分为三个区域,比例通常是 8:1:1 (可通过 -XX:SurvivorRatio 调整):

  • Eden区:对象出生的地方。绝大多数新创建的对象都在这里分配内存。
  • Survivor From区:经历过一次GC但未晋升的对象,放在这里"休养"。
  • Survivor To区 :每次GC时,存活对象的目的地。这个区域始终是空的,等待接收数据。

为什么要两个Survivor?这就是复制算法的核心,我们马上讲到。


二、GC算法的三剑客:标记-清除、标记-整理、复制算法

要理解为什么MinorGC选择复制算法,我们需要先了解三种主流GC算法的优缺点。

2.1 标记-清除算法

流程

  1. 标记所有存活对象
  2. 清除所有未标记对象

类比:就像在一个仓库里,先给有用的货物贴上标签,然后把没贴标签的货物扔掉。

优点 缺点
实现简单,不需要移动对象 产生内存碎片(扔掉的货物留下的空隙不连续)
效率较高 分配大对象时可能找不到连续空间

2.2 标记-整理算法

流程

  1. 标记所有存活对象
  2. 将所有存活对象向一端移动
  3. 清理边界以外的内存

类比:整理书架时,把要保留的书都推到左边,右边空出来的位置可以放新书。

优点 缺点
无内存碎片 需要移动对象,开销大
内存规整,分配快 Stop The World时间长

2.3 复制算法

流程

  1. 将内存分为两块(A区和B区)
  2. 每次只使用A区
  3. GC时,将A区的存活对象复制到B区
  4. 清空A区,交换角色

类比:你有两个杯子,一个装水,一个空着。需要清理时,把水倒到空杯子里,原来的杯子就可以彻底清洗。

优点 缺点
无内存碎片 浪费空间(总有一半内存空着)
实现简单,效率高 不适合存活率高的场景
分配内存快(连续分配)

三、为什么MinorGC选用复制算法?

现在我们回到新生代的特点:

  1. 对象存活率低:绝大多数对象很快死亡
  2. GC频率高:需要快速回收

基于这两个特点,我们来看三种算法的适配性:

3.1 淘汰标记-清除算法

  • 原因 :新生代GC频繁,如果使用标记-清除,会快速产生大量内存碎片。很快就会出现"有空闲内存但分配不了大对象"的尴尬局面。

3.2 淘汰标记-整理算法

  • 原因:每次GC都要移动大量对象(虽然存活对象少,但整理操作本身有开销),而且新生代对象多,频繁移动会影响性能。

3.3 复制算法胜出

  • 原因
    • 存活对象少 → 需要复制的对象少 → 复制开销小
    • 无内存碎片 → 分配新对象快
    • 虽然浪费了Survivor区(一半空间),但这是空间换时间的合理权衡

一句话总结:新生代用复制算法,是在"对象存活率低"这个前提下,做出的最优选择。


四、MinorGC完整流程详解

有了前面的铺垫,现在我们可以完整地走一遍MinorGC的全过程。

4.1 触发条件

Eden区已满。当有新对象需要分配,但Eden区剩余空间不足时,JVM就会触发MinorGC。

4.2 假设初始状态

假设:

  • Eden区有一些存活和死亡对象
  • Survivor From区有一些年龄不等的存活对象
  • Survivor To区为空

初始状态示意图:

复制代码
Eden:    [存活对象A(0岁)] [死亡对象] [存活对象B(0岁)]
From:    [存活对象C(3岁)] [存活对象D(1岁)] 
To:      [空]

4.3 Step 1:标记存活对象

GC线程通过可达性分析,从GC Roots出发,标记出Eden区和From区中所有存活的对象。

GC Roots包括:栈帧中的局部变量、静态变量、JNI引用等。

4.4 Step 2:年龄计算与更新

对于每个标记为存活的对象,GC会进行年龄处理:

对象来源 年龄更新规则
Eden区对象 年龄设置为1
From区对象 年龄加1

继续上面的例子:

复制代码
存活对象A(0岁) → 年龄=1
存活对象B(0岁) → 年龄=1
存活对象C(3岁) → 年龄=4
存活对象D(1岁) → 年龄=2

4.5 Step 3:目的地决策

更新完年龄后,GC需要决定每个对象的去向(假设晋升阈值为15):

对象 新年龄 是否>=15 去向
A 1 复制到To区
B 1 复制到To区
C 4 复制到To区
D 2 复制到To区

如果某个对象年龄 >= 15,它会直接晋升到老年代,不经过To区。

4.6 Step 4:清空与角色交换

所有存活对象处理完毕后:

  1. 清空Eden区和From区(这两区现在全是垃圾)
  2. 角色交换:To区变成新的From区,原来的From区变成To区(空)

GC后的状态:

复制代码
Eden:    [空]
From:    [对象A(1岁)] [对象B(1岁)] [对象C(4岁)] [对象D(2岁)]  (← 原来的To区)
To:      [空]  (← 原来的From区)
老年代:   [无变化]

4.7 关键问题解答

Q:年龄到底什么时候更新?

A:在复制到To区之前。先计算新年龄,再决定是去To区还是老年代。

Q:如果To区装不下怎么办?

A:这就涉及空间分配担保机制,我们会在后面发布的文章详细讲解。

Q:什么时候对象会晋升?

A:两种情况:

  1. 年龄达到阈值(默认15)
  2. 动态年龄判定触发(下篇文章详解)

五、代码层面的印证

我们可以通过JVM参数来观察和调整这些机制:

bash 复制代码
# 设置堆内存大小
-Xms1024m -Xmx1024m

# 设置新生代与老年代比例(1:2)
-XX:NewRatio=2

# 设置Eden与Survivor比例(8:1:1)
-XX:SurvivorRatio=8

# 设置晋升阈值
-XX:MaxTenuringThreshold=15

# 打印GC详细信息
-XX:+PrintGCDetails

示例GC日志:

复制代码
2024-01-01T10:00:00.123+0800: [GC (Allocation Failure) 
  [PSYoungGen: 51200K->5120K(58880K)] 51200K->10240K(189440K), 0.010s]

解读:

  • 51200K->5120K:新生代回收前51200K,回收后5120K
  • (58880K):新生代总容量
  • 51200K->10240K:堆总回收前51200K,回收后10240K
  • 0.010s:GC耗时

六、常见面试题

Q1:为什么新生代要分Eden和两个Survivor?

:这是为了配合复制算法。如果没有Survivor,每次GC后存活对象无处可放,只能直接进老年代,会导致老年代快速填满。两个Survivor是为了实现角色交换,保证始终有一个空区域用于复制。

Q2:如果只有一个Survivor会怎样?

:那每次GC只能把存活对象复制到这个唯一的Survivor,然后清空Eden。但这样下次GC时,就没有空的Survivor可以用了,无法继续使用复制算法。

Q3:Eden区满了就一定触发MinorGC吗?

:是的,Eden区满是最主要的触发条件。但如果是大对象分配,可能直接进入老年代(通过 -XX:PretenureSizeThreshold 设置)。

Q4:MinorGC会STW吗?

:会。MinorGC会暂停所有用户线程(Stop The World),但因为新生代对象少,通常暂停时间很短(几毫秒到几十毫秒)。


七、总结

  1. JVM分代设计:基于"朝生夕死"的对象特征,将堆分为新生代和老年代
  2. 新生代结构:Eden + Survivor From + Survivor To(8:1:1)
  3. 复制算法:新生代GC的核心算法,空间换时间,无碎片
  4. MinorGC流程:触发 → 标记 → 年龄更新 → 决策去向 → 清空 → 角色交换
  5. 年龄晋升:默认15岁进老年代,但实际还有动态判定机制

下期预告

在本文中我们多次提到"年龄达到阈值"和"To区装不下"的情况,但实际情况远比这复杂。JVM为了优化GC效率,设计了两个重要的"潜规则":

  • 动态年龄判定:为什么有的对象不到15岁就进了老年代?
  • 空间分配担保:如果To区装不下,谁来兜底?

敬请期待系列第二篇:《JVM核心机制(二):动态年龄判定与空间分配担保------MinorGC背后的"潜规则"》


参考资料

  1. 《深入理解Java虚拟机》周志明
  2. Oracle官方文档:Java Garbage Collection Basics
  3. HotSpot VM源码

如果你觉得本文有帮助,欢迎点赞、评论、转发!你的支持是我持续输出的动力。

相关推荐
zhouping@1 小时前
JAVA学习笔记day06
java·笔记·学习
Queenie_Charlie1 小时前
Manacher算法
c++·算法·manacher
闻缺陷则喜何志丹1 小时前
【树的直径 离散化】 P7807 魔力滋生|普及+
c++·算法·洛谷·离散化·树的直径
AI_Ming2 小时前
Seq2Seq-大模型知识点(程序员转行AI大模型学习)
算法·ai编程
毕设源码-郭学长2 小时前
【开题答辩全过程】以 某某协会管理与展示平台为例,包含答辩的问题和答案
java
若水不如远方2 小时前
分布式一致性(六):拥抱可用性 —— 最终一致性与 Gossip 协议
分布式·后端·算法
计算机安禾2 小时前
【C语言程序设计】第35篇:文件的打开、关闭与读写操作
c语言·开发语言·c++·vscode·算法·visual studio code·visual studio
多云的夏天2 小时前
docker容器部署-windows-ubuntu
java·docker·容器
Wect2 小时前
React Hooks 核心原理
前端·算法·typescript