`
shixiaomu
  • 浏览: 384086 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

虚拟机对象探秘

 
阅读更多
在上一篇《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)收集器为例,介绍内存回收的具体过程。

参考资料

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

•http://icyfenix.iteye.com/blog/1095132
•http://xiao-feng.blogspot.com/2008/01/gc-safe-point-and-safe-region.html
•http://rednaxelafx.iteye.com/blog/1044951

--------------------------------------------------------------------------------
分享到:
评论

相关推荐

    HotSpot虚拟机对象探秘-xmind脑图pdf

    《HotSpot虚拟机对象探秘》是一份详细探讨Java虚拟机内部对象创建、内存布局以及访问定位的资源。这份资料采用Xmind脑图的形式,旨在帮助读者深入理解JVM的底层原理,尤其聚焦于HotSpot虚拟机。内容涵盖对象实例化、...

    dai147444612#JVM#HotSpot虚拟机对象探秘1

    对象的创建加载:先去检测new指令能否再常量池中定位到一个类的符号引用,如果未被加载、解析、初始化过 执行相应的类加载过程分配内存: 为对象分配空间时采用指针碰

    08-Java虚拟机(JVM)面试题-重点.docx

    Java 虚拟机(JVM)面试题-重点 Java 虚拟机(JVM)是 Java 语言的核心组件之一,它提供了一个运行 Java ByteCode 的环境,负责...对象的回收是 HotSpot 虚拟机对象探秘的第四步,负责回收 Java 对象占用的内存空间。

    学习笔记之java虚拟机

    #### HotSpot虚拟机对象探秘 - **对象创建**: - 新建对象时,首先检查new指令的参数是否能在常量池中找到对应的类符号引用,并确保该类已经被加载、解析和初始化。 - 分配内存的方式有两种:指针碰撞和空闲列表...

    Java虚拟机中对象探秘--对象头创建、对象头、对象锁、synchoronized底层实现.docx

    Java虚拟机(JVM)在创建和管理对象时涉及多个关键概念和技术,这些概念与对象的内存布局、对象头、对象锁以及`synchronized`关键字的底层实现密切相关。在JVM中,对象的创建过程分为几个步骤: 1. **类加载检查**:...

    Java虚拟机

    2.3 HotSpot虚拟机对象探秘 2.3.1 对象的创建 2.3.2 对象的内存布局 2.3.3 对象的访问定位 2.4 实战:OutOfMemoryError异常 2.4.1 Java堆溢出 2.4.2 虚拟机栈和本地方法栈溢出 2.4.3 方法区和运行时常量池...

    Java虚拟机技术手册.docx

    #### 八、HotSpot虚拟机对象探秘 - **对象创建**:在HotSpot虚拟机中对象是如何创建的。 - **对象布局**:对象在内存中的布局情况。 - **对象访问**:HotSpot虚拟机中对象的访问方式。 #### 九、硬件内存架构与Java...

    Java虚拟机(JVM)面试题(2022最新版)-重点

    #### HotSpot虚拟机对象探秘 **对象的创建:** 对象的创建涉及类加载、内存分配、初始化等多个步骤。类加载确保类的结构信息已经加载到方法区;内存分配为对象分配内存空间;初始化则对对象进行必要的设置,使其可用...

    java内存分配 .pdf

    #### 二、HotSpot虚拟机对象探秘 在深入了解Java内存区域的同时,我们还需要关注Java对象在内存中的布局和访问方式。 ##### 2.1 对象的创建 对象创建时,首先会在内存中为其分配空间,然后初始化对象的状态。对象...

    JVM知识点思维导图版

    JVM详细的知识点总结,思维导图 1.类加载器子系统 2.Hotspot的内存详情 3.HotSpot虚拟机对象探秘 4.垃圾收集器

    最新大厂Java面试题(上).pdf

    #### HotSpot虚拟机对象探秘 HotSpot虚拟机的对象探秘涉及对象的创建、布局和访问定位。对象在JVM中创建,首先通过类加载器加载相应的类信息到方法区,然后在Java堆中分配内存,之后进行对象初始化。 #### 内存...

    NET探秘:MSIL权威指南

    MSIL代码包含一系列操作码(OpCodes),每个操作码代表一个特定的操作,如加载常量、调用方法或创建对象。 MSIL的优势在于其可验证性和安全性。由于MSIL是高级的、易于分析的代码,CLR可以执行安全性检查,确保代码...

    SWT深入内幕之消息机制探秘

    ### SWT深入内幕之消息机制探秘 #### 一、引言 SWT(Standard Widget Toolkit)作为一种基于Java的用户界面工具包,旨在提供高效的、原生外观的图形用户界面组件。SWT的设计目标之一就是在Java虚拟机(JVM)环境下尽...

    [探秘Java:如何像计算机科学家一样思考].(唐尼).张平.扫描版.pdf

    读者会了解到Java虚拟机(JVM)如何自动管理内存,以及对象生命周期的概念,这有助于优化程序性能和防止内存泄漏。 并发编程是现代软件开发中的重要一环。Java提供了丰富的并发工具,如线程、同步机制(如...

    【JVM】类的奇幻漂流——类加载机制探秘

    【JVM】类的奇幻漂流——类加载机制探秘 Java虚拟机(JVM)是运行Java程序的核心组件,它负责将我们编写的类加载到内存中并执行。类加载机制是JVM的一个重要组成部分,它确保了程序的正常运行。本文将带你深入理解...

Global site tag (gtag.js) - Google Analytics