MinorGC的完整流程与复制算法深度解析前言作为一个Java程序员我们每天都在写new Object()但你是否想过这个对象到底放在哪里什么时候被回收JVM的内存管理号称自动化但自动化的背后是一套精密的机制。今天我们就来深入剖析JVM中最频繁发生的垃圾回收——MinorGC看看对象是如何在新生代中出生、“搬家”、晋升的。这是JVM核心机制系列的第一篇后续还会深入动态年龄判定、空间分配担保等潜规则。一、JVM堆内存的分代结构在深入MinorGC之前我们需要先了解JVM堆内存的布局。HotSpot VM将堆内存划分为两个主要区域堆内存布局 ┌─────────────────────────────────────┬─────────────────┐ │ 新生代 │ 老年代 │ ├──────────┬──────────┬──────────────┤ │ │ Eden │ Survivor │ Survivor │ Old │ │ │ (From) │ (To) │ │ └──────────┴──────────┴──────────────┴─────────────────┘1.1 为什么需要分代JVM的设计者通过大量统计发现Java对象具有一个显著特征绝大多数对象都是朝生夕死的。举个例子publicvoidmethod(){ObjectobjnewObject();// 对象出生// ... 一些操作}// 方法结束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 标记-清除算法流程标记所有存活对象清除所有未标记对象类比就像在一个仓库里先给有用的货物贴上标签然后把没贴标签的货物扔掉。优点缺点实现简单不需要移动对象产生内存碎片扔掉的货物留下的空隙不连续效率较高分配大对象时可能找不到连续空间2.2 标记-整理算法流程标记所有存活对象将所有存活对象向一端移动清理边界以外的内存类比整理书架时把要保留的书都推到左边右边空出来的位置可以放新书。优点缺点无内存碎片需要移动对象开销大内存规整分配快Stop The World时间长2.3 复制算法流程将内存分为两块A区和B区每次只使用A区GC时将A区的存活对象复制到B区清空A区交换角色类比你有两个杯子一个装水一个空着。需要清理时把水倒到空杯子里原来的杯子就可以彻底清洗。优点缺点无内存碎片浪费空间总有一半内存空着实现简单效率高不适合存活率高的场景分配内存快连续分配三、为什么MinorGC选用复制算法现在我们回到新生代的特点对象存活率低绝大多数对象很快死亡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区对象年龄设置为1From区对象年龄加1继续上面的例子存活对象A(0岁) → 年龄1 存活对象B(0岁) → 年龄1 存活对象C(3岁) → 年龄4 存活对象D(1岁) → 年龄24.5 Step 3目的地决策更新完年龄后GC需要决定每个对象的去向假设晋升阈值为15对象新年龄是否15去向A1否复制到To区B1否复制到To区C4否复制到To区D2否复制到To区如果某个对象年龄 15它会直接晋升到老年代不经过To区。4.6 Step 4清空与角色交换所有存活对象处理完毕后清空Eden区和From区这两区现在全是垃圾角色交换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两种情况年龄达到阈值默认15动态年龄判定触发下篇文章详解五、代码层面的印证我们可以通过JVM参数来观察和调整这些机制# 设置堆内存大小-Xms1024m-Xmx1024m# 设置新生代与老年代比例1:2-XX:NewRatio2# 设置Eden与Survivor比例8:1:1-XX:SurvivorRatio8# 设置晋升阈值-XX:MaxTenuringThreshold15# 打印GC详细信息-XX:PrintGCDetails示例GC日志2024-01-01T10:00:00.1230800: [GC (Allocation Failure) [PSYoungGen: 51200K-5120K(58880K)] 51200K-10240K(189440K), 0.010s]解读51200K-5120K新生代回收前51200K回收后5120K(58880K)新生代总容量51200K-10240K堆总回收前51200K回收后10240K0.010sGC耗时六、常见面试题Q1为什么新生代要分Eden和两个Survivor答这是为了配合复制算法。如果没有Survivor每次GC后存活对象无处可放只能直接进老年代会导致老年代快速填满。两个Survivor是为了实现角色交换保证始终有一个空区域用于复制。Q2如果只有一个Survivor会怎样答那每次GC只能把存活对象复制到这个唯一的Survivor然后清空Eden。但这样下次GC时就没有空的Survivor可以用了无法继续使用复制算法。Q3Eden区满了就一定触发MinorGC吗答是的Eden区满是最主要的触发条件。但如果是大对象分配可能直接进入老年代通过-XXPretenureSizeThreshold设置。Q4MinorGC会STW吗答会。MinorGC会暂停所有用户线程Stop The World但因为新生代对象少通常暂停时间很短几毫秒到几十毫秒。七、总结JVM分代设计基于朝生夕死的对象特征将堆分为新生代和老年代新生代结构Eden Survivor From Survivor To8:1:1复制算法新生代GC的核心算法空间换时间无碎片MinorGC流程触发 → 标记 → 年龄更新 → 决策去向 → 清空 → 角色交换年龄晋升默认15岁进老年代但实际还有动态判定机制下期预告在本文中我们多次提到年龄达到阈值和To区装不下的情况但实际情况远比这复杂。JVM为了优化GC效率设计了两个重要的潜规则动态年龄判定为什么有的对象不到15岁就进了老年代空间分配担保如果To区装不下谁来兜底敬请期待系列第二篇《JVM核心机制(二)动态年龄判定与空间分配担保——MinorGC背后的潜规则》参考资料《深入理解Java虚拟机》周志明Oracle官方文档Java Garbage Collection BasicsHotSpot VM源码如果你觉得本文有帮助欢迎点赞、评论、转发你的支持是我持续输出的动力。
MinorGC的完整流程与复制算法深度解析
MinorGC的完整流程与复制算法深度解析前言作为一个Java程序员我们每天都在写new Object()但你是否想过这个对象到底放在哪里什么时候被回收JVM的内存管理号称自动化但自动化的背后是一套精密的机制。今天我们就来深入剖析JVM中最频繁发生的垃圾回收——MinorGC看看对象是如何在新生代中出生、“搬家”、晋升的。这是JVM核心机制系列的第一篇后续还会深入动态年龄判定、空间分配担保等潜规则。一、JVM堆内存的分代结构在深入MinorGC之前我们需要先了解JVM堆内存的布局。HotSpot VM将堆内存划分为两个主要区域堆内存布局 ┌─────────────────────────────────────┬─────────────────┐ │ 新生代 │ 老年代 │ ├──────────┬──────────┬──────────────┤ │ │ Eden │ Survivor │ Survivor │ Old │ │ │ (From) │ (To) │ │ └──────────┴──────────┴──────────────┴─────────────────┘1.1 为什么需要分代JVM的设计者通过大量统计发现Java对象具有一个显著特征绝大多数对象都是朝生夕死的。举个例子publicvoidmethod(){ObjectobjnewObject();// 对象出生// ... 一些操作}// 方法结束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 标记-清除算法流程标记所有存活对象清除所有未标记对象类比就像在一个仓库里先给有用的货物贴上标签然后把没贴标签的货物扔掉。优点缺点实现简单不需要移动对象产生内存碎片扔掉的货物留下的空隙不连续效率较高分配大对象时可能找不到连续空间2.2 标记-整理算法流程标记所有存活对象将所有存活对象向一端移动清理边界以外的内存类比整理书架时把要保留的书都推到左边右边空出来的位置可以放新书。优点缺点无内存碎片需要移动对象开销大内存规整分配快Stop The World时间长2.3 复制算法流程将内存分为两块A区和B区每次只使用A区GC时将A区的存活对象复制到B区清空A区交换角色类比你有两个杯子一个装水一个空着。需要清理时把水倒到空杯子里原来的杯子就可以彻底清洗。优点缺点无内存碎片浪费空间总有一半内存空着实现简单效率高不适合存活率高的场景分配内存快连续分配三、为什么MinorGC选用复制算法现在我们回到新生代的特点对象存活率低绝大多数对象很快死亡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区对象年龄设置为1From区对象年龄加1继续上面的例子存活对象A(0岁) → 年龄1 存活对象B(0岁) → 年龄1 存活对象C(3岁) → 年龄4 存活对象D(1岁) → 年龄24.5 Step 3目的地决策更新完年龄后GC需要决定每个对象的去向假设晋升阈值为15对象新年龄是否15去向A1否复制到To区B1否复制到To区C4否复制到To区D2否复制到To区如果某个对象年龄 15它会直接晋升到老年代不经过To区。4.6 Step 4清空与角色交换所有存活对象处理完毕后清空Eden区和From区这两区现在全是垃圾角色交换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两种情况年龄达到阈值默认15动态年龄判定触发下篇文章详解五、代码层面的印证我们可以通过JVM参数来观察和调整这些机制# 设置堆内存大小-Xms1024m-Xmx1024m# 设置新生代与老年代比例1:2-XX:NewRatio2# 设置Eden与Survivor比例8:1:1-XX:SurvivorRatio8# 设置晋升阈值-XX:MaxTenuringThreshold15# 打印GC详细信息-XX:PrintGCDetails示例GC日志2024-01-01T10:00:00.1230800: [GC (Allocation Failure) [PSYoungGen: 51200K-5120K(58880K)] 51200K-10240K(189440K), 0.010s]解读51200K-5120K新生代回收前51200K回收后5120K(58880K)新生代总容量51200K-10240K堆总回收前51200K回收后10240K0.010sGC耗时六、常见面试题Q1为什么新生代要分Eden和两个Survivor答这是为了配合复制算法。如果没有Survivor每次GC后存活对象无处可放只能直接进老年代会导致老年代快速填满。两个Survivor是为了实现角色交换保证始终有一个空区域用于复制。Q2如果只有一个Survivor会怎样答那每次GC只能把存活对象复制到这个唯一的Survivor然后清空Eden。但这样下次GC时就没有空的Survivor可以用了无法继续使用复制算法。Q3Eden区满了就一定触发MinorGC吗答是的Eden区满是最主要的触发条件。但如果是大对象分配可能直接进入老年代通过-XXPretenureSizeThreshold设置。Q4MinorGC会STW吗答会。MinorGC会暂停所有用户线程Stop The World但因为新生代对象少通常暂停时间很短几毫秒到几十毫秒。七、总结JVM分代设计基于朝生夕死的对象特征将堆分为新生代和老年代新生代结构Eden Survivor From Survivor To8:1:1复制算法新生代GC的核心算法空间换时间无碎片MinorGC流程触发 → 标记 → 年龄更新 → 决策去向 → 清空 → 角色交换年龄晋升默认15岁进老年代但实际还有动态判定机制下期预告在本文中我们多次提到年龄达到阈值和To区装不下的情况但实际情况远比这复杂。JVM为了优化GC效率设计了两个重要的潜规则动态年龄判定为什么有的对象不到15岁就进了老年代空间分配担保如果To区装不下谁来兜底敬请期待系列第二篇《JVM核心机制(二)动态年龄判定与空间分配担保——MinorGC背后的潜规则》参考资料《深入理解Java虚拟机》周志明Oracle官方文档Java Garbage Collection BasicsHotSpot VM源码如果你觉得本文有帮助欢迎点赞、评论、转发你的支持是我持续输出的动力。