速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

JVM 内存回收理论与实现

  • 2011-09-05
  • 本文字数:3634 字

    阅读完需:约 12 分钟

在上一篇《HotSpot 虚拟机对象探秘》中,我们讨论了在 HotSpot 里对象是如何创建的、有怎样的内存布局、如何查找和使用。在本篇中,我们将继续探讨虚拟机自动内存管理系统的最重要一块职能:虚拟机如何对死亡的对象进行内存回收。

本篇里面,所有涉及到具体 JVM 实现的内容,仍然默认为基于 HotSpot 虚拟机的实现,后文不再单独说明。

对象存活的判定

当一个对象不会再被使用的时候,我们会说这对象已经死亡。对象何时死亡,写程序的人应当是最清楚的。如果计算机也要弄清楚这件事情,就需要使用一些方法来进行对象存活判定,常见的方法有引用计数(Reference Counting)有可达性分析(Reachability Analysis)两种。

引用计数算法的大致思想是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。它的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软 COM(Component Object Model)技术、使用 ActionScript 3 的 FlashPlayer、Python 语言和在游戏脚本领域得到许多应用的 Squirrel 中都使用了引用计数算法进行内存管理。但是,至少 Java 语言里面没有选用引用计数算法来管理内存,其中最主要原因是它没有一个优雅的方案去对象之间相互循环引用的问题:当两个对象互相引用,即使它们都无法被外界使用时,它们的引用计数器也不会为 0。

许多主流程序语言中(如 Java、C#、Lisp),都是使用可达性分析来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为 GC 根节点(GC Roots)的对象作为起始点,从这些节点开始进行向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。如图 1 所示,对象 object 5、object 6、object 7 虽然互相有关联,它们的引用并不为 0,但是它们到 GC Roots 是不可达的,因此它们将会被判定为是可回收的对象。

图 1 可达性分析算法判定对象是否可回收

枚举根节点

在 Java 语言里面,可作为 GC Roots 的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。如果要使用可达性分析来判断内存是否可回收的,那分析工作必须在一个能保障一致性的快照中进行——这里“一致性”的意思是整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中,对象引用关系还在不断变化的情况,这点不满足的话分析结果准确性就无法保证。这点也是导致 GC 进行时必须“Stop The World”的其中一个重要原因,即使是号称(几乎)不会发生停顿的 CMS 收集器中,枚举根节点时也是必须要停顿的。

由于目前的主流 JVM 使用的都是准确式 GC(这个概念在第一篇中介绍过),所以当执行系统停顿下来之后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用。在 HotSpot 的实现中,是使用一组成为 OopMap 的数据结构来达到这个目的,在类加载完成的时候,HotSpot 就把对象内什么偏移量上是什么类型的数据计算出来,在 JIT 编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样 GC 在扫描时就就可以直接得知这些信息了。下面的代码清单 1 是 HotSpot Client VM 生成的一段 String.hashCode() 方法的本地代码,可以看到在 0x026eb7a9 处的 call 指令有 OopMap 记录,它指明了 EBX 寄存器和栈中偏移量为 16 的内存区域中各有一个普通对象指针(Ordinary Object Pointer)的引用,有效范围为从 call 指令开始直到 0x026eb730(指令流的起始位置)+142(OopMap 记录的偏移量)=0x026eb7be,即 hlt 指令为止。

代码清单 1 String.hashCode() 方法的编译后的本地代码

复制代码
[Verified Entry Point]
0x026eb730: mov %eax,-0x8000(%esp)
…………
;; ImplicitNullCheckStub slow case
0x026eb7a9: call 0x026e83e0 ; OopMap{ebx=Oop [16]=Oop off=142}
;*caload
; - java.lang.String::hashCode@48 (line 1489)
; {runtime_call}
0x026eb7ae: push $0x83c5c18 ; {external_word}
0x026eb7b3: call 0x026eb7b8
0x026eb7b8: pusha
0x026eb7b9: call 0x0822bec0 ; {runtime_call}
0x026eb7be: hlt

安全点

在 OopMap 的协助下,HotSpot 可以快速准确地地完成 GC Roots 枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap,那将会需要大量的额外空间,这样 GC 的空间成本将会变得很高。

实际上 HotSpot 也的确没有为每条指令都生成 OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置被称为安全点(Safepoint),即程序执行时并非在所有的地方都能停顿下来开始 GC,只有在到达安全点时才能暂停。Safepoint 的选定既不能太少以至于让 GC 等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。所以安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生 Safepoint。

对于 Sefepoint,另外一个需要考虑的问题是如何让 GC 发生时,让所有线程(这里不包括执行 JNI 调用的线程)都跑到最近的安全点上再停顿下来。我们有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension),抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应 GC 事件。

而主动式中断的思想是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。下面的代码清单 2 中的 test 指令是 HotSpot 生成的轮询指令,当需要暂停线程时,虚拟机把 0x160100 的内存页设置为不可读,那线程执行到 test 指令时就会停顿等待,这样一条指令便完成线程中断了。

代码清单 2 轮询指令

复制代码
0x01b6d627: call 0x01b2b210 ; OopMap{[60]=Oop off=460}
;*invokeinterface size
; - Client1::main@113 (line 23)
; {virtual_call}
0x01b6d62c: nop ; OopMap{[60]=Oop off=461}
;*if_icmplt
; - Client1::main@118 (line 23)
0x01b6d62d: test %eax,0x160100 ; {poll}
0x01b6d633: mov 0x50(%esp),%esi
0x01b6d637: cmp %eax,%esi

安全区域

使用 Safepoint 似乎已经完美解决如何进入 GC 的问题了,但实际情况却并不一定。Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配 CPU 时间,典型的例子就是线程处于 Sleep 状态或者 Blocked 状态,这时候线程无法响应 JVM 的中断请求,走到安全的地方去中断挂起,JVM 也显然不太可能等待线程重新被分配 CPU 时间。对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中任意地方开始 GC 都是安全的。我们也可以把 Safe Region 看作是被扩展了的 Safepoint。

在线程执行到 Safe Region 里面的代码时,首先标识自己已经进入了 Safe Region,那样当这段时间里 JVM 要发起 GC,就不用管标识自己为 Safe Region 状态的线程了。在线程要离开 Safe Region 时,它要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开 Safe Region 的信号为止。

到这里,我们简单介绍了虚拟机如何去发起内存回收的问题,但是虚拟机如何具体地进行内存回收动作仍然未涉及到。因为内存回收如何进行是由虚拟机所采用的 GC 收集器所决定的,而通常虚拟机中往往不止有一种 GC 收集器,像目前(JDK 7 时代)的 HotSpot 里面就包含有 Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、Concurrent Mark Sweep 和 Garbage First 七种收集器,在下一篇中,我们将以最新最先进的 Garbage First(G1)收集器为例,介绍内存回收的具体过程。

参考资料

本文撰写时主要参考了以下资料:


感谢张凯峰对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011-09-05 00:0015495

评论

发布
暂无评论
发现更多内容

Moblink节省开发者时间精力和公司成本

MobTech袤博科技

智慧公厕管理系统哪家好?

光明源智慧厕所

智慧城市

强大的克隆备份软件:EaseUS Todo Backup直装激活版

真大的脸盆

Mac Mac 软件 备份软件 克隆工具

ZBC新一轮流动性收益计划迎来新通缩,APR高达100%

股市老人

前阿里架构师离职带出的Spring Security实战开发学习手册震撼开源

开心学Java

Java aop ioc springsecurity 框架

DPU 厂商北中网芯加入龙蜥社区,共建网络通信与安全

OpenAnolis小助手

开源 龙蜥社区 DPU CLA 北中网芯

机器学习算法(四): 基于支持向量机的分类预测

汀丶人工智能

数据挖掘 机器学习 SVM

华为云低代码平台Astro|通过零代码快速搭建打卡小程序

路过的憨憨

灵魂一问:SELECT COUNT(*) 会造成全表扫描吗?

Java你猿哥

Java MySQL sql ssm

入选Gartner低代码魔力象限 |『华为云Astro』低调的背后

路过的憨憨

通信系统综合仿真

timerring

通信系统 通信系统仿真

RocketMQ源码-broker 消息接收流程(写入commitLog)

小小怪下士

Java 程序员 RocketMQ 后端 消息中间件

ChatGPT王炸更新!能联网获取新知识,可与5000+个应用交互,网友:太疯狂了

Openlab_cosmoplat

工业互联网 开源社区 智能制造 ChatGPT

软件测试/测试开发丨app自动化测试之Andriod微信小程序的自动化测试

测试人

微信小程序 软件测试 自动化测试 测试开发

【3ds MAX 插件】近期发布的几个小插件,总有一款适合你

Finovy Cloud

插件 3ds Max

上传iOS应用变得更加容易 - 在Windows上架iOS APP的工具介绍

雪奈椰子

性能最大提升60%,阿里云第八代企业级实例ECSg8i正式上线

云布道师

阿里云 ECS

点击量破百万!阿里内产微服务进阶讲义,简直是Java开发者的福音

Java你猿哥

Java 面试 面经 Java工程师

旺链科技荣获“高新技术企业证书”殊荣

旺链科技

区块链 区块链+ 高新技术企业

SRE是什么,与传统运维有什么不同?

不思jo

SRE #运维

某头部零售集团的数据云平台“多租户安全”实践 | 奇点云技术分享

奇点云

数据安全 多租户技术 奇点云

OpenHarmony关系型数据库[1]

白晓明

关系型数据库 OpenHarmony

九科企业级超级自动化平台引入ChatGPT,新技术助推产品能力全面提升

九科Ninetech

PCB生产工艺 | 第九道主流程之表面处理

华秋电子

网心科技荣获“深圳市自主创新百强中小企业”称号

网心科技

专精特新

安如泰山 华为云发布制品仓库CodeArts Artifact

路过的憨憨

开心档之Go 语言环境安装

雪奈椰子

ChatGPT编程秀-3:适合面向ChatGPT编程的架构

仝键

JavaScript 架构 java ChatGPT

分布式日志系统的设计和实践

Java你猿哥

Java 分布式 后端 分布式日志

通透!阿里P8撰写《深入解析Java虚拟机HotSpot 》让我涨薪70%

Java你猿哥

Java 后端 Java虚拟机 jvm调优

JVM内存回收理论与实现_Java_周志明_InfoQ精选文章