当前位置: 首页 > news >正文

JVM 垃圾收集器

       在 Java 开发中,自动内存管理是其显著优势之一,而这一切的核心就是 JVM 的垃圾收集机制。垃圾收集(Garbage Collection,GC)负责自动识别并回收不再使用的对象,释放内存资源,让开发者无需手动处理内存分配与释放,极大地提高了开发效率并减少了内存泄漏风险。

一、判断对象是否存活的方法

在进行垃圾回收前,JVM 必须先判断哪些对象仍然存活,哪些已经死亡(即不再被任何存活对象引用)。目前主要有两种判断方法:

1. 引用计数法(Reference Counting)

这是一种早期的实现方式,其核心思想是:

  • 每个对象都包含一个引用计数器
  • 当对象被新的引用指向时,计数器加 1
  • 当引用失效时(如超出作用域或被赋值为 null),计数器减 1
  • 当计数器的值为 0 时,认为该对象可以被回收

优点

  • 实现简单,判断效率高
  • 回收过程可以即时进行,无需等到内存不足时

缺点

  • 无法解决循环引用问题:如果两个对象相互引用,但都不再被其他对象引用,它们的计数器永远不会为 0,导致内存泄漏
  • 每次引用的创建和销毁都需要更新计数器,带来额外的性能开销

由于循环引用问题难以解决,主流 JVM(如 HotSpot)并未采用这种方法。

2. 可达性分析算法(Reachability Analysis)

这是目前 Java、C# 等主流语言采用的对象存活判断方法,其核心思想是:

  • 以一系列称为 "GC Roots" 的对象为起点,向下搜索引用链
  • 如果一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到该对象不可达),则认为该对象可以被回收

可作为 GC Roots 的对象包括

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象(如静态变量)
  • 方法区中常量引用的对象(如字符串常量池中的引用)
  • 本地方法栈中 JNI(即 Native 方法)引用的对象
  • JVM 内部的引用(如基本数据类型对应的 Class 对象,异常对象 NullPointerException 等)
  • 被同步锁(synchronized 关键字)持有的对象

优点

  • 能有效解决循环引用问题
  • 更符合实际的对象引用关系

缺点

  • 实现复杂,需要遍历整个引用链
  • 需要暂停应用线程以确保分析结果的准确性(即 Stop The World)

可达性分析算法是现代 JVM 实现的基础,后续的垃圾收集器优化也多围绕如何减少这种分析带来的停顿时间。

二、Java 中的四种引用类型

Java 从 1.2 版本开始,将对象的引用分为四种类型,赋予了 JVM 更灵活的内存管理能力,开发者可以根据对象的使用场景选择合适的引用类型:

1. 强引用(Strong Reference)

这是最常见的引用类型,我们日常开发中创建的大多数引用都属于强引用,例如:

Object obj = new Object(); // obj是强引用

特点

  • 只要强引用存在,被引用的对象就不会被垃圾收集器回收
  • 即使内存不足,JVM 宁愿抛出 OutOfMemoryError 异常,也不会回收强引用指向的对象
  • 只有当强引用被显式地置为 null,或者超出其作用域时,对象才有可能被回收

2. 软引用(Soft Reference)

软引用用于描述那些有用但非必需的对象,通过java.lang.ref.SoftReference类实现:

// 创建软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 获取软引用指向的对象
Object obj = softRef.get();

特点

  • 当内存充足时,软引用指向的对象不会被回收
  • 当内存不足时,JVM 会优先回收软引用指向的对象
  • 常被用于实现内存敏感的缓存,如图片缓存、数据缓存等

JVM 规范要求,在抛出 OutOfMemoryError 之前,必须回收所有软引用指向的对象。

3. 弱引用(Weak Reference)

弱引用用于描述非必需对象,强度比软引用更弱,通过java.lang.ref.WeakReference类实现:

// 创建弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 获取弱引用指向的对象
Object obj = weakRef.get();

特点

  • 无论内存是否充足,只要发生垃圾回收,弱引用指向的对象都会被回收
  • 弱引用的生命周期通常很短,只存在于一次垃圾回收之前
  • 适用于存储那些可能被随时丢弃的缓存数据

WeakHashMap 就是弱引用的典型应用,当键(Key)不再被强引用指向时,对应的键值对会被自动移除。

4. 虚引用(Phantom Reference)

虚引用也称为 "幽灵引用" 或 "幻影引用",是最弱的一种引用类型,通过java.lang.ref.PhantomReference类实现:

// 创建引用队列
ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 创建虚引用
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// 永远返回null
Object obj = phantomRef.get();

特点

  • 无法通过虚引用的get()方法获取对象实例,始终返回 null
  • 虚引用必须与引用队列(ReferenceQueue)配合使用
  • 当对象被回收时,虚引用会被加入到关联的引用队列中,用于跟踪对象的回收状态
  • 主要用于在对象被回收时执行一些资源释放操作,如释放直接内存(Direct Memory)

四种引用类型的优先级从高到低为:强引用 > 软引用 > 弱引用 > 虚引用。这种分级机制让 JVM 可以更灵活地管理内存,根据对象的重要性采取不同的回收策略。

三、垃圾收集算法

确定了需要回收的对象后,JVM 需要使用具体的算法来执行垃圾回收操作。常用的垃圾收集算法有以下几种:

1. 标记 - 清除算法(Mark-Sweep)

这是最基础的垃圾收集算法,分为两个阶段:

标记阶段:从 GC Roots 开始,遍历所有可达对象,并标记为存活对象。

清除阶段:遍历整个内存区域,回收所有未被标记的对象(即死亡对象),释放其占用的内存空间。

优点

  • 实现简单,不需要移动对象
  • 适用于对象存活率高的场景

缺点

  • 标记和清除过程效率都不高,需要遍历所有对象
  • 会产生大量不连续的内存碎片,导致后续大对象无法找到足够的连续内存而不得不提前触发垃圾回收

2. 复制算法(Copying)

为解决标记 - 清除算法的效率问题而提出,核心思想是将内存空间划分为两个大小相等的区域:

工作过程

  1. 将内存划分为两个区域(通常称为 From 区和 To 区)
  2. 只使用其中一个区域(From 区)
  3. 当 From 区内存不足时,将所有存活对象复制到另一个区域(To 区)
  4. 清除 From 区的所有对象,交换 From 区和 To 区的角色

优点

  • 实现简单,运行高效,只需复制存活对象
  • 不会产生内存碎片,因为存活对象被连续放置在 To 区
  • 适合对象存活率低的场景

缺点

  • 内存利用率低,只能使用一半的内存空间
  • 当对象存活率高时,复制操作的成本会很高

现代 JVM 对复制算法进行了优化,将新生代划分为 Eden 区和两个 Survivor 区(通常比例为 8:1:1),大大提高了内存利用率。

3. 标记 - 整理算法(Mark-Compact)

针对老年代对象存活率高的特点,标记 - 整理算法在标记 - 清除算法的基础上进行了改进:

工作过程

  1. 标记阶段与标记 - 清除算法相同,标记所有存活对象
  2. 整理阶段将所有存活对象向内存空间的一端移动,使它们紧凑排列
  3. 直接清理掉边界以外的所有内存空间

优点

  • 避免了内存碎片问题
  • 内存利用率高,不需要额外的内存空间
  • 适合对象存活率高的场景

缺点

  • 增加了对象移动的成本,需要更新所有引用这些对象的指针
  • 整理阶段的效率相对较低

4. 分代收集算法(Generational Collection)

当前商业虚拟机普遍采用的算法,其核心思想是根据对象的存活周期将内存划分为不同的区域(代),对不同的区域采用不同的收集算法:

分代思想

  • 新生代(Young Generation):对象创建后首先分配在新生代,特点是对象存活率低,大部分对象创建后很快就会被回收
  • 老年代(Old Generation):对象在新生代经历多次回收后仍然存活,会被移到老年代,特点是对象存活率高,生命周期长
  • 永久代 / 元空间(Permanent Generation/Metaspace):用于存储类信息、常量、静态变量等,回收频率较低

算法选择

  • 新生代:采用复制算法,因为对象存活率低,复制成本低
  • 老年代:采用标记 - 清除或标记 - 整理算法,因为对象存活率高,避免了复制算法的高成本

分代收集算法结合了不同算法的优点,根据不同代的特点选择最适合的收集策略,从而提高整体垃圾收集效率。

四、主流垃圾收集器

垃圾收集算法是 “理论基础”,而垃圾收集器是 “实际实现”。JVM 提供了多种收集器,按线程模型和工作方式可分为串行收集器并行收集器并发收集器三类,适用于不同的应用场景(如客户端、服务端、延迟敏感型、吞吐量优先型)。

1. 串行收集器

串行收集器采用单线程执行 GC 操作,收集期间触发 STW,暂停所有用户线程。其优势是实现简单、无线程交互开销,适合资源受限场景。

(1)Serial 收集器(新生代)

Serial 是最基础的新生代收集器,仅用一个线程执行 GC:

  • 核心特性:单线程标记 + 复制,STW 期间暂停所有用户线程;
  • 算法实现:新生代复制算法(Eden+Survivor 区);
  • 优势:单 CPU 环境下效率高(无线程切换开销),内存占用小;
  • 局限:堆内存增大时,STW 时间同步增加;无法利用多 CPU 资源;
  • 适用场景:客户端应用(如桌面程序)、嵌入式系统、单 CPU 环境;
  • 参数配置-XX:+UseSerialGC(启用后自动搭配 Serial Old 收集器)。
(2)Serial Old 收集器(老年代)

Serial Old 是 Serial 的老年代版本,同样采用单线程:

  • 核心特性:单线程标记 + 整理,STW 期间暂停所有用户线程;
  • 算法实现:老年代标记 - 整理算法(避免碎片);
  • 优势:内存利用率高,实现简单,资源消耗低;
  • 局限:大堆内存场景下 STW 时间长,不适合服务端应用;
  • 适用场景:与 Serial 配合使用、CMS 收集器失败后的备选方案;
  • 参数配置:无需单独配置,启用 Serial 后自动启用。

2. 并行收集器

并行收集器采用多线程执行 GC 操作,能充分利用多核 CPU 资源,通过并行处理缩短 GC 时间,核心目标是 “提升吞吐量”(吞吐量 = 用户线程运行时间 /(用户线程运行时间 + GC 时间))。

(1)ParNew 收集器(新生代)

ParNew 是 Serial 的多线程版本,专为新生代设计:

  • 核心特性:多线程并行标记 + 复制,仍需 STW;
  • 算法实现:新生代复制算法(与 Serial 一致,仅线程数不同);
  • 优势:多 CPU 环境下效率远高于 Serial;是唯一能与 CMS 收集器配合的新生代收集器;
  • 局限:线程数量过多时,线程间竞争会抵消并行优势;仍有明显 STW;
  • 适用场景:多 CPU 服务端应用、需与 CMS 配合的场景;
  • 参数配置-XX:+UseParNewGC(启用)、-XX:ParallelGCThreads=N(指定 GC 线程数,默认与 CPU 核心数一致)。
(2)Parallel Scavenge 收集器(新生代)

Parallel Scavenge 是一款 “吞吐量优先” 的新生代收集器,与 ParNew 的区别在于 “优化目标不同”:

  • 核心特性:多线程并行,以 “最大化吞吐量” 为首要目标;支持 “自适应调节”;
  • 算法实现:新生代复制算法;
  • 关键能力
    • 可通过-XX:MaxGCPauseMillis=N设置 “最大 GC 停顿时间”(JVM 会调整堆大小以满足目标,值越小堆越小,GC 频率越高);
    • 可通过-XX:GCTimeRatio=N设置 “GC 时间占比”(默认 99,即允许 GC 时间占比≤1%,值越大吞吐量越高);
    • 启用-XX:+UseAdaptiveSizePolicy后,JVM 会自动调整堆大小、Survivor 比例、晋升年龄等参数,无需手动调优;
  • 优势:高吞吐量,适合计算密集型应用;自适应调节降低调优难度;
  • 局限:无法与 CMS 配合使用;对延迟敏感的应用不友好;
  • 适用场景:后台批处理、大数据分析、科学计算等吞吐量优先场景;
  • 参数配置-XX:+UseParallelGC(启用)。
(3)Parallel Old 收集器(老年代)

Parallel Old 是 Parallel Scavenge 的老年代版本,为其提供完整的并行收集解决方案:

  • 核心特性:多线程并行执行老年代收集,延续 Parallel 系列 “吞吐量优先” 的设计目标;
  • 算法实现:采用标记 - 整理算法,避免老年代内存碎片;
  • 优势:与 Parallel Scavenge 配合形成完整的并行收集体系,大幅提升大堆内存场景下的吞吐量;相比 Serial Old,在多 CPU 环境下 STW 时间更短;
  • 局限:无法与 CMS 配合使用;对延迟敏感的应用不友好;
  • 适用场景:与 Parallel Scavenge 搭配使用,适用于对吞吐量要求高、堆内存较大的服务端应用;是 JDK 8 默认的服务器模式收集器组合;
  • 参数配置-XX:+UseParallelOldGC(启用,通常与-XX:+UseParallelGC同时使用)。

3.并发收集器

并发收集器通过让 GC 线程与用户线程并发执行(大部分阶段同时运行),最大限度减少 STW 时间,核心目标是 “降低延迟”,适合对响应时间敏感的应用(如 Web 服务器、电商交易系统)。

(1)CMS 收集器(Concurrent Mark Sweep)

CMS 是一款以 “低延迟” 为核心目标的老年代收集器,采用 “并发标记 - 清除” 算法:

  • 核心特性:老年代专用,大部分阶段与用户线程并发执行,仅初始标记和重新标记阶段需要短暂 STW;
  • 工作流程(四阶段)
  1. 初始标记:标记 GC Roots 直接关联的对象(如栈中引用的对象),需要 STW(通常仅数毫秒);
  2. 并发标记:从初始标记的对象出发,遍历整个引用链,标记所有可达对象,此阶段与用户线程并发执行(无 STW);
  3. 重新标记:修正并发标记期间因用户线程操作导致的 “标记变动”(如对象引用被修改),需要 STW(时间比初始标记稍长,但通常仍在毫秒级);
  4. 并发清除:回收所有未被标记的死亡对象,与用户线程并发执行(无 STW);
  • 优势:停顿时间短(通常在毫秒级),适合对延迟敏感的应用;并发执行期间不阻塞用户线程,保证服务连续性;
  • 局限:
  1. CPU 消耗高:并发阶段需要占用大量 CPU 资源,在 CPU 核心较少的环境中,可能因 GC 线程与用户线程竞争 CPU 而导致应用响应变慢;
  2. 内存碎片:采用标记 - 清除算法,老年代会产生内存碎片,可能导致大对象无法分配而提前触发 Full GC;
  3. 浮动垃圾:并发清除阶段用户线程新产生的垃圾(浮动垃圾)无法被本次 GC 回收,需预留足够内存空间避免 OOM;
  4. 收集停顿不可控:在并发标记阶段,若老年代内存不足,会触发 “Concurrent Mode Failure”,临时启用 Serial Old 收集器,导致长时间 STW;
  • 适用场景:Web 应用服务器、电商交易系统等对响应时间敏感的服务;多 CPU 环境(能承担并发 GC 的 CPU 开销);
  • 参数配置:-XX:+UseConcMarkSweepGC(启用,默认搭配 ParNew 作为新生代收集器);-XX:CMSInitiatingOccupancyFraction=N(设置老年代占用率阈值,默认约 92%,达到阈值时触发 CMS 收集);-XX:+UseCMSCompactAtFullCollection(Full GC 后进行内存整理,解决碎片问题)。
(2)G1 收集器(Garbage-First)

G1 是 JDK 7 引入的面向服务端应用的 “全能型” 收集器,兼顾吞吐量和延迟,是 JDK 9 及以上版本默认的服务器模式收集器:

  • 核心特性:
  1. 区域化管理:将堆内存划分为多个大小相等的Region(1MB~32MB,可通过-XX:G1HeapRegionSize设置),每个 Region 可动态标记为 Eden 区、Survivor 区或老年代区,无需物理隔离;
  2. 优先级回收:优先回收垃圾最多的 Region(Garbage-First),通过维护 “回收价值”(回收收益 / 所需时间)排序,确保在有限时间内获得最高回收效率;
  3. 混合收集:可同时回收新生代和老年代的 Region,无需区分固定的新生代和老年代区域;
  4. 可控延迟:通过-XX:MaxGCPauseMillis设置最大 GC 停顿时间目标(默认 200ms),G1 会根据历史数据动态调整回收 Region 的数量,确保不超过目标时间;
  • 工作流程(四阶段):
  1. 初始标记:标记 GC Roots 直接关联的对象,需要短暂 STW;
  2. 并发标记:从初始标记的对象出发,遍历引用链标记所有可达对象,与用户线程并发执行;
  3. 最终标记:处理并发标记期间产生的 “SATB(Snapshot-At-The-Beginning)” 日志,修正标记结果,需要短暂 STW;
  4. 筛选回收:根据 Region 的回收价值排序,优先回收高价值 Region,采用复制算法将存活对象复制到新 Region,同时整理内存(无碎片),此阶段可根据 MaxGCPauseMillis 目标选择部分 Region 回收,需要 STW;
  • 优势:
  1. 兼顾高吞吐量和低延迟,适应不同应用场景;
  2. 无内存碎片问题(采用复制算法整理内存);
  3. 支持大堆内存(可达数十 GB 甚至更大);
  4. 可精确控制 GC 停顿时间,满足延迟敏感型应用需求;
  • 局限:
  1. 内存占用较高:需要维护 Region 元数据(如回收价值、引用信息),额外内存消耗约为堆大小的 10%~20%;
  2. 小堆内存场景下性能不如传统收集器(如 Parallel 系列);
  • 适用场景:堆内存较大的服务端应用(如微服务、分布式系统);既要求高吞吐量又需要低延迟的场景;替代 CMS 解决其内存碎片和 CPU 消耗问题;
  • 参数配置:-XX:+UseG1GC(启用);-XX:MaxGCPauseMillis=N(设置最大 GC 停顿时间目标,默认 200ms);-XX:G1HeapRegionSize=N(设置 Region 大小,需为 2 的幂次方,范围 1MB~32MB)。


文章转载自:

http://22jn9WvZ.mmtjk.cn
http://WFw99c5b.mmtjk.cn
http://NyS1gABH.mmtjk.cn
http://fqMR9WHZ.mmtjk.cn
http://joMi1HaV.mmtjk.cn
http://AaVacMy0.mmtjk.cn
http://6BDCAfXY.mmtjk.cn
http://Ys5Ph39X.mmtjk.cn
http://NFw8Pq1e.mmtjk.cn
http://dOAtLM8D.mmtjk.cn
http://7QHqmbZr.mmtjk.cn
http://Qkf8BKvA.mmtjk.cn
http://w6nrVZpB.mmtjk.cn
http://QzYC3MMw.mmtjk.cn
http://ihxvusB0.mmtjk.cn
http://XTToyyvj.mmtjk.cn
http://FOMckj2i.mmtjk.cn
http://H0gLgjyu.mmtjk.cn
http://6KnDyhHx.mmtjk.cn
http://VrHAQWcg.mmtjk.cn
http://h9c30bpj.mmtjk.cn
http://rtiypA2k.mmtjk.cn
http://sxuq3na3.mmtjk.cn
http://XPoyZOXg.mmtjk.cn
http://cSE6xXhE.mmtjk.cn
http://Tu9mGbCd.mmtjk.cn
http://zUFTl0FA.mmtjk.cn
http://3KaESG0B.mmtjk.cn
http://jBSXOoZd.mmtjk.cn
http://ZwIRPAbt.mmtjk.cn
http://www.dtcms.com/a/384517.html

相关文章:

  • 学习日记-XML-day55-9.14
  • SenseVoice + WebRTC:打造行业级实时语音识别系统的底层原理与架构设计
  • C++ 异常机制深度解析:从原理到实战的完整指南
  • 在 Qoder 等 AI 二创 IDE 里用 VS Code Remote-SSH 的“曲线连接”实战
  • 云计算与大数据技术深入解析
  • 如何用Verdi APP抽出某个指定module的interface hierarchy
  • MySQL 锁机制详解+示例
  • 消息队列的“翻车“现场:当Kafka和RocketMQ遇到异常时会发生什么?
  • 在Cursor上安装检索不到的扩展库cline的方式方法
  • 第二十一章 ESP32S3 IIC_OLED 实验
  • 能取代 transform 的架构目前看来 有哪些
  • 为什么基频是信号速率的1/2?
  • Unity UI坐标说明
  • 微美全息(NASDAQ:WIMI)以声誉混合多层共识,开启区块链共识算法创新篇章
  • LAN9253通过CHIP_MODE改变链路顺序
  • 矩阵运算_矩阵A和向量a的转置T相关
  • C++异步任务处理与消息可靠性保障指南:从基础到实战
  • 总结-十大管理输入输出
  • 【Vue3】09-编写vue时,reactive的使用
  • Transformer原理学习(2)位置编码
  • C++编程语言:标准库:第38章——输入输出流(Bjarne Stroustrup)
  • 北理工提出仅依赖机载传感器针对IAP的控制与状态估计框架
  • JVM 垃圾收集算法详解!
  • pycharm选择conda的interpreter
  • 为什么要将OpenCV帧转换为PIL图像
  • Apache ShardingSphere 实战:自定义 SQL 拦截插件开发指南
  • 【langchain】加载、处理和分割源数据文件
  • cmake .. -G “Visual Studio 12“
  • i.MX6ULL 外设初始化
  • Node.js如何实现一个WebSocket服务