JVM学习笔记(4) 第二部分 自动内存管理 第3章 垃圾收集器与分配策略

文章目录

  • 第3章:垃圾收集器与内存分配策略
    • [3.0 个人感悟](#3.0 个人感悟)
    • [3.1 概述](#3.1 概述)
    • [3.2. 对象已死?------ 对象存活判定算法](#3.2. 对象已死?—— 对象存活判定算法)
      • [3.2.1 引用计数法](#3.2.1 引用计数法)
      • [3.2.2 可达性分析算法](#3.2.2 可达性分析算法)
      • [3.2.3 引用类型(强、软、弱、虚)](#3.2.3 引用类型(强、软、弱、虚))
      • [3.2.4 finalize() 方法(了解即可,不推荐使用)](#3.2.4 finalize() 方法(了解即可,不推荐使用))
    • [3.3. 垃圾收集算法](#3.3. 垃圾收集算法)
      • [3.3.1 分代收集理论(理论基础)](#3.3.1 分代收集理论(理论基础))
      • [3.3.2 标记-清除算法](#3.3.2 标记-清除算法)
      • [3.3.3 标记-复制算法(新生代主流算法)](#3.3.3 标记-复制算法(新生代主流算法))
      • [3.3.4 标记-整理算法(老年代主流算法)](#3.3.4 标记-整理算法(老年代主流算法))
    • [3.4. HotSpot 的算法实现细节](#3.4. HotSpot 的算法实现细节)
      • [3.4.1 根节点枚举](#3.4.1 根节点枚举)
      • [3.4.2 安全点(Safepoint)](#3.4.2 安全点(Safepoint))
      • [3.4.3 安全区域(Safe Region)](#3.4.3 安全区域(Safe Region))
      • [3.4.4 记忆集与卡表](#3.4.4 记忆集与卡表)
      • [3.4.5 写屏障](#3.4.5 写屏障)
      • [3.4.6 并发的可达性分析](#3.4.6 并发的可达性分析)
    • [3.5 经典垃圾收集器](#3.5 经典垃圾收集器)
      • [3.5.1 Serial 与 Serial Old](#3.5.1 Serial 与 Serial Old)
      • [3.5.2 ParNew](#3.5.2 ParNew)
      • [3.5.3 Parallel Scavenge 与 Parallel Old](#3.5.3 Parallel Scavenge 与 Parallel Old)
      • [3.5.4 Concurrent Mark Sweep (CMS)](#3.5.4 Concurrent Mark Sweep (CMS))
      • [3.5.5 Garbage First (G1)](#3.5.5 Garbage First (G1))
    • [3.6 低延迟垃圾收集器](#3.6 低延迟垃圾收集器)
      • [3.6.1 Shenandoah收集器](#3.6.1 Shenandoah收集器)
      • [3.6.2 ZGC收集器](#3.6.2 ZGC收集器)
    • [3.7 选择合适的垃圾收集器](#3.7 选择合适的垃圾收集器)
      • [3.7.1 Epsilon收集器](#3.7.1 Epsilon收集器)
      • [3.7.2 收集器的权衡与选择](#3.7.2 收集器的权衡与选择)
      • [3.7.3 虚拟机及垃圾收集器日志](#3.7.3 虚拟机及垃圾收集器日志)
      • [3.7.4 垃圾收集器参数总结](#3.7.4 垃圾收集器参数总结)
    • [3.8 实战内存分配与回收策略](#3.8 实战内存分配与回收策略)
      • [3.8.1 对象优先在 Eden 分配](#3.8.1 对象优先在 Eden 分配)
      • [3.8.2 大对象直接进入老年代](#3.8.2 大对象直接进入老年代)
      • [3.8.3 长期存活的对象将进入老年代](#3.8.3 长期存活的对象将进入老年代)
      • [3.8.4 动态对象年龄判定](#3.8.4 动态对象年龄判定)
      • [3.8.5 空间分配担保](#3.8.5 空间分配担保)

第3章:垃圾收集器与内存分配策略

3.0 个人感悟

  • 这章的知识点细节比较多。建议可以围绕垃圾收集关注的3个核心问题来记忆。很多技术、名称的出现是为了解决某些问题。
    • 可达性分析算法是为了判断对象存活,确定哪些内存需要回收。为了提升效率,引入了ooMap、安全点、安全区域概念
    • 回收算法回答了什么时候回收、如何回收
    • 对象跨代引用场景,为避免扫描整个老年代GC Roots,引入记忆集。卡表是它的一种实现。写屏障可以理解为JVM中的AOP,用于维护卡表信息
    • 并发可达会引发误标和漏标场景,类似数据库的脏、幻读场景。漏标场景(本来应该存活的对象给清理了)有两个必要条件,破坏任意条件即刻,产生2种解决方案:增加更新、原始快照
  • GC 的本质是"分治":根据对象生命周期,将堆分为新生代(复制算法,牺牲空间换时间)和老年代(标记-整理,减少停顿或碎片)
  • 很多事情不是追求完美而是均衡:性能与吞吐量的博弈,CMS/G1 追求低延迟(STW 短),但牺牲了吞吐量或内存占用;Parallel Scavenge 追求高吞吐量,适合后台计算。
  • G1 的演进意义:G1 通过 Region 化打破了分代物理隔离,实现了可预测的停顿模型,是未来垃圾收集器发展的里程碑(为 ZGC 等奠定基础)。

3.1 概述

垃圾收集关注的核心问题:

  • 哪些内存需要回收 -- 对象存活判定算法
  • 什么时候回收 -- 收集器的机制和配置项
  • 如何回收 -- 垃圾收集算法

重点关注区域:

  • Java堆和方法区 具有动态性,运行期确定,重点关注这部分内存的管理。
  • 虚拟机栈、本地方法栈、程序计数器,随线程生灭,内存分配和回收具有确定下,不需要太多关注。

3.2. 对象已死?------ 对象存活判定算法

3.2.1 引用计数法

原理

给对象添加一个引用计数器,每当有一个地方引用它,计数器+1;引用失效时,计数器-1。计数器为0的对象可被回收。

优点

实现简单,判定效率高。

致命缺陷

无法解决对象之间循环引用的问题。例如 A 引用 B,B 引用 A,但外部无引用,两者计数器不为0,实际却已无用。hotSpot虚拟机 并未采用此算法。

3.2.2 可达性分析算法

原理:通过一系列称为 "GC Roots" 的根对象作为起始节点集,向下搜索。搜索走过的路径称为 "引用链"。若某对象到 GC Roots 无任何引用链相连(即不可达),则证明该对象可被回收。

可作为 GC Roots 的对象包括

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中静态属性引用的对象(如 static 变量)。
  3. 方法区中常量引用的对象(如 final 常量)。
  4. 本地方法栈中 JNI(Native方法)引用的对象。
  5. Java 虚拟机内部的引用(基本数据类型对应的 Class 对象、常驻的异常对象、系统类加载器等)。
  6. 所有被同步锁持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

3.2.3 引用类型(强、软、弱、虚)

为了更灵活地控制对象的生命周期,Java 提供了四种引用类型:

  1. 强引用:引用赋值,如Object obj = new Object()。只要强引用存在,垃圾收集器永远不会回收。
  2. 软引用:oftReference。在系统将要发生内存溢出之前,会把这些对象列入回收范围进行第二次回收。适合做缓存。
  3. 弱引用:WeakReference。被弱引用关联的对象只能生存到下一次垃圾收集发生为止。无论内存是否足够,都会被回收。
  4. 虚引用:PhantomReference。无法通过虚引用获取对象实例,唯一目的是在这个对象被回收时收到一个系统通知。

3.2.4 finalize() 方法(了解即可,不推荐使用)

  • 对象被判定为不可达后,会被标记并筛选。若对象需要执行 finalize() 方法且尚未执行,则会放入 F-Queue 队列中,由一条低优先级的 Finalizer 线程去执行。
  • 注意 :如果在 finalize() 中重新与引用链上的对象建立关联(如把自己赋值给某个类变量或成员变量),则对象会"逃脱"回收。但官方已明确声明 finalize() 方法已被弃用,不建议使用。

3.3. 垃圾收集算法

3.3.1 分代收集理论(理论基础)

核心假设

  1. 弱分代假说:Weak Generational Hypothesis。绝大多数对象都是朝生夕灭的。
  2. 强分代假说:Strong Generational Hypotheis。熬过越多次垃圾收集过程的对象就越难以消亡。

设计原则

虚拟机将堆划分为新生代和老年代,根据各自特点使用不同的收集算法。

3.3.2 标记-清除算法

过程

先标记所有需要回收的对象,标记完成后统一回收。

示意图:

缺点

  1. 执行效率不稳定(堆中大量对象时,标记和清除效率随对象数量增长而降低)。
  2. 内存碎片问题(标记清除后产生大量不连续内存碎片,导致大对象无法分配而提前触发 GC)。

3.3.3 标记-复制算法(新生代主流算法)

原理

将内存分为大小相等的两块,每次只使用其中一块。当这块用完,将存活对象复制到另一块,再把使用过的空间一次清理掉。

示意图

优点:实现简单,运行高效,无碎片问题。

缺点:内存利用率低(可用内存缩小为一半)。

实际应用:HotSpot 虚拟机将新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间(默认比例 8:1:1)。每次使用 Eden 和其中一块 Survivor。回收时将存活对象复制到另一块 Survivor 中。若 Survivor 空间不够,需依赖老年代进行 分配担保。

3.3.4 标记-整理算法(老年代主流算法)

原理

标记过程与"标记-清除"一样,但后续步骤不是直接清理,而是让所有存活对象向内存空间一端移动,然后直接清理掉边界以外的内存。

示意图 :

优点:解决了内存碎片问题。

缺点:移动对象需要更新引用,存在开销(Stop The World 时间较长)。

3.4. HotSpot 的算法实现细节

3.4.1 根节点枚举

Stop The World

根节点枚举必须在一个能确保一致性的快照中进行,因此必须暂停所有用户线程。

OopMap :(提升枚举效率的数据结构)

HotSpot 在特定位置(如安全点)记录了栈和寄存器中哪些位置是引用,无需遍历全栈,加快了根节点枚举速度。

3.4.2 安全点(Safepoint)

定义

程序执行时并非在所有地方都能停顿下来进行 GC,只有在到达安全点时才能暂停。

作用 :

提升枚举效率。如果每个指令都生成oopMap,代价会非常昂贵。只在安全点生成,有利于提高效率。

选点标准:以"是否具有让程序长时间执行的特征"为标准(如方法调用、循环跳转、异常跳转等)。

如何让线程跑到安全点

  • 抢先式中断(几乎不用)。当发生垃圾收集事,系统把所有用户线程全中断,如果有不在安全点上的,就回复这个线程,让其跑到安全点。
  • 主动式中断(设置标志位,线程轮询标志)

3.4.3 安全区域(Safe Region)

定义

当线程处于 Sleep 或 Blocked 状态时,无法响应 JVM 的中断请求。安全区域指在一段代码片段中,引用关系不会发生变化,在此区域任何位置开始 GC 都是安全的。

机制

线程进入安全区域时标记;离开时检查是否完成了根节点枚举,若未完成则等待。

3.4.4 记忆集与卡表

定义

跨代引用(老年代对象引用新生代对象)场景。为了避免遍历整个老年代来寻找跨代引用,引入了记忆集的数据结构。

卡表:是记忆集的一种实现方式。将内存划分为 512 字节的卡页。若卡页内存在跨代引用,则将卡表对应元素标记为"脏"。

3.4.5 写屏障

用于维护卡表状态的机制。在赋值操作(更新引用)的字节码指令前后插入指令,更新卡表标记。

以下是《深入理解Java虚拟机第三版》第3章 3.4.6 并发的可达性分析 的内容总结。该节是理解 CMS、G1 等并发收集器工作原理的核心。

3.4.6 并发的可达性分析

问题 :

可达性分析(根节点枚举 + 对象图遍历)理论上要求整个分析期间冻结用户线程(Stop The World),以保证引用关系不变。但这样会带来长时间停顿。为此,收集器希望在遍历对象图阶段(即标记阶段)与用户线程并发执行。然而,并发执行会导致对象引用关系在标记过程中发生变化,可能出现两种致命后果:

  • 误标(浮动垃圾):将本应回收的对象标记为存活,后果可以容忍(下次再回收)。
  • 漏标:将本应存活的对象标记为可回收,导致实际存活的对象被错误回收,这是必须避免的。

示意图

漏标的充要条件 :

当且仅当以下两个条件同时满足时,会产生漏标:

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用(即黑色对象原本没指向白色对象,现在指向了它)。
  2. 赋值器删除了所有从灰色对象到该白色对象的直接或间接引用(即灰色对象原本可以到达白色对象,现在引用被移除了)。

解决方案 :

只要破坏其中任意一个条件,就能避免漏标。基于此产生了两种并发标记的解决方案:

  • 增量更新(Incremental Update)
  • 原始快照(Snapshot At The Beginning, SATB

增量更新(Incremental Update):

  • 思路:破坏第一个条件。当黑色对象新插入了指向白色对象的引用时,将这个新插入的引用记录下来,等并发标记结束后,再以这些黑色对象为根重新扫描一次。
  • 实现 :利用写屏障,在赋值操作(引用写入)时记录这种新增的跨代/跨色引用。
  • 代表收集器:CMS 采用增量更新。

原始快照(SATB):

  • 思路:破坏第二个条件。当灰色对象要删除指向白色对象的引用时,将这个要删除的引用记录下来,在并发扫描结束后,以这些灰色对象为根重新扫描一次。相当于在开始并发标记时拍了一张对象图的"快照",标记过程基于这张快照进行,无论后续引用如何变化,原本快照中存活的对象在本次收集中都会被视为存活。
  • 实现:同样通过写屏障,在引用赋值时记录被删除的引用(或者记录旧的引用值)。
  • 代表收集器:G1 采用 SATB。

3.5 经典垃圾收集器

3.5.1 Serial 与 Serial Old

Serial(新生代):单线程,采用复制算法。进行 GC 时必须暂停所有用户线程(Stop The World)。

Serial Old(老年代):单线程,采用标记-整理算法。

适用:客户端模式下的虚拟机。

3.5.2 ParNew

实质*:Serial 的多线程版本,同样使用复制算法。是许多运行在服务端模式下的虚拟机首选的新生代收集器(唯一能与 CMS 配合工作的)。

3.5.3 Parallel Scavenge 与 Parallel Old

关注点吞吐量(CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值)。适合后台计算不需要太多交互的任务。

Parallel Scavenge(新生代):复制算法,多线程。可设置 -XX:MaxGCPauseMillis(最大停顿时间)和 -XX:GCTimeRatio(吞吐量大小)。

Parallel Old(老年代):多线程,标记-整理算法。

3.5.4 Concurrent Mark Sweep (CMS)

目标:获取最短回收停顿时间,适用于互联网或 B/S 系统。

运作过程

  1. 初始标记:标记 GC Roots 能直接关联到的对象。需要 STW(Stop The World),但速度很快。
  2. 并发标记:从 GC Roots 直接关联对象开始遍历整个对象图。用户线程与 GC 线程并发执行,耗时较长但不停顿。
  3. 重新标记:修正并发标记期间因用户程序继续运行导致标记产生变动的记录。需要 STW,时间比初始标记长但比并发标记短。
  4. 并发清除:清除标记阶段判断为死亡的对象。并发执行。

缺点

  1. 对 CPU 资源敏感:占用一部分线程导致应用变慢。
  2. 无法处理浮动垃圾 :并发清理时用户线程产生的新垃圾只能下次清理。可能因预留空间不足导致 "Concurrent Mode Failure",触发 Serial Old 降级,停顿时间变长。
  3. 内存碎片:标记-清除算法导致,参数 -XX:UseCMSCompactAtFullCollection(默认开启)用于在 Full GC 时进行碎片整理。

3.5.5 Garbage First (G1)

** 设计目标 :在延迟可控的情况下获得尽可能高的吞吐量,取代 CMS。
特点

  1. Region 布局:将堆划分为多个大小相等的独立区域(Region),不再有物理上的新生代/老年代区别,但逻辑上仍保留分代概念。
  2. 回收粒度:优先处理回收价值最大的 Region(垃圾最多)。
  3. 停顿模型:用户可以指定期望的停顿时间(如 100ms),G1 会基于模型选择回收哪几个 Region。

运作过程

  1. 初始标记(STW):标记 GC Roots。
  2. 并发标记:可达性分析。
  3. 最终标记(STW):处理 SATB(Snapshot-At-The-Beginning) 缓冲区中的遗留记录。
  4. 筛选回收(STW):对 Region 回收价值排序,根据用户期望停顿时间制定回收计划,将存活对象复制到空 Region,清理旧 Region。

G1 vs CMS

  • G1 不会产生内存碎片。
  • G1 停顿时间可控且可预测。
  • G1 内存占用较高(维护卡表等元数据),执行负载较高。

以下是《深入理解Java虚拟机第三版》第3章 3.6 低延迟垃圾收集器3.7 选择合适的垃圾收集器 的内容总结。

3.6 低延迟垃圾收集器

衡量垃圾收集器的三个核心指标:
内存占用吞吐量
延迟**(停顿时间)。

随着硬件发展,内存越来越大,应用对响应时间的要求也越来越高。传统的垃圾收集器(如CMS、G1)虽然在一定程度上控制了停顿,但在大堆内存场景下(如几十GB甚至上百GB),仍可能出现较长的Stop-The-World停顿。

低延迟垃圾收集器的目标

在几乎不停止用户线程的前提下完成垃圾收集,将停顿时间控制在10ms甚至几毫秒以内,且停顿时间不随堆内存大小增长而增加。

3.6.1 Shenandoah收集器

开发者

Red Hat公司(后被Oracle收购)

首次发布

JDK 12(作为实验特性)

定位

低延迟、与堆大小无关的停顿时间

核心原理 :

Shenandoah在G1的Region布局基础上,引入了并发压缩(Concurrent Compaction) 能力。与G1只在筛选回收阶段(需要STW)进行对象复制不同,Shenandoah的绝大多数操作都是并发的,包括:

  • 并发标记(与G1类似,使用SATB)
  • 并发整理 :通过布鲁克斯指针(Brooks Pointer)转发指针(Forwarding Pointer) 技术,在对象头中添加一个指针字段,指向对象当前的实际地址。当对象被移动时,只需修改这个转发指针,而不需要更新所有指向该对象的引用。用户线程通过转发指针"自动跳转"到新地址。

运行过程 :

Shenandoah的收集过程包含9个阶段,核心阶段包括:

  1. 初始标记(STW,极短)
  2. 并发标记(并发)
  3. 最终标记(STW)
  4. 并发清理(并发)
  5. 并发回收并发复制存活对象,这是与G1最大的区别)
  6. 并发重定位(并发更新引用)

优点

极低的停顿时间(与堆大小无关)

无内存碎片(压缩整理)

停顿时间可预测

缺点

吞吐量相对较低(并发操作占用CPU)

内存占用较高(Brooks指针额外开销)

性能在JDK 12初期版本中仍有优化空间

3.6.2 ZGC收集器

开发者

Oracle

首次发布

JDK 11(实验特性),JDK 15正式上线

定位

低延迟、可扩展至数TB级别的堆内存

核心原理

ZGC在JDK 11中采用了基于染色指针(Colored Pointers)和读屏障(Load Barrier) 的技术,JDK 14之后引入多映射内存(Multi-Mapped Memory 优化。

  • 染色指针 :将64位指针的高位(前4位)用于存储标志位,标记对象的状态(如"已标记"、"将要重定位"等)。这使得ZGC无需在对象头中存储GC信息,也无需维护额外的数据结构。
  • 读屏障:在应用程序读取对象引用的指令前插入检查逻辑。如果指针被染色标记为"需要修正",读屏障会介入完成引用修正(如将指针从旧地址指向新地址),避免用户线程访问到已移动的对象。
  • 多重映射:将同一块物理内存映射到多个虚拟地址空间,利用虚拟地址的染色位来表示对象状态,实现高效的并发操作。

运行过程 :

ZGC的运作也以并发为主,核心阶段包括:

  1. 并发标记(并发)
  2. 并发预备重分配(并发)
  3. 并发重分配(并发,将存活对象复制到新Region)
  4. 并发重映射(并发,通过读屏障"自愈"式地修正引用,而非主动扫描)

优点

极低停顿时间(通常<1ms)

停顿时间与堆大小无关

支持超大堆(TB级)

无内存碎片

缺点 :

内存占用较高(染色指针和多映射占用虚拟地址空间)

不支持分代(JDK 21之前),可能影响吞吐量

读屏障增加额外开销

对操作系统和CPU架构有一定要求

ZGC的分代支持

  • JDK 21开始,ZGC引入了分代ZGC,将堆划分为新生代和老年代,进一步提升吞吐量。
  • 分代ZGC在保持低延迟的同时,减少了不必要的全局扫描。

3.7 选择合适的垃圾收集器

3.7.1 Epsilon收集器

开发者

Red Hat

首次发布

JDK 11(实验特性)

定位
"不做任何垃圾收集" 的收集器

特点:

  • 不回收任何对象:只负责内存分配,不执行任何垃圾回收操作。
  • 适用场景:短生命周期应用(如一次性任务)、性能测试(测量无GC影响下的纯代码性能)、极度内存敏感且可容忍内存耗尽退出的场景。

使用注意 :

当堆内存耗尽时,Epsilon会直接导致JVM进程退出(OutOfMemoryError),需要业务具备快速重启或容错能力。

3.7.2 收集器的权衡与选择

选择垃圾收集器需要综合考虑以下因素:
批处理、离线计算、后台任务

  • 高吞吐量
  • Parallel Scavenge + Parallel Old
  • 吞吐量优先,允许较长停顿

Web服务、API网关、实时交互

  • 低延迟
  • G1、ZGC、Shenandoah
  • 控制停顿时间在几十毫秒内

超大堆内存(>64GB)且低延迟

  • 低延迟 + 大堆
  • ZGC、Shenandoah
  • 停顿时间与堆大小无关

桌面应用、客户端、资源受限

  • 低内存占用 + 低延迟
  • Serial、Serial Old
  • 简单,资源占用低

短生命周期任务、性能测试

  • 无GC干扰
  • Epsilon
  • 不回收对象,测试专用

JDK版本与默认收集器

  • JDK 8:Parallel Scavenge + Parallel Old
  • JDK 9 - JDK 14: G1
  • JDK 15+:G1(ZGC需手动启用)

3.7.3 虚拟机及垃圾收集器日志

日志作用

  • GC日志是分析内存问题、调优收集器性能的核心工具。
  • JDK 9之后引入了统一日志管理框架(Unified Logging) ,使用 -Xlog 参数配置。

常用GC日志参数

  • -Xlog:gc开启基础GC日志
  • -Xlog:gc*开启所有GC相关日志
  • -Xlog:gc+heap=debug 查看堆内存变化详情
  • -Xlog:gc+phases=trace 查看GC各阶段耗时
  • -Xlog:gc+region=trace 查看G1/ZGC的Region信息
  • -Xlog:gc*=info:file=gc.log:time,uptime 输出到文件,含时间戳

日志解读示例

复制代码
[0.123s][info][gc] GC(0) Pause Young (G1 Evacuation Pause) 512M->256M 1024M 15.2ms
  • GC(0):第0次GC
  • Pause Young (G1 Evacuation Pause):新生代暂停,G1疏散暂停
  • 512M->256M:GC前后堆内存使用量
  • 1024M:当前堆总容量
  • 15.2ms:GC停顿时间

3.7.4 垃圾收集器参数总结

常用参数:

  • -XX:+UseSerialGC 使用Serial + Serial Old
  • -XX:+UseParNewGC 使用ParNew + Serial Old
  • -XX:+UseParallelGC 使用Parallel Scavenge + Parallel Old 吞吐量优先
  • -XX:+UseConcMarkSweepGC 使用CMS + ParNew 延迟敏感
  • -XX:+UseG1GC 使用G1 JDK 9+默认
  • -XX:+UseZGC 使用ZGC(需实验性开启) 超大堆低延迟
  • -XX:MaxGCPauseMillis=<N> 设置最大GC停顿目标 G1、Parallel
  • -XX:GCTimeRatio=<N> 设置吞吐量目标(1/(1+N))
  • -XX:InitialHeapSize / -Xms 初始堆大小
  • -XX:MaxHeapSize / -Xmx 最大堆大小
  • -Xlog:gc* GC日志(JDK 9+)Parallel
  • ...

3.8 实战内存分配与回收策略

我这里验证了下第一点,其它项大家有兴趣可以试试

3.8.1 对象优先在 Eden 分配

原理:

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间时,虚拟机发起一次 Minor GC。

验证思路 :

使用SerialGC

总堆20M 新生代10M(eden 8M survivor0 1M suvivor1 1M) 老年代10

先放置一个3M对象,会在eden

然后放置一个6M对象,这时eden空间不足,触发minor GC

3M对象无法放到suvivor,通过担保机制放到老年代

6M对象放到Eden

代码

JVM参数

复制代码
-XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -Xlog:gc* -XX:SurvivorRatio=8

解释

复制代码
-XX:+UseSerialGC  显式指定使用 Serial 垃圾收集器
-verbose:gc 在控制台输出 C 简要日志
-Xms20M 设置 Java 堆的初始内存大小 为 20MB
-Xmx20M 设置 Java 堆的最大内存大小 为 20MB
-Xmn10M 置 新生代(Young Generation)的大小 为 10MB
-Xlog:gc* **详细的 GC 日志**
-XX:SurvivorRatio=8 设置 Eden 区与 Survivor 区的大小比例 为 8

测试代码

java 复制代码
/**  
 * @Description 验证SerialGC  
 * @Author bigHao  
 * @Date 2026/3/25  
 */
 public class GcTest {  
    private static final int ONE_MB = 1024 * 1024;  
  
    static void main() {  
        // JVM参数 -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -Xlog:gc* -XX:SurvivorRatio=8  
        byte[] allocation1, allocation2;  
        allocation1 = new byte[3 * ONE_MB];  
        allocation2 = new byte[6 * ONE_MB];  
    }  
}

结果

复制代码
D:\devtool\java\jdk21\bin\java.exe -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 --enable-preview "-javaagent:D:\devtool\idea\IntelliJ IDEA 2025.2.1\lib\idea_rt.jar=55702" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath D:\code\com.dawn\out\production\com.dawn jvm.chapter2.GcTest
[0.002s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[0.006s][info   ][gc,init] CardTable entry size: 512
[0.006s][info   ][gc     ] Using Serial
[0.006s][info   ][gc,init] Version: 21.0.8+12-LTS-250 (release)
[0.006s][info   ][gc,init] CPUs: 20 total, 20 available
[0.006s][info   ][gc,init] Memory: 64869M
[0.006s][info   ][gc,init] Large Page Support: Disabled
[0.006s][info   ][gc,init] NUMA Support: Disabled
[0.006s][info   ][gc,init] Compressed Oops: Enabled (32-bit)
[0.006s][info   ][gc,init] Heap Min Capacity: 20M
[0.006s][info   ][gc,init] Heap Initial Capacity: 20M
[0.006s][info   ][gc,init] Heap Max Capacity: 20M
[0.006s][info   ][gc,init] Pre-touch: Disabled
[0.012s][info   ][gc,metaspace] CDS archive(s) mapped at: [0x000002c75a000000-0x000002c75ac90000-0x000002c75ac90000), size 13172736, SharedBaseAddress: 0x000002c75a000000, ArchiveRelocationMode: 1.
[0.012s][info   ][gc,metaspace] Compressed class space mapped at: 0x000002c75b000000-0x000002c79b000000, reserved size: 1073741824
[0.012s][info   ][gc,metaspace] Narrow klass base: 0x000002c75a000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
[0.045s][info   ][gc,start    ] GC(0) Pause Young (Allocation Failure)
[0.047s][info   ][gc,heap     ] GC(0) DefNew: 5915K(9216K)->886K(9216K) Eden: 5915K(8192K)->0K(8192K) From: 0K(1024K)->886K(1024K)
[0.047s][info   ][gc,heap     ] GC(0) Tenured: 0K(10240K)->3072K(10240K)
[0.047s][info   ][gc,metaspace] GC(0) Metaspace: 589K(768K)->589K(768K) NonClass: 551K(640K)->551K(640K) Class: 38K(128K)->38K(128K)
[0.047s][info   ][gc          ] GC(0) Pause Young (Allocation Failure) 5M->3M(19M) 1.334ms
[0.047s][info   ][gc,cpu      ] GC(0) User=0.00s Sys=0.00s Real=0.00s
[0.047s][info   ][gc,heap,exit] Heap
[0.047s][info   ][gc,heap,exit]  def new generation   total 9216K, used 7373K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
[0.047s][info   ][gc,heap,exit]   eden space 8192K,  79% used [0x00000000fec00000, 0x00000000ff2559f0, 0x00000000ff400000)
[0.047s][info   ][gc,heap,exit]   from space 1024K,  86% used [0x00000000ff500000, 0x00000000ff5ddb58, 0x00000000ff600000)
[0.047s][info   ][gc,heap,exit]   to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
[0.047s][info   ][gc,heap,exit]  tenured generation   total 10240K, used 3072K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
[0.047s][info   ][gc,heap,exit]    the space 10240K,  30% used [0x00000000ff600000, 0x00000000ff900010, 0x00000000ff900200, 0x0000000100000000)
[0.047s][info   ][gc,heap,exit]  Metaspace       used 646K, committed 832K, reserved 1114112K
[0.047s][info   ][gc,heap,exit]   class space    used 40K, committed 128K, reserved 1048576K

Process finished with exit code 0

分析:

从日志来看,运行流程是符合预期的,但数据上有些出入

可以看到,发生了GC,3M的对象放入了老年代

新生代不知为何有2M左右的数据被回收了,网上查了下没找到确切结论,可能是JDK21的收集器做了什么优化。先放一放 不纠结。

复制代码
[0.045s][info   ][gc,start    ] GC(0) Pause Young (Allocation Failure)
[0.047s][info   ][gc,heap     ] GC(0) DefNew: 5915K(9216K)->886K(9216K) Eden: 5915K(8192K)->0K(8192K) From: 0K(1024K)->886K(1024K)
[0.047s][info   ][gc,heap     ] GC(0) Tenured: 0K(10240K)->3072K(10240K)

3.8.2 大对象直接进入老年代

大对象:需要大量连续内存空间的 Java 对象,如很长的字符串或数组。

参数:-XX:PretenureSizeThreshold(仅对 Serial 和 ParNew 有效)。大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间产生大量内存复制。

3.8.3 长期存活的对象将进入老年代

对象在 Survivor 区每熬过一次 Minor GC,年龄就增加 1 岁。当年龄达到阈值(默认 15)时,晋升到老年代。

参数:-XX:MaxTenuringThreshold。

3.8.4 动态对象年龄判定

如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到阈值。

3.8.5 空间分配担保

在发生 Minor GC 之前,虚拟机检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

  • 如果大于,则 Minor GC 安全。
  • 如果小于,则检查 -XX:HandlePromotionFailure 参数是否允许担保失败。
  • 若允许,检查老年代连续空间是否大于历次晋升到老年代对象的平均大小,若大于则尝试 Minor GC(有风险),否则改为 Full GC
相关推荐
LuminousCPP2 小时前
3 道结构体 + 位段高频错题全拆解(附表格详解)
经验分享·笔记·结构体·位段
VelinX2 小时前
【个人学习||ai提示词工程】01Prompt Engineering
学习
一定要AK2 小时前
Java流程控制
java·开发语言·笔记
chase。2 小时前
【学习笔记】基于扩散模型的运动规划学习与适应
人工智能·笔记·学习
xiaokangzhe3 小时前
MySQL主从复制读写分离笔记
笔记·mysql·adb
321.。3 小时前
Linux 进程控制深度解析:从创建到替换的完整指南
linux·开发语言·c++·学习
CheerWWW3 小时前
C++学习笔记——枚举、继承、虚函数、可见性
c++·笔记·学习
Heartache boy4 小时前
野火STM32_HAL库版课程笔记-TIM通道捕获应用之编码器模式
笔记·stm32·单片机·嵌入式硬件
老虎06274 小时前
LeetCode热题100 刷题笔记(第四天)二分 「 寻找两个正序数组的中位数」
笔记·算法·leetcode