ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【后端开发|JVM基础01】—— Java垃圾回收全解:从分代内存模型到各收集器的分代职责

【后端开发|JVM基础01】—— Java垃圾回收全解:从分代内存模型到各收集器的分代职责 Java 垃圾回收全解从分代内存模型到各收集器的分代职责很多人问垃圾收集器到底是收哪一代的垃圾这个问题的坑在于它默认了每个收集器都收一整堆。实际上 JVM 把堆分成了年轻代和老年代不同收集器要么只负责其中一代、要么整堆一把抓、要么干脆不分代。搞懂这张地图比死记每个收集器的名字有用得多。本文按一条递进链讲透GC 为什么存在内存模型→ GC 在收什么对象可达性→ 用什么算法收三种回收算法→ 具体哪个收集器收哪一代核心→ 怎么搭配怎么选型。一、先厘清GC 到底在收哪一代的垃圾1.1 JVM 运行时内存分区JVM 把运行时内存按用途分了几块。跟垃圾回收直接相关的只有堆Heap其他区域要么不参与回收、要么回收规则简单区域是否参与 GC说明堆Heap✅ 主要回收对象存放对象实例GC 的主战场方法区 / 元空间Metaspace⚠️ 有回收但较弱存放类元信息、常量池JDK8 用元空间替代永久代主要回收废弃的类和无用的常量虚拟机栈 / 本地方法栈 / 程序计数器❌ 不参与 GC栈帧随方法进出栈自动创建销毁 重点平时说的垃圾回收九成九指堆的回收。1.2 堆的分代模型堆内部不是一坨整块而是按对象存活时间切成不同区域┌────────────────────────────── 堆 ──────────────────────────────┐ │ 年轻代Young │ 老年代Old │ │ ┌──────────┬─────┬─────┐ │ │ │ │ Eden │ S0 │ S1 │ │ │ │ └──────────┴─────┴─────┘ │ │ └──────────────────────────────────────────────────────────────┘ Eden新对象出生地 Survivor S0/S1从 Eden 熬过一轮 GC 的幸存者两块来回倒 Old熬过多轮 GC 的对象或者大对象直接进来年轻代Eden 两块 SurvivorS0/S1。绝大多数对象在这出生、也在这消亡。老年代存活久、晋升上来的对象以及大对象。分代回收的三种叫法Young GC新生代/Minor GC、Old GCMajor GC一般特指老年代、Full GC整堆回收。1.3 为什么要分代弱分代假说分代不是拍脑袋背后是两个经验性规律合称分代收集理论弱分代假说Weak Generational Hypothesis绝大多数对象都是朝生夕灭的用完就扔。强分代假说Strong Generational Hypothesis熬过越多次回收的对象越难以消亡。由此推导出一个设计把朝生夕灭的对象集中放在年轻代回收时只处理这一小块、重点保留少量存活对象而不是每次整堆扫描去标记大量将死的对象。这大幅降低了单次回收的成本。⚠️ 避坑分代是大多数情况下的优化不代表老年代对象不会引用年轻代对象。做年轻代回收时JVM 会借助记忆集Remembered Set来记录老年代对年轻代的引用避免整堆扫描老年代。二、回收的前提先判定哪些对象是垃圾GC 的第一步不是收而是判断谁该死。主流判断方式是可达性分析而不是简单数引用。2.1 引用计数法的缺陷早期语言用引用计数每个对象记录被引用的次数为 0 就回收。它有两个硬伤导致 JVM 不用它无法处理循环引用A 引 B、B 引 A彼此都不为 0但已无人可达。每次引用赋值都要维护计数器开销大。2.2 可达性分析与 GC RootsJVM 采用可达性分析从一组根节点GC Roots出发沿着引用链往下走能走到的对象标记为存活走不到的即为可回收垃圾。可作为 GC Roots 的对象主要包括GC Root来源虚拟机栈栈帧本地变量表中引用的对象正在执行的方法里的局部变量方法区中静态属性引用的对象类的静态变量方法区中常量引用的对象字符串常量池、字面量等本地方法栈中 JNI 引用的对象Native 方法持有的引用被同步锁synchronized持有的对象锁监视器活跃线程对象、类加载器等JVM 内部引用✅ 一句话从 GC Roots 出发摸得到的活着摸不到的该死。三、回收算法不同代为什么用不同算法判断完谁该死下一步是用什么姿势收。三大经典算法各有短板这正是哪一代用哪个算法由来的根源。3.1 标记-清除Mark-Sweep分两步标记存活对象再统一清除未被标记的对象。优点实现简单无需移动对象。缺点产生大量内存碎片导致后续分配大对象时明明有空间却放不下且标记和清除两个阶段效率都随对象数量下降。3.2 复制Copying把内存分成两块只用一块回收时把存活对象复制到另一块原块一次性清空。优点无碎片实现简单只处理存活对象存活少时极快。缺点浪费空间总有一半空闲存活对象多时复制成本高。所以复制算法天然适合年轻代——因为年轻代绝大多数对象朝生夕灭、存活率低复制成本低还能省下整理碎片的时间。JVM 把它细化为 Eden 两块 Survivor比例默认 8:1:1不是简单对半切避免一半内存闲置。3.3 标记-整理Mark-Compact标记存活对象后把它们往一端移动压实再清掉边界之外的空间。优点无碎片空间利用率高。缺点移动对象需要更新所有引用STW 成本高。标记-整理适合老年代——老年代对象存活率高、复用复制算法不划算而长期运行又受不了碎片。3.4 算法与代的对应关系算法空间浪费碎片存活率高时表现适合的代标记-清除无严重一般CMS 用靠后续整理兜底复制有无差年轻代标记-整理无无好老年代四、各收集器 分代职责核心章节 本节回答你最关心的问题每个收集器到底收哪一代。先记结论——传统收集器分三类只收年轻代、只收老年代、整堆一把抓而 G1 用 Region 打破了代边界ZGC / Shenandoah 干脆不分代。4.1 只收集【年轻代】的收集器收集器算法线程特点Serial复制单线程最简单适合单核/客户端、内存小的场景ParNew复制多线程Serial 的多线程版只能配合 CMS使用Parallel Scavenge复制多线程目标是吞吐量优先可精确控制吞吐量Serial垃圾回收时Stop The World暂停应用并单线程收集简单可靠是默认兜底。ParNew本质是 Serial 的并行版。它的存在意义很大一部分是给 CMS 当年轻代搭档——CMS 只处理老年代年轻代得有人收。Parallel Scavenge关注点在于让吞吐量最大化CPU 用于用户代码的时间占比适合后台批量计算场景。4.2 只收集【老年代】的收集器收集器算法线程特点Serial OldMSC标记-整理单线程Serial 的老年代版Parallel Old标记-整理多线程Parallel Scavenge 的搭档吞吐量优先CMS标记-清除并发并发低停顿但会产生碎片Serial Old / Parallel Old分别是 Serial / Parallel Scavenge 的老年代另一半靠标记-整理消灭碎片。CMSConcurrent Mark Sweep追求最短回收停顿——标记清除的主要阶段与应用线程并发执行不全程 STW。代价是标记-清除产生内存碎片且并发阶段占用 CPU。⚠️ 避坑CMS只收老年代不碰年轻代。所以它必须搭配一个年轻代收集器通常就是 ParNew。这也是ParNew CMS成为经典组合的原因。 归宿CMS 在 JDK9 被标记废弃JDK14JEP 363正式移除由 G1 接棒。4.3 整堆收集器G1打破代边界G1Garbage First不再把堆简单切为年轻代/老年代两块而是把整堆划分成若干固定大小的 Region。每个 Region 可以动态扮演 Eden、Survivor 或 Old 的角色逻辑上的年轻代/老年代只是 Region 集合的概念。收集目标整个堆但优先回收垃圾最多回收价值最大的 Region——这就是名字 “Garbage First” 的由来。特点可预测停顿时间通过-XX:MaxGCPauseMillis设定目标兼顾吞吐与延迟适合大堆GB 级以上。地位JDK9JEP 248起成为默认收集器至今仍是默认。✅ 一句话G1 名义上整堆收集但内部仍保留年轻代/老年代的分代回收逻辑只是边界由 Region 动态划定。4.4 不分代收集器ZGC / Shenandoah这两兄弟颠覆了分代前提——没有年轻代、老年代的概念直接整堆回收。它们追求的是极短甚至接近可忽略的 STW面向大堆、低延迟场景。收集器分代核心卖点演进关键节点ZGC最初不分代停顿时间不随堆大小增长可到毫秒级JDK11 引入实验→ JDK15 转正 →JDK21 分代化JEP 439→ JDK23 分代默认 → JDK24 移除非分代模式Shenandoah最初不分代与 ZGC 类似的低延迟RedHat 开发JDK12 引入实验→ JDK15 转正 →JDK25 分代化JEP 521 有意思的演进分代本身是一种优化。ZGC / Shenandoah 最初为了极简放弃分代但后来发现年轻对象死得快、频繁回收能省内存和 CPU于是JEP 439ZGC和 JEP 521Shenandoah又都补上了分代能力。这恰好反证了分代假说的价值。4.5 收集器总表一句话记忆收集器收集区域算法特点 / 备注Serial年轻代复制单线程最简ParNew年轻代复制多线程配合 CMSParallel Scavenge年轻代复制吞吐量优先Serial Old老年代标记-整理单线程Parallel Old老年代标记-整理吞吐量优先CMS老年代标记-清除并发低停顿JDK14 已移除G1整堆Region综合优先回收垃圾多的块JDK9 起默认ZGC整堆JDK21 后分代综合超低 STW大堆Shenandoah整堆JDK25 后分代综合低延迟五、收集器如何搭配与选型传统收集器是一代一个所以存在经典搭配组合G1 / ZGC 是整堆的不需要配。5.1 经典组合组合年轻代老年代适用场景Serial Serial OldSerialSerial Old单核、内存小、客户端ParNew CMSParNewCMS追求低延迟曾经的主流CMS 已退役Parallel Scavenge Parallel OldParallel ScavengeParallel Old吞吐量优先后台批量计算G1单收集器整堆 Region整堆 RegionJDK9 默认通用ZGC / Shenandoah单收集器不分代JDK21/25 后分代—大堆 低延迟5.2 按场景选型JDK9 视角没特殊诉求直接 G1默认。吞吐量优先能接受停顿Parallel Scavenge Parallel Old。低延迟、堆很大ZGC 或 Shenandoah。单核 / 小内存 / 调试Serial。CMS生产环境别再用它是历史概念JDK14 起已不存在。5.3 JDK 版本关键变化容易考的演进史版本变化依据JDK 9G1 成为默认收集器JEP 248JDK 11ZGC 引入实验JEP 333JDK 12Shenandoah 引入实验JEP 189JDK 14CMS 被移除JEP 363JDK 15ZGC / Shenandoah 转生产级JEP 377 / JEP 379JDK 21分代 ZGCGenerational ZGCJEP 439JDK 23ZGC 分代模式成为默认JEP 474JDK 24ZGC 移除非分代模式JEP 490JDK 25分代 ShenandoahJEP 521⚠️ 注意上面是较新 JDK 的走向。如果你还在用 JDK8默认仍是 Parallel也没有 ZGC / ShenandoahJDK8 无官方版本升级前要先想清楚收集器的迁移。六、总结你真正需要记住的 N 件事GC 的主战场是堆堆分年轻代Eden 两块 Survivor和老年代。分代的依据是弱分代假说大多数对象朝生夕灭集中回收年轻代效率最高。判定垃圾用可达性分析从 GC Roots 出发摸得到的存活摸不到的回收不用引用计数解决不了循环引用。三种算法与代对应年轻代用复制无碎片、存活少成本低老年代用标记-整理无碎片、能扛高存活标记-清除有碎片CMS 的代价。只收年轻代的收集器Serial、ParNew、Parallel Scavenge。只收老年代的收集器Serial Old、Parallel Old、CMSCMS 需配 ParNew 收年轻代。G1 整堆收集但内部仍分代靠 Region 动态扮演不同代JDK9 起是默认。ZGC / Shenandoah 最初不分代但 ZGC 在 JDK21、Shenandoah 在 JDK25 都补上了分代——反证分代假说的价值。选型默认 G1吞吐优先用 Parallel 双开大堆低延迟用 ZGC / ShenandoahCMS 已是历史。验证清单能不看表说出每个收集器收哪一代能解释为什么年轻代用复制、老年代用标记-整理能说出 CMS 为什么必须搭配 ParNew能讲清 G1 与分代的关系Region 动态扮演代能说清 ZGC 从不分代到分代的演进JEP 439/474/490能在面试中被问到默认收集器时答出G1JDK9参考资源Oracle 官方 GC 调优指南HotSpot Virtual Machine Garbage Collection Tuning GuideOracle《Migrating from JDK 8 to Later JDK Releases》G1 默认 / JDK 迁移变化JEP 439: Generational ZGCOpenJDKJEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage CollectorJEP 248: Make G1 the Default Garbage CollectorJEP 474: ZGC: Generational Mode by Default / JEP 490: ZGC: Remove the Non-Generational ModeJEP 521: Generational Shenandoah《深入理解 Java 虚拟机》周志明——分代收集理论、可达性分析、GC Roots 权威参考
返回列表