JVM垃圾回收 前言JVM垃圾回收是Java自动内存管理的核心机制它会自动识别并回收不再被程序使用的对象内存无需开发者手动分配和释放内存从根源上避免了手动内存管理容易出现的内存泄漏、野指针等问题。下面从核心原理、关键机制、常见实现和优化思路几个维度完整解析一、垃圾回收的核心前提垃圾回收的主要工作区域是JVM堆内存这里几乎存储了所有Java对象实例方法区也会纳入部分回收逻辑。判断对象是否可回收的核心依据是‌可达性分析‌从GC Roots根对象集合包含栈中的局部变量、常量引用、静态变量等程序可直接访问的对象出发遍历引用链最终无法到达的对象就会被判定为垃圾对象可以被回收。1对象什么时候可以被垃圾回收器回收2怎么确定垃圾?引用计数法和可达性分析算法2.1引用计数法引用计数法带来的问题出现上面的情况导致anull、bnull导致引用断开但是ref没有0 而是 ref-11 这个就存在了内存泄露无法被垃圾回收器回收2.2 可达性分析算法二、经典垃圾回收算法不同算法针对不同场景的痛点做了针对性优化主流的实现分为四类‌1标记-清除算法‌分为标记和清除两个阶段先标记所有存活对象再直接清理未标记的垃圾对象。实现逻辑简单但缺点是会产生大量内存碎片后续分配大对象时很容易提前触发GC更适合存活对象多的老年代场景。‌2复制算法‌将内存划分为两块独立区域每次只使用其中一块回收时把所有存活对象复制到另一块空闲区域再清空原区域的全部内存。这种实现不会产生内存碎片遍历效率极高缺点是可用内存直接减半非常适配新生代“绝大多数对象朝生夕死”的特性HotSpot虚拟机中新生代就采用该算法将Eden区和两个Survivor区按8:1:1比例分配仅浪费10%的内存空间。‌3标记-整理算法‌在标记完存活对象后不直接清理垃圾而是把所有存活对象向内存的一端移动最后直接清理边界外的全部空间。既解决了内存碎片问题也不需要额外预留空闲内存非常适合老年代这种存活对象占比高的场景缺点是移动对象的过程会带来一定的性能开销。‌4分代收集算法‌5分代收集算法工作机制分代收集算法是当前主流商用JVM的核心垃圾回收实现思路它基于“不同对象的存活周期差异极大”的统计特性将堆内存按对象生命周期划分为不同区域为每个区域匹配最适配的回收算法在吞吐量和停顿时间之间实现最优平衡大幅降低了单次垃圾回收的工作量。它的完整工作机制可以拆解为以下几个核心部分5.1内存分代的核心划分HotSpot虚拟机将Java堆划分为两大核心区域针对不同特性做定向优化‌新生代年轻代‌存放绝大多数新创建的短生命周期对象98%的对象都会在这个区域“朝生夕死”。新生代进一步划分为1块Eden区和2块大小相等的Survivor区默认比例为8:1:1最大化内存利用率。‌老年代‌存放经过多轮GC后依然存活的长生命周期对象这里的对象存活率高、不会频繁被回收占据了堆内存的大部分空间。方法区原永久代/元空间作为补充回收区域仅回收废弃常量和无用类回收“性价比”较低。5.2 不同代的专属回收策略针对两个区域的对象特性分代算法匹配了完全不同的回收逻辑‌新生代使用复制算法‌利用新生代存活对象极少的特点不需要按1:1均分内存。每次回收时将Eden区和其中一块Survivor区中的所有存活对象一次性拷贝到另一块空闲的Survivor区中之后直接清空Eden和刚使用过的Survivor区。这种方式几乎不会产生内存碎片回收速度极快仅需要付出少量存活对象的复制成本。新生代eden区内存不足标记eden和from存活对象复制存活对象到to中经过一段时间eden内存又不足了标记eden和to存活对象复制到from中当幸存者对象熬过一定的回收次数默认15次晋升到老年代‌老年代使用标记-清除/标记-整理算法‌老年代对象存活率高没有额外的空闲空间做分配担保因此不再使用复制算法。要么通过标记后直接清理垃圾对象的标记-清除算法要么通过将存活对象向内存一端移动、再清理边界外空间的标记-整理算法避免了复制大量存活对象的高额开销。5.3 对象年龄晋升规则JVM为每个对象定义了“年龄”计数器实现对象在不同代之间的流转对象在Eden区首次创建第一次Minor GC后如果存活且能被Survivor区容纳就会被移动到Survivor区年龄标记为1。对象每在Survivor区中熬过一轮Minor GC年龄就1当年龄达到阈值默认15时就会被直接晋升到老年代中。针对超过-XX:PretenureSizeThreshold阈值的大对象会直接绕过新生代分配直接进入老年代避免在新生代中产生大量内存复制开销。5.4 空间分配担保机制为了保障新生代Minor GC的安全性JVM引入了分配担保逻辑在执行Minor GC前虚拟机会先检查老年代的连续可用空间只要老年代剩余空间大于新生代所有对象的总大小或者大于历次晋升到老年代对象的平均大小就可以安全执行Minor GC如果不满足条件就会直接触发Full GC提前清理老年代释放足够空间避免新生代存活对象无法安置的问题。这是当前商用JVM的标准实现思路根据对象生命周期的差异把堆内存划分为新生代和老年代新生代对象生命周期短使用复制算法实现高效回收老年代对象存活率高、没有额外空间做分配担保使用标记-清除或标记-整理算法最大化整体回收效率。三、关键机制STWStop-The-World垃圾回收的标记和清理过程中如果用户线程还在同时运行会导致对象引用关系被修改、内存数据不一致的问题。STW机制会在GC的关键阶段短暂暂停所有用户业务线程保证回收过程的准确性几乎所有垃圾回收器都无法完全避免STW只是不同实现的停顿时长差异极大。四、主流垃圾回收器JVM针对不同场景的吞吐量、延迟需求提供了多类回收器本质是在吞吐量、响应时间和系统资源之间做权衡‌1Serial串行收集器‌单线程执行GC回收过程全程暂停用户线程适合客户端应用或者资源有限的小型服务。‌2Parallel并行收集器‌使用多线程并行执行GC最大化系统吞吐量适合后台计算密集型、对停顿时间不敏感的任务场景。‌3CMS低延迟收集器‌以获取最低停顿时间为目标实现了用户线程和GC线程大部分阶段并发执行适合对响应速度要求高的业务但运行过程中容易产生内存碎片。CMSConcurrent Mark Sweep是HotSpot虚拟机专为老年代设计的低延迟垃圾回收器核心目标是尽可能缩短垃圾回收过程中的STWStop-The-World停顿时间是JDK 1.5推出的首款主打并发回收的垃圾回收器目前已在JDK 9被标记为废弃、JDK 14正式移除但其设计思路为后续G1、ZGC等低延迟回收器奠定了核心基础。CMS完整运行原理可以拆解为以下几个核心部分3.1、核心底层基础CMS完全基于‌标记-清除‌算法实现而非复制或标记整理算法这是它所有特性和缺陷的根源它不会移动、整理老年代的存活对象而是通过维护空闲列表来管理回收后的内存空间以此降低对象移动带来的性能开销支撑大部分阶段和用户线程并发运行。它通常和新生代的ParNew回收器搭配使用通过-XX:UseConcMarkSweepGC参数即可启用。3.2、四大核心运行阶段CMS将回收流程拆分仅在两个阶段触发短暂的STW其余阶段都可以和用户业务线程并行执行‌初始标记‌仅触发极短时间的STW只标记GC Roots直接关联的对象不需要遍历整个对象图执行速度极快。‌并发标记‌该阶段GC线程和用户线程同时运行从初始标记得到的根对象出发完整遍历整个老年代的对象图标记出所有存活对象。‌重新标记‌再次触发短暂STW修正并发标记阶段因为用户线程运行而产生变动的引用关系补全之前漏标记的存活对象。可以通过-XX:CMSScavengeBeforeRemark参数在该阶段前先执行一次新生代GC减少新生代对老年代的无效引用进一步缩短这个阶段的停顿时长。‌并发清除‌GC线程和用户线程并行运行直接清理所有已经被标记为死亡的对象直接释放对应的内存空间。3.3、固有特性与常见问题‌浮动垃圾问题‌并发清除阶段用户线程还在持续运行新产生的垃圾对象无法在本次回收中被清理这部分对象就被称为浮动垃圾只能留到下一次GC再处理。‌并发模式失败‌CMS不能等到老年代几乎完全占满才触发回收需要预留部分空间给并发阶段的用户线程使用默认在老年代占用率达到92%时就会主动触发回收。如果回收速度跟不上对象分配速度预留空间耗尽就会触发Concurrent Mode FailureJVM会临时切换为单线程的Serial Old回收器执行Full GC带来长时间的业务停顿。‌内存碎片问题‌因为基于标记-清除算法回收完成后老年代会产生大量不连续的内存碎片当需要分配大对象时即便老年代总剩余空间足够也可能找不到连续的内存区域被迫提前触发Full GC。CMS提供了-XX:UseCMSCompactAtFullCollection参数在Full GC时同步执行内存整理通过移动存活对象消除碎片同时可以通过-XX:CMSFullGCsBeforeCompaction设置间隔多少次不带整理的Full GC后执行一次带碎片整理的回收。‌CPU资源敏感‌并发阶段GC线程会占用部分CPU算力默认回收线程数为(CPU核心数3)/4在CPU资源紧张的场景下会直接挤占业务线程的算力拖慢整体服务的吞吐量。‌4G1分区收集器‌将整个堆内存拆分为多个大小相等的独立区域不再严格区分连续的新生代和老年代兼顾吞吐量和响应时间专门适配大内存堆场景是JDK9之后的默认回收器。‌ZGC低延迟收集器‌新一代低延迟回收器可实现亚毫秒级的STW停顿支持TB级别的超大堆内存适合对延迟极其敏感的高并发服务。五、常见GC类型‌Minor GC‌仅针对新生代执行回收因为新生代对象大多生命周期极短回收频率高、速度极快对业务影响很小。‌Full GC‌对整个堆内存新生代老年代执行完整回收执行耗时远高于Minor GC业务优化的核心目标就是尽可能减少Full GC的触发次数。