Java 虚拟机 | 垃圾回收机制

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 「Java 路线」| 导读 —— 他山之石,可以攻玉 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)


目录


前置知识

这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~


1. 垃圾回收概述

垃圾回收机制(Garbage Collection,GC) 是一种自动的内存管理机制,即:当内存中的对象不再需要时,就自动释放以让出存储空间。

垃圾回收机制是 Java 虚拟机的重要特性之一,同时也是面试重要考点之一。在实践中,由于 GC 会占用程序运行资源,欲进行更有深度的内存性能优化也需要对垃圾回收机制有一定理解。

在讨论垃圾回收机制的时候,需要讨论的以下三个问题,你可以带着这三个问题阅读后面的内容,思路会更清晰。

  • 回收的对象: 哪些对象 / 区域需要回收?
  • 回收的时机: 什么时候触发 GC?
  • 回收的过程: 如何回收?

1.1 GC 相关概念

这一节,我们先罗列一些 GC 相关知识中比较重要的概念:

概念 描述
collector 表示程序中负责垃圾回收的模块
mutator 表示程序中除了 collector 以外的模块
增量式回收
(Incremental Collection)
每次 GC 只针对堆的一部分,而不是整个堆,大幅减少了停顿时间
分代回收
(Generational GC)
增量式回收的实现方式之一,将堆分为新生代、老生代和永生代等部分,每次 GC 往往只针对其中一代
并行回收
(Parallel Collection)
collector 中有多个垃圾回收线程
并发回收
(Concurrent Collection)
指垃圾回收工作的某个阶段,collector 线程和 mutator 可以同时执行。这样避免了 collector 线程工作时需要暂停 mutator 线程(stop-the-world)

1.2 垃圾回收的优缺点

  • 优点: 不再需要为每个 new 操作编写对应的 delete / free 操作,程序不容易出现内存泄漏或内存溢出问题;

  • 风险: 垃圾回收处理程序本身也占用系统资源(CPU 资源 / 内存),增大程序暂停时间。

1.3 GC 算法性能指标

在介绍垃圾回收算法之前,我们先来定义评价垃圾回收方法的性能指标:

指标 定义 描述
吞吐量(throughput) 指单位时间内的处理能力 吞吐量 = \frac{运行用户代码时间} {垃圾回收频率 * 单次垃圾回收时间}
最大暂停时间(pause time) 指因执行 GC 而暂停执行程序的最长时间 /
堆利用率(space overhead) 指有效使用的堆空间占整个堆的比例 影响因素:对象头大小 + 回收算法
访问局部性 指回收方法是否倾向于访问局部内存 访问局部内存更容易命中 CPU 缓存行

提示: 若不理解 “访问局部性” 的概念,可联想快速排序和堆排序的性能对比,前者的访问局部性更优。


2. 垃圾回收管理的区域(回收的对象)

根据《Java虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下区域:

运行时数据区域 线程独占 描述
程序计数寄存器 私有 存储下一条字节码指令的内存地址
Java 虚拟机栈 私有 存储线程栈帧(Stack Frame )

栈帧包含:局部变量表、操作数栈、动态连接、返回地址等信息
本地方法栈 私有 存储本地方法栈帧
Java 堆 共享 大多数对象的存储区域
方法区 共享 存储类型信息、常量、类静态变量、即使编译器编译后的代码缓存等

并不是 Java 虚拟机管理的所有区域都需要垃圾回收,线程独占的区域会随着线程结束而销毁,不需要垃圾回收。因此垃圾回收机制需要管理的区域是:

  • 堆: 垃圾对象;

  • 方法区: 废弃的常量和不再使用的类型。


3. 如何判定垃圾对象?(回收的时机)

判断对象是否为垃圾对象的方法可以分为两种:引用计数 & 可达性分析。以判断方法为划分,后文所讲的垃圾回收算法也可以划分为 引用计数式 & 追踪式 两大类。

3.1 引用计数算法(Reference Counting)

3.1.1 判定方法

在分配对象时,会额外为对象分配一段空间,用于记录指向该对象的引用个数。如果有一个新的引用指向该对象,则计数器加 1;当一个引用不再指向该对象,则计数器减 1 。当计数器的值为 0 时,则该对象为垃圾对象。

3.1.2 优点

  • 1、及时性:当对象变成垃圾后,程序可以立刻感知,马上回收;而在可达性分析算法中,直到执行 GC 才能感知;
  • 2、最大暂停时间短:GC 可与应用交替运行。

3.1.3 缺点

  • 1、计数器值更新频繁:大多数情况下,对象的引用状态会频繁更新,更新计数器值的任务会变得繁重;
  • 2、堆利用率降低:计数器至少占用 32 位空间(取决于机器位数),导致堆的利用率降低;
  • 3、实现复杂;
  • 4、(致命缺陷)无法回收循环引用对象。

易错: 引用计数法是算法简单,实现较难。

3.2 可达性分析算法(Reachability Analysis)

3.2.1 判定方法

从 GC 根节点(GC Root)为起点,根据引用关系形成引用链。当一个对象存在到 GC Root 的引用链,则为存活对象,否则为垃圾对象。在 Java 中,GC Root 主要包括:

  • 1、Java 虚拟机栈中引用的对象(即栈帧中的本地变量表);
  • 2、本地方法栈中引用的对象;
  • 3、方法区中类静态变量引用的对象;
  • 4、方法区常量池中引用的对象;
  • 5、同步锁(synchronized 关键字)持有的对象;

3.2.2 优点

  • 1、可回收循环引用对象;
  • 2、实现简单。

3.2.3 缺点

  • 1、最大停顿时间长:在 GC 期间,整个应用停顿(stop-the-world,STW);
  • 2、回收不及时:直到执行 GC 才能感知垃圾对象;

3.3 小结

判定方法 优点 缺点
引用计数 1、及时性
2、最大暂停时间短
1、计数器值更新频繁
2、堆利用率降低
3、实现复杂
4、无法回收循环引用对象
可达性分析 1、可回收循环引用对象
2、实现简单
1、最大停顿时间长
2、回收不及时

由于引用计数式 GC 存在 「无法回收循环引用对象」 的致命缺陷,工业实现上还是追踪式 GC 占据了主流,后面我主要介绍的也是追踪式 GC。


4. 垃圾回收算法(回收的过程)

从原理上,垃圾回收算法可以分为以下四类基础算法,其它的垃圾回收算法其实是对基础算法的改进或组合。

时间 早期提出者 算法 类别
1960年 Lisp 之父 John McCarthy 标记 - 清理算法 追踪式
1960年 George E. Collins 引用计数算法 引用计数式
1969年 Fenichel 复制算法 追踪式
1974年 Edward Lueders 标记 - 整理算法 追踪式

在实践中,当代绝大多数垃圾收集器都采用了 “分代收集模型” ,该模型的经验前提是:

  • 1、绝大多数对象都是朝生夕死,无法熬过第一次垃圾回收;
  • 2、熬过了多次垃圾回收的对象,往往越难被回收。

在上述事实经验的基础上,虚拟机往往使用了 动静分离 的设计思想:将新对象和难以回收的老对象存储在不同的区域,新对象存放在新生代,难回收的对象存在老年代。并且针对不同区域的特性采用不同的垃圾回收算法。

—— 图片引用自网络

  • 1、新生代: 新生代中的对象存活率低,只要付出少量的复制成本就能完成回收过程,因此选用复制算法;

  • 2、老生代: 老生代中的对象存活率高,并且没有额外空间进行分配担保,因此选用 “标记 - 清理” 或 “标记 - 整理” 算法。

4.1 标记 - 清理算法(Mark-Sweep)

4.1.1 算法回收过程

标记 - 清理算法的回收过程主要分为两个阶段:

  • 标记(Mark)阶段: 遍历整个堆,标记出垃圾对象(也可以标记存活对象);

  • 清理(Sweep)阶段: 遍历整个堆,将垃圾对象分块链接空闲列表。

4.1.2 优点

实现简单;

4.1.3 缺点

  • 1、执行效率不稳定:Java 堆中对象越多,标记和清理的过程可能会越耗时;
  • 2、内存碎片化(fragmentation):回收过程会逐渐产生很多不连续的小内存,当小内存不足以分配对象内存时,又会触发一次垃圾回收动作(GC for Alloc)。

4.2 复制算法(Copying)

4.2.1 算法回收过程

复制算法的回收过程要点如下:

  • 1、将堆分为大小相同的两个空间:from 区和 to 区;
  • 2、对象的内存分配只使用 from 区,当 from 区占满时,将存活对象全部复制到 to 区;
  • 3、复制完成后互换 from 区和 to 区的指针。

—— 图片引用自 https://weread.qq.com/web/reader/3ee32e60717f5af83ee7b37k6a932c50311a6a9aeddf374 邓凡平 著

4.2.2 优点

  • 1、快速分配对象:空闲分块是一个连续内存空间,不需要向标记-清理算法那样遍历空闲列表;
  • 2、避免内存碎片化:存活对象和新分配对象都被压缩到 tospace 的一端,避免出现很多不连续的小内存。

4.2.3 缺点

  • 1、堆利用率低:把堆做二等分只能利用其中的一半,堆利用率最高仅为 50 %。

4.2.4 改进

  • 1、将新生代分为:一块 Eden 区和两块 Survivor 区,对应的比例为 8:1:1;
  • 2、对象只在 Eden 区分配,当 Eden 区占满后,将 Eden 区和 from Survivor 区的存活对象全部赋值到 to Survivor 区;
  • 3、复制完成后互换 from Survivor 区和 to Survivor 区的指针。

改进后堆利用率提升到最高 90%。

4.3 标记 - 整理算法(Mark-Compact)

4.3.1 算法回收过程

标记 - 清除算法与标记 - 整理算法的本质差异在于是否移动对象。标记 - 整理算法的回收过程主要分为两个阶段:

  • 标记(Mark)阶段: 遍历整个堆,标记出垃圾对象(这个步骤与标记 - 清理算法相同);

  • 整理(Compact)阶段: 将所有存活对象移动(压缩)到堆的一端,然后直接清理掉边界以外的内存。

4.3.2 优点

  • 1、避免内存碎片化,堆利用率高,吞吐量更高;
  • 2、快速分配对象:空闲分块是一个连续内存空间,不需要向标记-清理算法那样遍历空闲列表;

4.3.3 缺点

  • 1、移动对象比清理对象更耗时,导致 GC 停顿时间(Stop-the-world)时间更长。

5. 并发回收

5.1 stop-the-world 现象

在标准的垃圾回收算法中,在垃圾回收线程(collector)进行标记 - 清理 / 整理 / 复制的过程中需要暂停所有的用户线程(mutator),这是为了保证能够彻底清理所有垃圾对象。

但是这种做法却会导致虚拟机的吞吐量降低(吞吐量 = \frac{运行用户代码时间} {垃圾回收频率 * 单次垃圾回收时间})。

5.2 CMS 垃圾收集器

在追求响应速度的系统上,希望垃圾收集器暂停时间尽可能小,为此发展出了允许回收线程与用户线程并发运行的垃圾收集器 —— CMS(Concurrent Mark Sweep,并发标记清除)。

CMS 垃圾收集器的主要工作过程分为 4 个步骤:

  • 1、初始标记(短暂 stop-the-world): 仅仅标记被 GC Root 直接引用的对象,由于 GC Root 相对较少,这个过程速度很块;

  • 2、并发标记(耗时): 继续遍历 GC Root 引用链上的对象,这个过程比较耗时,所以采用并发处理;

  • 3、重新标记(短暂 stop-the-world): 为了修正并发标记期间用户线程导致的引用关系变化,需要暂停用户线程重新标记;

  • 4、并发清除(耗时) 由于清除对象的过程比较耗时,所以采用并发处理。

—— 图片引用自网络

5.3 CMS 的优点

  • 1、缩短了系统 stop-the-world 时间,提高了吞吐量;

5.4 CMS 的缺点

  • 1、CPU 敏感: 采用了并发策略,系统整体上会占用更多 CPU 资源;
  • 2、浮动垃圾: 由于并发清理的过程中用户线程还在运行,CMS 无法回收这个阶段中用户线程产生的垃圾,这一部分垃圾称为 “浮动垃圾”。由于浮动垃圾的存在,垃圾收集器需要预留出一部分空间来允许浮动垃圾的产生,如果预留的空间还不足以存放浮动垃圾,就会出现 Concurrent Mode Failure,此时需要临时启动非并发清理方案来代替 CMS;
  • 3、内存碎片: 采用标记 - 清理算法,会产生内存碎片。

6. 总结

  • 1、垃圾回收算法的性能指标主要有:吞吐量、最大暂停时间、堆利用率、访问局部性。在理解垃圾回收机制的过程中,可以带着 “回收的对象” & “回收的时机” & “回收的过程” 三个问题来理解;

  • 2、垃圾回收机制管理的区域有堆和方法区;

  • 3、判断垃圾对象的算法分为引用计数算法和可达性分析算法,两者各有优缺点;

  • 4、垃圾回收算法可以分为四类基本算法:引用计数算法、标记-清理算法、标记-整理算法和复制算法。其它的垃圾回收算法都是对基础算法的改进或组合。比如主流的虚拟机垃圾回收算法采用分代回收模型:即在新生代选用复制算法(对象存活率低),而老生代选用 “标记 - 清理” 或 “标记 - 整理” 算法(对象存活率高,并且没有额外空间进行分配担保);

  • 5、在标准的垃圾回收算法中,垃圾回收过程会 stop-the-world。使用并发收集可以降低系统的暂停时间,提供吞吐量。


参考资料


创作不易,你的「三连」是丑丑最大的动力,我们下次见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容