JVM的垃圾回收

为什么会有垃圾回收,简单来说就是内存不够用了,需要清除一些不用的对象释放内存。

🎯 如何判断对象"可回收"?

垃圾回收的前提是准确判断哪些对象已经"死亡"。JVM主要使用可达性分析算法,而非简单的引用计数法(因其无法解决循环引用问题)。

可达性分析算法的核心思想 是:以一系列称为 "GC Roots" 的对象作为起始点,开始向下搜索,所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时(即对象不可达),则证明此对象不再被使用,可以被回收。

哪些对象可以作为GC Roots? 主要包括以下几类:

  • 虚拟机栈中的引用:正在执行的各个方法中的局部变量表所引用的对象。
  • 方法区中静态属性引用的对象:类的静态变量。
  • 方法区中常量引用的对象:比如字符串常量池里的引用。
  • 本地方法栈中引用的对象:JNI(Native方法)引用的对象。
  • Java虚拟机内部的引用:如基本数据类型的Class对象,常驻的异常对象等。
  • 被同步锁持有的对象 :例如synchronized关键字锁定的对象。

为了更直观地理解对象从创建到被回收的完整路径,特别是可达性分析的过程,我们可以参考下面的流程图:

不可达
可达



对象实例化
在Eden区分配内存
对象被GC Roots引用
Eden区满
执行Minor GC
进行可达性分析
从GC Roots

是否可达?
被标记为可回收对象
对象内存被回收
存活对象被复制到

Survivor区或老年代
对象年龄增加
年龄超过阈值?
对象晋升至老年代
老年代空间不足?
执行Full GC

🔗 认识Java的引用类型

对象的"可达"与"不可达"与其引用类型息息相关。Java提供了四种强度不同的引用类型,它们对垃圾回收的影响各不相同:

引用类型 强度 被回收时机 常见应用场景
强引用 最强 永远不会被GC回收(即使OOM) 平常的 Object obj = new Object()
软引用 次之 内存不足时,会被GC回收 实现内存敏感的缓存,如图片缓存
弱引用 较弱 只能生存到下一次GC发生之前 构建非必需的缓存,WeakHashMap
虚引用 最弱 无法通过虚引用取得对象实例,其存在仅用于接收对象被回收的系统通知 用于在对象被回收时收到通知,例如管理堆外内存

⚙️ 核心垃圾回收算法

当确定对象可回收后,就需要具体的算法来执行清理。以下是三种经典算法:

  1. 标记-清除算法

    • 过程:首先标记出所有需要回收的对象;在标记完成后,统一回收所有被标记的对象。
    • 缺点 :效率问题,标记和清除两个过程的效率都不高;会产生大量不连续的内存碎片,可能导致后续无法分配大对象而提前触发GC。
  2. 复制算法

    • 过程:将可用内存按容量分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还存活的对象复制到另一块上,然后把已使用的空间一次清理掉。
    • 优点:实现简单,运行高效,没有内存碎片。
    • 缺点:内存利用率低,只有一半内存可用。如果对象存活率高,复制开销会很大。
    • 应用场景 :主要适用于新生代,因为新生代中98%的对象都是"朝生夕死"的。HotSpot虚拟机将新生代划分为一个Eden区和两个Survivor区,每次使用Eden和一个Survivor,回收时将存活对象复制到另一个Survivor区。
  3. 标记-整理算法

    • 过程:标记过程与"标记-清除"一样。但后续不是直接清理,而是让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。
    • 优点:避免了内存碎片问题,也避免了复制算法的空间浪费。
    • 缺点:移动对象需要一定开销,并且需要更新被移动对象的引用地址。
    • 应用场景 :主要适用于老年代,因为老年代对象存活率高,不适合频繁复制。

🧠 分代收集策略:算法的实践智慧

现代JVM综合以上算法,采用了分代收集策略 。它基于一个经验法则:绝大多数对象都是朝生夕死的。因此,Java堆被划分为新生代和老年代,对不同生命周期的对象采用不同的回收策略。

  • 新生代 :对象创建的地方。每次垃圾回收时都有大量对象死去,只有少量存活,因此采用复制算法,效率高。
  • 老年代 :存放长期存活的对象。对象存活率高,没有额外的内存空间进行分配担保,因此采用标记-清除标记-整理算法。

🚀 主流垃圾收集器详解

垃圾收集器是上述算法的具体实现。JDK中提供了多种收集器,适用于不同场景。

收集器 目标 新生代算法 老年代算法 特点与适用场景
Serial / Serial Old 单线程、简单高效 复制 标记-整理 客户端模式,单CPU环境,简单有效
Parallel Scavenge / Parallel Old 高吞吐量 复制 标记-整理 JDK8默认,后台运算、批处理任务
ParNew + CMS 低停顿时间 复制 并发标记-清除 B/S系统,重视响应速度(CMS已废弃)
G1 兼顾吞吐量与停顿 整体标记-整理,局部复制 同上 JDK9及以后默认,通用服务端应用,可预测停顿
ZGC / Shenandoah 超低停顿(<10ms) 并发标记-整理/复制 同上 大内存、极致低延迟,如金融交易、实时系统

各个版本的JDK的垃圾收集器对比

不同JDK版本的默认垃圾收集器并不相同,它们随着版本迭代在不断演进,其核心目标是更好地平衡吞吐量延迟内存管理效率

下表清晰地展示了这几个关键JDK版本的默认垃圾收集器及其核心变化。

JDK 版本 默认垃圾收集器 核心变化与特点
JDK 8 Parallel GC (Parallel Scavenge + Parallel Old) 追求高吞吐量,适合后台运算、数据处理等对停顿时间不敏感的场景。是JDK 8及之前版本在Server模式下的默认选择 。
JDK 11 G1 GC (Garbage-First) 标志着默认GC从吞吐量优先转向低延迟目标。G1旨在尽可能缩短大型堆(如超过4GB )的停顿时间,同时保持良好的吞吐量,适用于需要更稳定响应时间的应用服务器 。
JDK 17 G1 GC 继续以G1为默认,并对其进行了大量优化(如更多阶段的并行化 )。同时,ZGCShenandoah作为成熟的低延迟收集器可供选择,但CMS收集器被移除 。
JDK 21 G1 GC G1保持默认。最重要的进展是引入了分代式ZGC,通过应用分代思想显著提升了ZGC的性能 ,为未来可能成为默认选项铺平了道路 。

🔄 垃圾收集器的演进逻辑

JDK垃圾收集器的演进主要围绕解决三个核心问题:

  1. 吞吐量 vs. 延迟 :早期Parallel GC最大化吞吐量,但可能带来较长停顿。后续的G1、ZGC等更关注降低单次GC停顿时间。
  2. 堆内存增大 :随着应用内存需求增长,传统收集器在全堆GC时停顿时间变长。G1引入的Region分区 、ZGC的并发处理能力都是为了更好地管理大内存堆 。
  3. 可预测性 :现代应用(如微服务)需要更可预测的响应时间。G1的停顿预测模型-XX:MaxGCPauseMillis)和ZGC的亚毫秒级停顿目标都是为此努力 。

💡 如何选择垃圾收集器?

没有"最好"的收集器,只有最合适你应用场景的。你可以参考以下思路:

  • 追求极致吞吐量 :如果应用是后台计算任务,对停顿不敏感,Parallel GC可能仍然是好选择。
  • 平衡吞吐量与延迟 :对于大多数面向用户的应用(如Web服务),G1 GC是稳健的默认选择,它在JDK 11及以后版本中能很好地平衡吞吐量和延迟 。
  • 要求极低延迟 :若应用对停顿极度敏感(如金融交易、实时系统),且堆内存较大,可以考虑启用ZGCShenandoah 。特别是在JDK 21及以上版本,分代ZGC是非常有吸引力的选项 。
  • 特殊用途Epsilon GC (无操作GC)适用于短期存活或已知内存充足的无GC场景(如性能测试);Serial GC适用于微服务、资源受限环境(如容器)。

🛠️ 如何查看和指定垃圾收集器?

  • 查看默认GC :使用JVM参数 -XX:+PrintCommandLineFlags,输出中若包含 -XX:+UseG1GC 则表示当前使用G1 。
  • 指定GC :通过JVM启动参数指定,例如:
    • -XX:+UseG1GC (启用G1,JDK9后默认)
    • -XX:+UseParallelGC (启用Parallel GC)
    • -XX:+UseZGC (启用ZGC,适用于JDK 11以上 )

💎 简单总结

简单来说,JDK 8之后,默认垃圾收集器从注重吞吐量的Parallel GC 转向了注重低延迟的G1 GC。JDK 11开始,G1成为默认选择并持续优化,同时提供了ZGC等更先进的低延迟选项。JDK 21的分代ZGC进一步增强了其在低延迟场景下的竞争力。

相关推荐
星辰_mya4 分钟前
RockerMQ之commitlog与consumequeue
java·开发语言
꧁Q༒ོγ꧂5 分钟前
C++ 入门完全指南(六)--指针与动态内存
开发语言·c++
IT=>小脑虎8 分钟前
2026版 Go语言零基础衔接进阶知识点【详解版】
开发语言·后端·golang
ChangYan.9 分钟前
ffi-napi运行失败,报错:No native build was found,解决办法
开发语言
专注VB编程开发20年9 分钟前
压栈顺序是反向(从右往左)的,但正因为是反向压栈,所以第一个参数反而离栈顶(ESP)最近。
java·开发语言·算法
say_fall14 分钟前
C++ 类与对象易错点:初始化列表顺序 / 静态成员访问 / 隐式类型转换
android·java·开发语言·c++
热爱专研AI的学妹14 分钟前
2026世界杯观赛工具自制指南:实时比分推送机器人搭建思路
开发语言·人工智能·python·业界资讯
Dev7z22 分钟前
基于MATLAB图像处理的苹果品质自动分级系统设计与实现
开发语言·图像处理·matlab
源代码•宸27 分钟前
Golang基础语法(go语言指针、go语言方法、go语言接口、go语言断言)
开发语言·经验分享·后端·golang·接口·指针·方法
Bony-28 分钟前
Golang 常用工具
开发语言·后端·golang