`

【转】JVM内存回收理论与实现

阅读更多
在本篇中,我们将继续探讨虚拟机自动内存管理系统的最重要一块职能:虚拟机如何对死亡的对象进行内存回收。

本篇里面,所有涉及到具体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)收集器为例,介绍内存回收的具体过程。

转载地址:http://www.infoq.com/cn/articles/jvm-memory-collection


  • 大小: 15.2 KB
分享到:
评论

相关推荐

    jvm内存管理,pdf

    ### JVM内存管理详解 #### 一、引言 在探讨JVM内存管理之前,我们先来看一下为何要深入了解这一主题。对于深入掌握Java的人来说,内存管理是不可或缺的一部分。随着技术的发展,内存管理变得越来越自动化,但这也...

    Java内存泄露及内存无法回收解决方案

    Java虚拟机(JVM)中有三个主要的内存区域:堆内存(Heap)、栈内存(Stack)和方法区(Method Area)。其中,堆内存是Java对象的主要存储场所,栈内存主要存储方法调用时的局部变量,而方法区则存储类的信息,如类...

    Jvm性能优化-JVM内存结构原理分析03

    "Jvm性能优化-JVM内存结构原理分析03" Jvm性能优化是Java虚拟机(JVM)中非常重要的一部分,它对Jvm的性能产生了很大的影响。本文将从Jvm内存结构的角度来分析Jvm性能优化的原理。 Jvm内存结构主要分为五部分:堆...

    jvm相关代码仓库,包括类加载机制,字节码执行机制,JVM内存模型,GC垃圾回收,JV-jvm_practice.zip

    JVM内存主要分为堆内存(Heap)、栈内存(Stack)、方法区(Method Area)、程序计数器(PC Register)和本地方法栈(Native Method Stack)。重点理解对象创建、内存分配以及栈帧的工作方式。GC(垃圾回收)与内存...

    JVM内存设置管理大全

    ### JVM内存管理详解 #### 一、JVM内存工作原理及优化设置 Java虚拟机(JVM)是Java程序运行的基础环境,在JVM中,内存管理是确保程序高效稳定运行的关键因素之一。理解JVM内存的工作原理及其优化设置,对于开发高...

    JVM调优视频理论及工具

    2. **JVM内存模型** - 堆内存:存储对象实例,分为新生代(Young Generation)和老年代(Tenured Generation),新生代又分为Eden区和两个Survivor区。 - 栈内存:每个线程都有独立的栈,用于存储方法调用信息,...

    JVM内存管理白皮书[借鉴].pdf

    在《JVM内存管理白皮书》中,深入探讨了JVM如何处理内存分配、垃圾收集以及各种收集器的工作原理。以下是对该白皮书部分内容的详细解读: 1. 显式与自动内存管理: 在传统的C++等语言中,程序员需要手动进行内存...

    JVM模拟内存泄漏代码

    元空间大小默认与物理内存关联,理论上不会溢出,但如果加载了大量的类,尤其是大量的动态生成的类,也可能导致元空间耗尽。 元空间泄漏的一种常见情况是过度使用动态代理或者反射。例如,不断地生成新的代理类或者...

    jvm调优实战经验

    总结,JVM调优是一项技术性极强的工作,需要深入理解JVM内存结构、垃圾回收机制,结合实际应用情况,通过调整相关参数实现最佳性能。实践中应结合理论知识与实践经验,不断测试、监控、分析,确保应用的高效稳定运行...

    JVM内存模型及垃圾收集策略解析

    在深入理解JVM内存模型与垃圾收集策略之前,我们首先需要知道JVM的主要组成部分:类装载器、运行数据区、执行引擎、本地方法接口和本地方法库。 1. **JVM内存模型** JVM内存主要分为以下几个区域: - **程序...

    JVM规范与深入理解

    书中的内容不仅包括理论知识,还有丰富的实践案例,帮助读者将理论与实际相结合,提升对JVM的理解。特别是对于内存模型的讲解,如堆内存、栈内存、方法区以及本地方法栈的运作机制,这些内容对于理解Java程序的运行...

    理论与实践结合 解密JVM-day01.rar

    《理论与实践结合:解密JVM》 Java虚拟机(JVM)是Java平台的核心组成部分,它负责执行Java程序并提供跨平台的运行环境。本资料“理论与实践结合 解密JVM-day01”将带你深入理解JVM的工作原理,通过理论讲解与实际...

    jdk,jvm源码

    Java虚拟机(JVM)是Java程序运行的核心,它负责解释和执行字节码,为Java应用程序提供了一个跨平台的运行环境。...通过结合理论知识与实际源码阅读,可以更好地掌握Java编程的精髓,提高解决复杂问题的能力。

    理论与实践结合 解密JVM-day04.rar

    在深入探讨JVM(Java虚拟机)的理论与实践结合时,我们首先需要理解JVM在Java编程中的核心地位。JVM是Java平台的核心组成部分,它负责执行编译后的Java字节码,使得Java程序具备跨平台的能力。在这个解密JVM-day04的...

    JVM原理.pdf

    JVM(Java Virtual Machine,Java虚拟机)...冯立全通过分享,将JVM原理的理论与实践相结合,为听众提供了一个全面了解JVM的机会。这对于需要深入探讨Java生态系统和提高Java开发技能的开发者来说,是非常宝贵的资源。

    理论与实践结合 解密JVM-day02.rar

    7. **JVM内存溢出** 内存溢出是JVM运行中常见的问题,如堆溢出(Heap Overflow)、栈溢出(Stack Overflow)等。理解不同类型的溢出以及如何处理它们是JVM实战中的重要技能。 8. **JVM字节码指令** 学习JVM字节码...

    实战Java虚拟机 JVM故障诊断与性能优化 葛一鸣

    1. **内存模型**:JVM内存分为堆内存、栈内存、方法区、本地方法栈和程序计数器五个部分。堆内存用于存储对象实例,栈内存则存储线程局部变量,方法区存储类信息,本地方法栈服务于JNI调用,程序计数器记录当前线程...

    JVM&g1gc;带书签,完整版本

    G1(Garbage-First)是Oracle JDK从1.6版本开始引入的一种垃圾收集器,设计目标是实现低延迟的内存回收,尤其适合大规模分布式系统。G1垃圾收集器采用了一种全新的内存区域划分方式,将Java堆划分为多个大小相等的...

    JVM高级特性与最佳实践(第2版)+pdf+源码

    3. **内存管理**:JVM内存分为堆、栈、方法区、本地方法栈和程序计数器五大部分。堆是对象的主要存储区域,栈用于存储方法调用,方法区存储类信息,本地方法栈服务于JNI调用,程序计数器记录下一条指令地址。理解各...

Global site tag (gtag.js) - Google Analytics