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

一篇文章带你彻底搞懂 JVM 垃圾收集器

垃圾收集器是 JVM 内存管理的执行引擎,负责自动回收无用的对象内存。其设计核心是 权衡:主要是吞吐量停顿时间之间的权衡。没有“最好”的收集器,只有“最适合”特定场景的收集器。

一、核心基础:分代收集模型

主流 HotSpot JVM 采用分代模型,将堆内存划分为不同区域,基于对象存活周期的“弱分代假说”采用不同的收集策略:

新生代

  • 存放新创建的对象。特点是区域小、对象存活率低、回收频繁。

    • Eden区:对象诞生的地方。

    • Survivor幸存者区 :存放 Minor GC 后存活的对象。

老年代

  • 存放经过多次 Young GC 后依然存活的对象。特点是区域大、对象存活率高。

  • 元空间 :存放类元数据等,由本地内存提供,不属堆内存。

垃圾收集类型:

  • Minor GC / Young GC:只收集年轻代的垃圾。

  • Major GC / Old GC:只收集老年代的垃圾。

  • Full GC:收集整个堆(新生代 + 老年代 + 方法区)的垃圾。

二、垃圾收集算法

分代收集理论:

目前主流JVM虚拟机中的垃圾收集器,都遵循分代收集理论:

弱分代:绝大多数对象都是朝生夕灭
强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡

按照分代收集理论设计的“分代垃圾收集器”,所采用的设计原则:收集器应该将Java堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历过垃圾收集过程的次数)分配到不同的区域存储。

分代存储:

  • 如果一个区域中大多数对象都是朝生夕灭(新生代),难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,只关注如何保留少量存活对象,而不是去标记大量将要回收的对象,就能以较低代价回收到大量的空间。
  • 如果一个区域中大多数对象都是难以回收(老年代),那么把它们集中放在一起,JVM虚拟机就可以使用较低的频率,来对这个区域进行回收。

这样设计的好处是,兼顾垃圾收集的时间开销和内存空间的有效利用。

分代收集:

堆区按照分代存储的好处:
  • 在Java堆区划分成不同区域后,垃圾收集器才可以每次只回收其中某一个或者某些区域,所以才有MinorGC、MajorGC、FullGC等垃圾收集类型划分。
  • 在Java堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾收集算法:标记-复制算法、标记-清除算法、标记-整理算法等。

垃圾收集算法:

垃圾收集算法主要解决三个问题:

  1. 如何判断对象已死?(标记阶段)

  2. 如何回收垃圾对象占用的内存?(清除阶段)

  3. 如何避免内存碎片?(整理阶段)

围绕这些问题,衍生出了以下几种基础算法。

1. 基础:如何判断对象“已死”?

垃圾收集的前提是判断对象是否存活。主要有两种算法:

(1). 引用计数算法
  • 机制:在对象中添加一个引用计数器。每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一。任何时刻计数器为零的对象就是不可能再被使用的。

  • 优点:原理简单,判定效率高。

  • 缺点无法解决对象之间循环引用的问题(对象A引用B,对象B引用A,但再无第三方引用它们俩)。因此,主流Java虚拟机均不采用此算法

(2). 可达性分析算法
  • 机制:通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链”。如果某个对象到GC Roots间没有任何引用链相连(即从GC Roots到这个对象不可达),则证明此对象是不可能再被使用的。

  • Java中可作为GC Roots的对象包括

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。

    • 方法区中类静态属性引用的对象。

    • 方法区中常量引用的对象。

    • 本地方法栈中JNI(即通常说的Native方法)引用的对象。

    • Java虚拟机内部的引用(如基本数据类型对应的Class对象,常驻的异常对象等)。

    • 所有被同步锁(synchronized关键字)持有的对象。

  • 这是目前主流Java虚拟机采用的判断对象是否存活的算法。

2. 核心:垃圾收集算法

确定了哪些对象是垃圾之后,就需要进行回收。主要有三种基础算法:

1. 标记-清除算法
  • 过程:算法分为“标记”和“清除”两个阶段。

    1. 标记:首先通过可达性分析,标记出所有需要回收的对象。

    2. 清除:随后,统一回收掉所有被标记的对象。

  • 优点:是最基础的收集算法,后续算法都是在其基础上改进的。

  • 缺点

    • 执行效率不稳定:如果堆中包含大量需要回收的对象,则标记和清除两个过程的执行效率都会随之降低。

    • 内存碎片化:标记、清除之后会产生大量不连续的内存碎片。碎片太多可能导致以后在分配大对象时无法找到足够的连续内存,从而不得不提前触发另一次垃圾收集。

2. 复制算法
  • 过程:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

  • 优点

    • 高效:只需要按顺序分配内存,移动堆顶指针即可,分配新对象的速度极快。

    • 无碎片:复制过程中会将存活对象紧凑地排列在另一块内存,完全避免了碎片问题。

  • 缺点内存利用率低,可用内存缩小为了原来的一半。

  • 应用是HotSpot虚拟机中年轻代垃圾收集的核心算法。但商业虚拟机都对其进行了优化:并不按1:1的比例划分内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间(通常是8:1:1)。每次分配只使用Eden和其中一块Survivor。发生Minor GC时,将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。这样只有10%的内存会被“浪费”。

3. 标记-整理算法
  • 过程:也分为“标记”和“整理”两个阶段。

    1. 标记:与“标记-清除”算法一样,首先标记所有需要回收的对象。

    2. 整理:让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。

  • 优点

    • 无内存碎片:整理后内存是紧凑的。

    • 内存利用率高:无需浪费一半的内存空间。

  • 缺点“整理”阶段涉及大量对象的移动,并且需要更新所有引用这些对象的指针,是一种开销较大的操作,而且移动对象时必须全程暂停用户应用程序(Stop The World)。

  • 应用主要用于老年代的垃圾收集,如Serial Old和Parallel Old收集器。

4. 分代收集算法
  • 本质:这并非一种新的算法思想,而是上述三种算法的实际应用范式

  • 思路:根据对象存活周期的不同,将Java堆划分为年轻代老年代

    • 在年轻代:对象“朝生夕死”,存活率低,回收频繁。因此选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,且速度最快。

    • 在老年代:对象存活率高,没有额外的空间对它进行分配担保。因此必须使用 “标记-清除”或“标记-整理” 算法来进行回收。

3. 垃圾收集算法总结:

算法过程优点缺点适用场景
标记-清除标记 → 清除实现简单效率低、产生碎片老年代 (CMS)
复制标记 → 复制到另一半 → 清空原空间效率高、无碎片浪费一半空间年轻代 (几乎所有收集器)
标记-整理标记 → 整理存活对象到一端 → 清理边界无碎片、空间利用率高移动对象成本高老年代 (Serial Old, Parallel Old)
分代收集根据代的特点选用上述算法综合优势,扬长避短实现复杂现代JVM的实际实践

三、经典垃圾收集器详解

垃圾收集器是内存回收算法的具体实现。不同收集器适用于不同场景,核心目标是在 吞吐量 和 停顿时间 之间取得最佳平衡。

1. Serial 收集器(新生代)

  • 特点单线程工作的收集器。在进行垃圾收集时,必须暂停所有用户工作线程("Stop The World"),直到收集结束。

  • 算法:复制算法。

  • 定位:是 Client 模式下的默认新生代收集器。简单而高效,没有线程交互开销。适用于内存资源受限的客户端应用或单核服务器环境。

2. Serial Old 收集器(老年代)

  • 特点Serial 收集器的老年代版本,同样是一个单线程收集器。

  • 算法:标记-整理算法。

  • 定位:主要用于 Client 模式。在 Server 模式下,它主要作为 CMS 收集器失败时的后备预案(Concurrent Mode Failure)。

3. ParNew 收集器(新生代)

  • 特点:本质上是 Serial 收集器的多线程并行版本。它使用多条线程进行垃圾收集,但在收集时同样需要“Stop The World”。

  • 算法:复制算法。

  • 定位:在 JDK 7 之前,它是许多服务端应用的首选新生代收集器,因为它是 唯一能与 CMS 收集器配合工作 的新生代收集器。

4. Parallel Scavenge 收集器(新生代)

  • 特点:也称为“吞吐量优先”收集器。它使用并行多线程进行收集,但其关注点是达到一个 可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。

  • 算法:复制算法。

  • 定位:适合后台运算、科学计算等 不需要低延迟,但需要高吞吐量 的任务。是 JDK 8 默认的新生代收集器

5. Parallel Old 收集器(老年代)

  • 特点Parallel Scavenge 收集器的老年代版本,使用多线程进行收集。

  • 算法:标记-整理算法。

  • 定位:在 JDK 6 之后才开始提供。它的出现使得 Parallel Scavenge + Parallel Old 成为一套真正专注于 高吞吐量 的完整组合。

6. CMS 收集器(老年代)

  • 目标:以 获取最短回收停顿时间 为目标,注重用户体验。

  • 过程:其核心过程有四步骤:

    1. 初始标记 (Stop The World):速度极快。

    2. 并发标记 (与用户线程并发):耗时最长但无需停顿。

    3. 重新标记 (Stop The World):修正并发标记期间的变化。

    4. 并发清除 (与用户线程并发)。

  • 算法:标记-清除算法(会产生碎片)。

  • 缺点:对CPU资源敏感、无法处理“浮动垃圾”、会产生内存碎片。

  • 定位:适用于互联网站、B/S系统的服务端,重视服务的响应速度。现已不推荐使用,被 G1 等收集器取代。

7. G1 收集器(老年代)

  • 革新:放弃了传统物理连续的分代模型,将堆划分为多个大小相等的独立区域(Region)。虽然保留分代概念,但年轻代和老年代是这些 Region 的动态集合。

  • 目标可预测的停顿时间模型。允许用户指定期望的停顿时间。

  • 工作机制:跟踪各个 Region 的回收价值(空间大小及回收成本),优先回收价值最大的 Region。

  • 算法:整体上看是标记-整理算法,局部(两个Region之间)基于复制算法。

在不同的应用场景下,选择合适的垃圾收集器至关重要。对于追求高吞吐量的后台计算任务,Parallel Scavenge与Parallel Old的组合是最佳选择;若需要低延迟的响应式服务,CMS或新一代的G1收集器更为合适;而对于内存敏感的单核环境,Serial收集器仍是最实用的选项。随着技术发展,G1收集器凭借其平衡的吞吐量和延迟表现,已成为大多数现代应用的默认推荐。在选择时,还需考虑JDK版本和具体的性能需求,才能做出最合适的决策。

http://www.xdnf.cn/news/20122.html

相关文章:

  • 大数据开发计划表(实际版)
  • Python入门教程之数学运算符
  • 基于单片机智能水龙头/智能洗漱台设计
  • STM32F103_Bootloader程序开发15 - 从Keil到vscode + EIDE + GCC的迁移实践
  • 8051单片机-成为点灯大师
  • STL重点
  • Web Session 机制深度解析
  • Windows 11使用技巧
  • 汉诺塔递归过程推导(详细+省流)
  • 2025 年高教社杯全国大学生数学建模竞赛A 题 烟幕干扰弹的投放策略完整成品 思路 模型 代码 结果 全网首发高质量!!!
  • 2025跨境独立站最新最完整的搭建流程
  • AI智汇社区凭什么半年估值破亿?这家公司让普通人也能玩转AI开发
  • 【IO】共享内存、信息量集
  • 【已更新文章+代码】2025数学建模国赛B题思路代码文章高教社杯全国大学生数学建模-碳化硅外延层厚度的确定
  • 《设计模式之禅》笔记摘录 - 19.备忘录模式
  • 新增MCP工具管理,AI对话节点新增工具设置,支持对接企业微信机器人,MaxKB v2.1.0版本发布
  • 理解进程栈内存的使用
  • 嵌入式第四十六天(51单片机)
  • git提交代码
  • React笔记_组件之间进行数据传递
  • 只会git push?——git团队协作进阶
  • RAG(检索增强生成)-篇一
  • Linux-xargs-seq-tr-uniq-sort
  • Oracle 数据库使用事务确保数据的安全
  • 实现自己的AI视频监控系统-第三章-信息的推送与共享4
  • 如何在SpringBoot项目中优雅的连接多台Redis
  • vue3的 三种插槽 匿名插槽,具名插槽,作用域插槽
  • 无需Python:Shell脚本如何成为你的自动化爬虫引擎?
  • Dubbo消费者无法找到提供者问题分析和处理
  • 记录SSL部署,链路不完整问题