- 浏览: 3053594 次
- 性别:
- 来自: 海外
文章分类
- 全部博客 (430)
- Programming Languages (23)
- Compiler (20)
- Virtual Machine (57)
- Garbage Collection (4)
- HotSpot VM (26)
- Mono (2)
- SSCLI Rotor (1)
- Harmony (0)
- DLR (19)
- Ruby (28)
- C# (38)
- F# (3)
- Haskell (0)
- Scheme (1)
- Regular Expression (5)
- Python (4)
- ECMAScript (2)
- JavaScript (18)
- ActionScript (7)
- Squirrel (2)
- C (6)
- C++ (10)
- D (2)
- .NET (13)
- Java (86)
- Scala (1)
- Groovy (3)
- Optimization (6)
- Data Structure and Algorithm (3)
- Books (4)
- WPF (1)
- Game Engines (7)
- 吉里吉里 (12)
- UML (1)
- Reverse Engineering (11)
- NSIS (4)
- Utilities (3)
- Design Patterns (1)
- Visual Studio (9)
- Windows 7 (3)
- x86 Assembler (1)
- Android (2)
- School Assignment / Test (6)
- Anti-virus (1)
- REST (1)
- Profiling (1)
- misc (39)
- NetOA (12)
- rant (6)
- anime (5)
- Links (12)
- CLR (7)
- GC (1)
- OpenJDK (2)
- JVM (4)
- KVM (0)
- Rhino (1)
- LINQ (2)
- JScript (0)
- Nashorn (0)
- Dalvik (1)
- DTrace (0)
- LLVM (0)
- MSIL (0)
最新评论
-
mldxs:
虽然很多还是看不懂,写的很好!
虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 -
HanyuKing:
Java的多维数组 -
funnyone:
Java 8的default method与method resolution -
ljs_nogard:
Xamarin workbook - .Net Core 中不 ...
LINQ的恶搞…… -
txm119161336:
allocatestlye1 顺序为 // Fields o ...
最近做的两次Java/JVM分享的概要
继续打捞一些零散的文字。这次是ItEye站内信里的一段对话。
下面是问题和回复内容,带补充。
===================================================================
问题
月初的时候收到这样一个问题:
===================================================================
简要回答
===================================================================
实现例子:Oracle/Sun HotSpot VM
Sun HotSpot VM从设计之初就使用准确式GC。
在HotSpot VM之前,Sun在1.0.x到1.2.x中提供的JVM(后来称为Classic VM)用的是半保守式的设计。在SPARC版JDK 1.2.1和1.2.2中提供的EVM(也称为ExactVM)则实现了准确式GC。
后面举的一些例子也都显示,高性能VM实现更倾向使用准确式GC;即便一开始为了快速开发使用了半保守式GC,后续发展为了追求性能也会渐渐朝着准确式的方向发展。
简要回答里已经提到了HotSpot的实现方式。下面引用几篇论文分别看看HotSpot的client编译器(JDK6)与server编译器对OopMap / safepoint的支持。
Design of the Java HotSpot Client Compiler for Java 6
A Compiler for the Java HotSpot Virtual Machine (描述的是2000年JDK 1.3.0里的HotSpot Client Compiler)
The Java HotSpot Server Compiler
在实际代码中,生成OopMap的逻辑在Compile::BuildOopMaps()实现(hotspot/src/share/vm/opto/buildOopMap.cpp)
-------------------------------------------------------------------
我的草稿箱里有一些关于HotSpot的client和server编译器分别如何生成OopMap的笔记,不过还没整理完所以暂时就不放出来了。不过各位同学如果对此感兴趣的话,可以从-XX:+PrintAssembly输出的日志入手来了解OopMap会关联在哪些位置上,里面的内容有些什么。知道了如何解读OopMap的信息,就能很方便的知道一个被JIT编译过的方法会如何跟GC交互了,某些局部变量是否需要显式置null也会一目了然。
例如说,
这段输出含有一条x86-64的callq指令,用来实现对intern()方法的调用。在后面的注释当中可以看到:
它的意思是:
有一个OopMap与这条callq指令之后的一条指令(inc %ebp)关联在一起;
在该指令位置上只有一个活跃的引用,在栈顶上。
这是怎么解读的呢?
OopMap记录输出的日志的构成是:
在这个例子中,
[0]表示栈顶指针+偏移量0,这里就是[rsp + 0],也就是栈顶;右边的"=Oop"说明这个位置存着一个普通对象指针(ordinary object pointer,HotSpot将指向GC堆中对象开头位置的指针称为Oop)。
除了Oop之外,可能出现的类型有:
hotspot/src/share/vm/compiler/oopMap.hpp
对应的显示为:
hotspot/src/share/vm/compiler/oopMap.cpp
off=88就是这个OopMap记录关联的指令在方法的指令流中的偏移量,这个数字是十进制的。
可以看到,该方法的指令流是从地址0x00007f3749ea3400开始的;十进制的88就是十六进制的0x58;
0x00007f3749ea3400 + 0x58 = 0x00007f3749ea3458,正好就是例子中callq指令后的inc %ebp所在的位置。
-------------------------------------------------------------------
再举几个例子。
这个记录比较简单。它说明在该方法的指令流中,有一个OopMap与偏移量228的位置上的指令关联在一起。
该OopMap显示这个位置上没有任何活着的引用。
在偏移量为144的指令上关联了一个OopMap的记录,有一个活跃的引用在寄存器RBP里。
在偏移量为248的指令上关联了一个OopMap的记录,有一个活跃的引用在寄存器RBP里,并且这个引用是压缩过的(NarrowOop)。
这个OopMap记录看起来比较壮观。其实是它关联的指令位置上正好所有的callee-save的寄存器都保存到栈上了而已。例如说,在[栈顶+296]位置上保存的是调用方的eax;至于它到底是不是一个Oop就得看调用方对应的OopMap是怎么说的了。
这里可以看到derived oop类型的数据,在栈上[28]位置的是由栈上[24]开始的一个对象的派生引用。
===================================================================
实现例子:IBM Sovereign JVM
IBM在JDK 5之前主要提供的JVM是Sovereign VM,里面的GC是半保守式的(栈上保守,堆上准确);而从JDK 5开始主要提供J9 VM,里面的GC转为准确式。
关于IBM DK for Java 1.4.1中的Sovereign JVM所使用的半保守式GC设计,可以参考下面这段文字:
IBM JVM Garbage Collection and Storage Allocation techniques
===================================================================
实现例子:Mono
(不熟悉Mono的同学们:Mono不是一种JVM实现,而是一种CLI实现。不过CLI与JVM有许多相似之处,所以顺便拿Mono来举例)
早期版本的Mono采用Boehm GC,使用的是半保守模式。可以参考以下文档的描述:
Mono's use of Boehm GC
从Mono 2.8开始,一种名为SGen的新GC实现被包含在发布包中。但这个版本的SGen仍然是半保守式GC。
未来Mono将逐渐转为使用完全准确式的GC。这篇文档描述了设计思路:Precise Stack Marking
===================================================================
进一步阅读
Finding References in Java Stacks
1997年Sun Labs的一篇论文。主要讲解了在Java的栈里找出引用的特点、问题与解决思路,特别是与jsr带来的问题。其中前三章概括介绍了保守式GC与准确式GC的概念、准确式GC涉及的元数据(stack map)、生成stack map的方法等内容。
GC Points in a Threaded Environment
1998年Sun Labs的一篇论文。讲解了多线程条件下允许GC发生的位置,所谓的“GC point”,也称为“safepoint”。
Oracle JRockit: The Definitive Guide 第三章,84-87页,Livemaps
介绍了JRockit的GC是如何找到活动记录中的引用的。JRockit的livemap也是由JIT编译器生成出来的。
Mostly Accurate Stack Scanning
2001年IBM出的一篇论文。讲解了几种半保守式扫描栈的方法。
Support for Garbage Collection at Every Instruction in a Java Compiler
1999年Intel出的一篇论文。它的论点是通过采用压缩技术,即便为每条指令都生成GC map,消耗的空间也可以接受。
虽然如此,但现在主流的VM里没有一个是会为每条指令都生成GC map的;相反,多数是不但只在safepoint才生成GC map,而且平时还将GC map压缩起来放在内存里。
Accurate Garbage Collection in Uncooperative Environments Revisited
2002年的一篇论文,描述了Ovm是如何在缺乏编译器支持的情况下实现准确式GC。Ovm是一种比较特别的JVM实现,它先把Java字节码翻译为C/C++,然后再用GCC之类的C/C++编译器生成最终的代码。本来Java比较容易实现完全准确的GC,但经过中间C/C++这层事情就变得复杂了。
Runtime Tags Aren't Necessary
1988年Andrew W. Appel(虎书作者)写的一篇论文。它提出了在ML语言的静态多态类型系统的支持下,GC可以怎样在不使用tag的前提下将引用与非引用区分开。
Accurate Garbage Collection with LLVM
编译器框架LLVM的官方文档,介绍了如何将LLVM与准确式GC结合起来。后面这个演示稿介绍了VMKit是如何将MMTk与LLVM整合在一起实现准确式GC的:Precise and Efficient Garbage Collection in VMKit with MMTk。
Ruby Hacking Guide,第五章 垃圾收集,is_pointer_to_heap()
原版(日文)
ItEye专栏里的中文翻译
介绍了CRuby是如何区分一个数据是不是指向GC堆中的指针。CRuby的GC也是属于半保守式的。
ガベージコレクションのアルゴリズムと実装
介绍GC的算法与实现的一本书。其中11.5与12.4小节分别介绍了Rubinius与V8的准确式GC的一些设计。
Shared Source CLI Essentials, 249-250页, Scheduling Collection
Shared Source CLI 2.0 Internals, 253-254页, Scheduling Collection
介绍SSCLI的书的第一版与第二版(第二版是免费的电子版,点上面的链接可以下载到)。
这本书在之前一帖里介绍过,强力推荐对VM的实现感兴趣的同学阅读。前提是能接受SSCLI License。
上面提到的章节里介绍了SSCLI的safepoint与GC map。相关代码可以参考:
sscli20\sscli20\clr\src\vm\fjit_eetwain.cpp
sscli20\sscli20\clr\src\fjit\fjitencode.cpp
既然都提到了Mono和SSCLI,那顺带也提一下微软的CLI实现,CLR的玩法吧。
MSDN: SOS.dll (SOS Debugging Extension)
这篇MSDN文档介绍了随CLR一起发布的SOS扩展的命令。其中,
可以查看托管方法的栈帧上局部变量的状态。这个命令显示的信息就是由CLR中JIT编译器生成的GC map所提供的。下面是例子
不是,什么代码都不是。它只是!CLRStack这个命令显示的结果,内容是线程的调用栈的状况。可以把这个输出分解来看:
这是线程ID
这是格式说明,告诉用户下面的两个数字是什么意思。
这是这个调用栈最顶上的一个栈帧,它的栈顶指针(ESP)值是0x0012f3cc,当前执行到的指令地址(EIP)是0x00de0136。这个栈帧对应的方法是ConsoleApplication16.Program.ImportString(System.String[,])。
该栈帧里的局部变量的状态。等号左手边的是地址,右手边的是在这个地址里存的值。
可以看到这个例子里有9个被记录下来的局部变量是在栈上的,有1个是在寄存器里的。(具体是哪个寄存器这里没写。我印象中这个是ESI…不太肯定。
弱弱的问一下,这鞋是什么代码,汇编吗?
下面是问题和回复内容,带补充。
===================================================================
问题
月初的时候收到这样一个问题:
同学F 写道
请教下,识jvm堆栈中一个数据类型是否为为引用类型,目前虚拟机实现中是如何做的?
===================================================================
简要回答
RednaxelaFX 写道
嘿嘿,这个问题估计很多人都有疑问,如果不介意的话能搬到高级语言虚拟机圈子去开帖问么?
调用栈里的引用类型数据是GC的根集合(root set)的重要组成部分;找出栈上的引用是GC的根枚举(root enumeration)中不可或缺的一环。
==========================================
要看JVM选择用什么方式。通常这个选择会影响到GC的实现。
如果JVM选择不记录任何这种类型的数据,那么它就无法区分内存里某个位置上的数据到底应该解读为引用类型还是整型还是别的什么。这种条件下,实现出来的GC就会是“保守式GC(conservative GC)”。在进行GC的时候,JVM开始从一些已知位置(例如说JVM栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。然后递归的这么扫描出去。
保守式GC的好处是相对来说实现简单些,而且可以方便的用在对GC没有特别支持的编程语言里提供自动内存管理功能。Boehm-Demers-Weiser GC是保守式GC中的典型代表,可以嵌入到C或C++等语言写的程序中。
小历史故事:
微软的JScript和早期版VBScript也是用保守式GC的;微软的JVM也是。VBScript后来改回用引用计数了。而微软JVM的后代,也就是.NET里的CLR,则改用了完全准确式GC。
为了赶上在一个会议上发布消息,微软最初的JVM原型只有一个月左右的时间从开工到达到符合Java标准。所以只好先用简单的办法来实现,也就自然选用了保守式GC。
信息来源:Patrick Dussud在Channel 9的访谈,23分钟左右
保守式GC的缺点有:
1、会有部分对象本来应该已经死了,但有疑似指针指向它们,使它们逃过GC的收集。这对程序语义来说是安全的,因为所有应该活着的对象都会是活的;但对内存占用量来说就不是件好事,总会有一些已经不需要的数据还占用着GC堆空间。具体实现可以通过一些调节来让这种无用对象的比例少一些,可以缓解(但不能根治)内存占用量大的问题。
2、由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法可以在使用保守式GC的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。但是这样的话引用的访问速度就降低了。Sun JDK的Classic VM用过这种全handle的设计,但效果实在算不上好。
由于JVM要支持丰富的反射功能,本来就需要让对象能了解自身的结构,而这种信息GC也可以利用上,所以很少有JVM会用完全保守式的GC。除非真的是特别懒…
------------------------------------------
JVM可以选择在栈上不记录类型信息,而在对象上记录类型信息。这样的话,扫描栈的时候仍然会跟上面说的过程一样,但扫描到GC堆内的对象时因为对象带有足够类型信息了,JVM就能够判断出在该对象内什么位置的数据是引用类型了。这种是“半保守式GC”,也称为“根上保守(conservative with respect to the roots)”。
为了支持半保守式GC,运行时需要在对象上带有足够的元数据。如果是JVM的话,这些数据可能在类加载器或者对象模型的模块里计算得到,但不需要JIT编译器的特别支持。
前面提到了Boehm GC,实际上它不但支持完全保守的方式,也可以支持半保守的方式。GCJ和Mono都是以半保守方式使用Boehm GC的例子。
Google Android的Dalvik VM的早期版本也是使用半保守式GC的一个例子。不过到2009年中的时候Dalvik VM的内部版本就已经开始支持准确式GC了——代价是优化过的DEX文件的体积膨胀了约9%。
其实许多较老的JVM都选择这种实现方式。
由于半保守式GC在堆内部的数据是准确的,所以它可以在直接使用指针来实现引用的条件下支持部分对象的移动,方法是只将保守扫描能直接扫到的对象设置为不可移动(pinned),而从它们出发再扫描到的对象就可以移动了。
完全保守的GC通常使用不移动对象的算法,例如mark-sweep。半保守方式的GC既可以使用mark-sweep,也可以使用移动部分对象的算法,例如Bartlett风格的mostly-copying GC。
半保守式GC对JNI方法调用的支持会比较容易:管它是不是JNI方法调用,是栈都扫过去…完事了。不需要对引用做任何额外的处理。当然代价跟完全保守式一样,会有“疑似指针”的问题。
------------------------------------------
与保守式GC相对的是“准确式GC”,原文可以是precise GC、exact GC、accurate GC或者type accurate GC。外国人也挺麻烦的,“准确”都统一不到一个词上⋯
是什么东西“准确”呢?关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才可以合理地解读数据的含义;GC所关心的含义就是“这块数据是不是指针”。
要实现这样的GC,JVM就要能够判断出所有位置上的数据是不是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。
有几种办法:
1、让数据自身带上标记(tag)。这种做法在JVM里不常见,但在别的一些语言实现里有体现。就不详细介绍了。打标记的方式在半保守式GC中倒是更常见一些,例如CRuby就是用打标记的半保守式GC。CLDC-HI比较有趣,栈上对每个slot都配对一个字长的tag来说明它的类型,通过这种方式来减少stack map的开销;类似的实现在别的地方没怎么见过,大家一般都不这么取舍。
2、让编译器为每个方法生成特别的扫描代码。我还没见过JVM实现里这么做的,虽说在别的语言实现里有见过。
3、从外部记录下类型信息,存成映射表。现在三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样做的。其中,HotSpot把这样的数据结构叫做OopMap,JRockit里叫做livemap,J9里叫做GC map。Apache Harmony的DRLVM也把它叫GCMap。
要实现这种功能,需要虚拟机里的解释器和JIT编译器都有相应的支持,由它们来生成足够的元数据提供给GC。
使用这样的映射表一般有两种方式:
1、每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
2、为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。
在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。
每个被JIT编译过后的方法也会在一些特定的位置记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了。这些特定的位置主要在:
1、循环的末尾
2、方法临返回前 / 调用方法的call指令后
3、可能抛异常的位置
这种位置被称为“安全点”(safepoint)。之所以要选择一些特定的位置来记录OopMap,是因为如果对每条指令(的位置)都记录OopMap的话,这些记录就会比较大,那么空间开销会显得不值得。选用一些比较关键的点来记录就能有效的缩小需要记录的数据量,但仍然能达到区分引用的目的。因为这样,HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。
而仍然在解释器中执行的方法则可以通过解释器里的功能自动生成出OopMap出来给GC用。
平时这些OopMap都是压缩了存在内存里的;在GC的时候才按需解压出来使用。
HotSpot是用“解释式”的方式来使用OopMap的,每次都循环变量里面的项来扫描对应的偏移量。
对Java线程中的JNI方法,它们既不是由JVM里的解释器执行的,也不是由JVM的JIT编译器生成的,所以会缺少OopMap信息。那么GC碰到这样的栈帧该如何维持准确性呢?
HotSpot的解决方法是:所有经过JNI调用边界(调用JNI方法传入的参数、从JNI方法传回的返回值)的引用都必须用“句柄”(handle)包装起来。JNI需要调用Java API的时候也必须自己用句柄包装指针。在这种实现中,JNI方法里写的“jobject”实际上不是直接指向对象的指针,而是先指向一个句柄,通过句柄才能间接访问到对象。这样在扫描到JNI方法的时候就不需要扫描它的栈帧了——只要扫描句柄表就可以得到所有从JNI方法能访问到的GC堆里的对象。
但这也就意味着调用JNI方法会有句柄的包装/拆包装的开销,是导致JNI方法的调用比较慢的原因之一。
调用栈里的引用类型数据是GC的根集合(root set)的重要组成部分;找出栈上的引用是GC的根枚举(root enumeration)中不可或缺的一环。
==========================================
要看JVM选择用什么方式。通常这个选择会影响到GC的实现。
如果JVM选择不记录任何这种类型的数据,那么它就无法区分内存里某个位置上的数据到底应该解读为引用类型还是整型还是别的什么。这种条件下,实现出来的GC就会是“保守式GC(conservative GC)”。在进行GC的时候,JVM开始从一些已知位置(例如说JVM栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。然后递归的这么扫描出去。
保守式GC的好处是相对来说实现简单些,而且可以方便的用在对GC没有特别支持的编程语言里提供自动内存管理功能。Boehm-Demers-Weiser GC是保守式GC中的典型代表,可以嵌入到C或C++等语言写的程序中。
小历史故事:
微软的JScript和早期版VBScript也是用保守式GC的;微软的JVM也是。VBScript后来改回用引用计数了。而微软JVM的后代,也就是.NET里的CLR,则改用了完全准确式GC。
为了赶上在一个会议上发布消息,微软最初的JVM原型只有一个月左右的时间从开工到达到符合Java标准。所以只好先用简单的办法来实现,也就自然选用了保守式GC。
信息来源:Patrick Dussud在Channel 9的访谈,23分钟左右
保守式GC的缺点有:
1、会有部分对象本来应该已经死了,但有疑似指针指向它们,使它们逃过GC的收集。这对程序语义来说是安全的,因为所有应该活着的对象都会是活的;但对内存占用量来说就不是件好事,总会有一些已经不需要的数据还占用着GC堆空间。具体实现可以通过一些调节来让这种无用对象的比例少一些,可以缓解(但不能根治)内存占用量大的问题。
2、由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法可以在使用保守式GC的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。但是这样的话引用的访问速度就降低了。Sun JDK的Classic VM用过这种全handle的设计,但效果实在算不上好。
由于JVM要支持丰富的反射功能,本来就需要让对象能了解自身的结构,而这种信息GC也可以利用上,所以很少有JVM会用完全保守式的GC。除非真的是特别懒…
------------------------------------------
JVM可以选择在栈上不记录类型信息,而在对象上记录类型信息。这样的话,扫描栈的时候仍然会跟上面说的过程一样,但扫描到GC堆内的对象时因为对象带有足够类型信息了,JVM就能够判断出在该对象内什么位置的数据是引用类型了。这种是“半保守式GC”,也称为“根上保守(conservative with respect to the roots)”。
为了支持半保守式GC,运行时需要在对象上带有足够的元数据。如果是JVM的话,这些数据可能在类加载器或者对象模型的模块里计算得到,但不需要JIT编译器的特别支持。
前面提到了Boehm GC,实际上它不但支持完全保守的方式,也可以支持半保守的方式。GCJ和Mono都是以半保守方式使用Boehm GC的例子。
Google Android的Dalvik VM的早期版本也是使用半保守式GC的一个例子。不过到2009年中的时候Dalvik VM的内部版本就已经开始支持准确式GC了——代价是优化过的DEX文件的体积膨胀了约9%。
其实许多较老的JVM都选择这种实现方式。
由于半保守式GC在堆内部的数据是准确的,所以它可以在直接使用指针来实现引用的条件下支持部分对象的移动,方法是只将保守扫描能直接扫到的对象设置为不可移动(pinned),而从它们出发再扫描到的对象就可以移动了。
完全保守的GC通常使用不移动对象的算法,例如mark-sweep。半保守方式的GC既可以使用mark-sweep,也可以使用移动部分对象的算法,例如Bartlett风格的mostly-copying GC。
半保守式GC对JNI方法调用的支持会比较容易:管它是不是JNI方法调用,是栈都扫过去…完事了。不需要对引用做任何额外的处理。当然代价跟完全保守式一样,会有“疑似指针”的问题。
------------------------------------------
与保守式GC相对的是“准确式GC”,原文可以是precise GC、exact GC、accurate GC或者type accurate GC。外国人也挺麻烦的,“准确”都统一不到一个词上⋯
是什么东西“准确”呢?关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才可以合理地解读数据的含义;GC所关心的含义就是“这块数据是不是指针”。
要实现这样的GC,JVM就要能够判断出所有位置上的数据是不是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。
有几种办法:
1、让数据自身带上标记(tag)。这种做法在JVM里不常见,但在别的一些语言实现里有体现。就不详细介绍了。打标记的方式在半保守式GC中倒是更常见一些,例如CRuby就是用打标记的半保守式GC。CLDC-HI比较有趣,栈上对每个slot都配对一个字长的tag来说明它的类型,通过这种方式来减少stack map的开销;类似的实现在别的地方没怎么见过,大家一般都不这么取舍。
2、让编译器为每个方法生成特别的扫描代码。我还没见过JVM实现里这么做的,虽说在别的语言实现里有见过。
3、从外部记录下类型信息,存成映射表。现在三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样做的。其中,HotSpot把这样的数据结构叫做OopMap,JRockit里叫做livemap,J9里叫做GC map。Apache Harmony的DRLVM也把它叫GCMap。
要实现这种功能,需要虚拟机里的解释器和JIT编译器都有相应的支持,由它们来生成足够的元数据提供给GC。
使用这样的映射表一般有两种方式:
1、每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
2、为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。
在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。
每个被JIT编译过后的方法也会在一些特定的位置记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了。这些特定的位置主要在:
1、循环的末尾
2、方法临返回前 / 调用方法的call指令后
3、可能抛异常的位置
这种位置被称为“安全点”(safepoint)。之所以要选择一些特定的位置来记录OopMap,是因为如果对每条指令(的位置)都记录OopMap的话,这些记录就会比较大,那么空间开销会显得不值得。选用一些比较关键的点来记录就能有效的缩小需要记录的数据量,但仍然能达到区分引用的目的。因为这样,HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。
而仍然在解释器中执行的方法则可以通过解释器里的功能自动生成出OopMap出来给GC用。
平时这些OopMap都是压缩了存在内存里的;在GC的时候才按需解压出来使用。
HotSpot是用“解释式”的方式来使用OopMap的,每次都循环变量里面的项来扫描对应的偏移量。
对Java线程中的JNI方法,它们既不是由JVM里的解释器执行的,也不是由JVM的JIT编译器生成的,所以会缺少OopMap信息。那么GC碰到这样的栈帧该如何维持准确性呢?
HotSpot的解决方法是:所有经过JNI调用边界(调用JNI方法传入的参数、从JNI方法传回的返回值)的引用都必须用“句柄”(handle)包装起来。JNI需要调用Java API的时候也必须自己用句柄包装指针。在这种实现中,JNI方法里写的“jobject”实际上不是直接指向对象的指针,而是先指向一个句柄,通过句柄才能间接访问到对象。这样在扫描到JNI方法的时候就不需要扫描它的栈帧了——只要扫描句柄表就可以得到所有从JNI方法能访问到的GC堆里的对象。
但这也就意味着调用JNI方法会有句柄的包装/拆包装的开销,是导致JNI方法的调用比较慢的原因之一。
===================================================================
实现例子:Oracle/Sun HotSpot VM
Sun HotSpot VM从设计之初就使用准确式GC。
在HotSpot VM之前,Sun在1.0.x到1.2.x中提供的JVM(后来称为Classic VM)用的是半保守式的设计。在SPARC版JDK 1.2.1和1.2.2中提供的EVM(也称为ExactVM)则实现了准确式GC。
后面举的一些例子也都显示,高性能VM实现更倾向使用准确式GC;即便一开始为了快速开发使用了半保守式GC,后续发展为了追求性能也会渐渐朝着准确式的方向发展。
简要回答里已经提到了HotSpot的实现方式。下面引用几篇论文分别看看HotSpot的client编译器(JDK6)与server编译器对OopMap / safepoint的支持。
Design of the Java HotSpot Client Compiler for Java 6
引用
The Java HotSpotTM VM also provides various other garbage collectors [Sun Microsystems, Inc. 2006c]. Parallel garbage collectors for server machines with large physical memories and multiple CPUs distribute the work among multiple threads, thus decreasing the garbage collection overhead and increasing the application throughput. A concurrent mark-and-sweep algorithm [Boehm et al. 1991; Printezis and Detlefs 2000] allows the user program to continue its execution while dead objects are reclaimed.
Exact garbage collection requires information about pointers to heap objects. For machine code, this information is contained in object maps (also called oop maps) created by the JIT compiler. Besides, the compiler creates debugging information that maps the state of a compiled method back to the state of the interpreter. This enables aggressive compiler optimizations, because the VM can deoptimize [Holzle et al. 1992] back to a safe state when the assumptions under which an optimization was performed are invalidated (see Section 2.6). The machine code, the object maps, and the debugging information are stored together in a so-called native method object. Garbage collection and deoptimization are allowed to occur only at some discrete points in the program, called safepoints, such as backward branches, method calls, return instructions, and operations that may throw an exception.
...
After register allocation, machine code can be generated in a rather simple and straightforward way. The compiler traverses the LIR, operation by operation, and emits appropriate machine instructions into a code buffer. This process also yields object maps and debugging information.
Exact garbage collection requires information about pointers to heap objects. For machine code, this information is contained in object maps (also called oop maps) created by the JIT compiler. Besides, the compiler creates debugging information that maps the state of a compiled method back to the state of the interpreter. This enables aggressive compiler optimizations, because the VM can deoptimize [Holzle et al. 1992] back to a safe state when the assumptions under which an optimization was performed are invalidated (see Section 2.6). The machine code, the object maps, and the debugging information are stored together in a so-called native method object. Garbage collection and deoptimization are allowed to occur only at some discrete points in the program, called safepoints, such as backward branches, method calls, return instructions, and operations that may throw an exception.
...
After register allocation, machine code can be generated in a rather simple and straightforward way. The compiler traverses the LIR, operation by operation, and emits appropriate machine instructions into a code buffer. This process also yields object maps and debugging information.
A Compiler for the Java HotSpot Virtual Machine (描述的是2000年JDK 1.3.0里的HotSpot Client Compiler)
引用
4.5 Debug Information
The back end generates debug information as a side effect of code generation. Debug information includes a mapping of program counter offsets to bytecode indices, so-called pc-maps. It also includes oop maps (i.e., pointer maps), specifying the exact stack location of all oops for all pc's where GC can occur. Finally, it includes safepoint information, which is used for GC as well.
Pc-maps are required for exception handling: if a runtime exception occurs, the pc where the exception occurred is mapped to the corresponding bytecode index (bci). This bci is used to loop upi the exception handler.
Oop maps are bit maps specifying which stack and register locations hold oops for a given pc. During GC, these locations need to be visited since they represent roots for GC. Also, if GC moves objects, pointers must be updated.
GC cannot happen at arbitrary places, but only at safepoints. Safepoints are designated places in the code, usually function calls, backward branches, and return instructions. All threads running code (interpreted or compiled) must be suspended at a safepoint before GC is executed. At a safepoint, the location of all oops on the stack is known. The safepoint suspension mechanism is independent of the compiler. Its discussion is beyond the scope of this paper.
Debug information is stored in a compressed form together with the generated code in native method objects. They reside in the code cache managed by the VM.
The back end generates debug information as a side effect of code generation. Debug information includes a mapping of program counter offsets to bytecode indices, so-called pc-maps. It also includes oop maps (i.e., pointer maps), specifying the exact stack location of all oops for all pc's where GC can occur. Finally, it includes safepoint information, which is used for GC as well.
Pc-maps are required for exception handling: if a runtime exception occurs, the pc where the exception occurred is mapped to the corresponding bytecode index (bci). This bci is used to loop upi the exception handler.
Oop maps are bit maps specifying which stack and register locations hold oops for a given pc. During GC, these locations need to be visited since they represent roots for GC. Also, if GC moves objects, pointers must be updated.
GC cannot happen at arbitrary places, but only at safepoints. Safepoints are designated places in the code, usually function calls, backward branches, and return instructions. All threads running code (interpreted or compiled) must be suspended at a safepoint before GC is executed. At a safepoint, the location of all oops on the stack is known. The safepoint suspension mechanism is independent of the compiler. Its discussion is beyond the scope of this paper.
Debug information is stored in a compressed form together with the generated code in native method objects. They reside in the code cache managed by the VM.
The Java HotSpot Server Compiler
引用
15.Cleanup
...
Then we gather and output the information necessary for garbage collection. This includes the location of all object pointers which are alive across a safepoint, as well as the location of all values which are callee saves in the method, and all necessary computation information for derived pointers which have an object pointer as their base.
...
17.Code Generation
In addition to executable machine code, the code generator also provides oopmaps, debug info, exception tables, relocation information, and an implicit−null check table for use by the runtime system. All of this information is associated with one or more native-code offsets from method entry. Oopmaps and debug info are associated with the offset to their safepoint. Oopmaps are generated during register allocation and the code generator simply packages this information for the runtime. Safepoints at which a deoptimization may occur also record debug info describing either the constant value or native storage location for monitors, locals, and expression stack entries. The storage location may be a register or a stack frame offset.
...
...
Then we gather and output the information necessary for garbage collection. This includes the location of all object pointers which are alive across a safepoint, as well as the location of all values which are callee saves in the method, and all necessary computation information for derived pointers which have an object pointer as their base.
...
17.Code Generation
In addition to executable machine code, the code generator also provides oopmaps, debug info, exception tables, relocation information, and an implicit−null check table for use by the runtime system. All of this information is associated with one or more native-code offsets from method entry. Oopmaps and debug info are associated with the offset to their safepoint. Oopmaps are generated during register allocation and the code generator simply packages this information for the runtime. Safepoints at which a deoptimization may occur also record debug info describing either the constant value or native storage location for monitors, locals, and expression stack entries. The storage location may be a register or a stack frame offset.
...
在实际代码中,生成OopMap的逻辑在Compile::BuildOopMaps()实现(hotspot/src/share/vm/opto/buildOopMap.cpp)
-------------------------------------------------------------------
我的草稿箱里有一些关于HotSpot的client和server编译器分别如何生成OopMap的笔记,不过还没整理完所以暂时就不放出来了。不过各位同学如果对此感兴趣的话,可以从-XX:+PrintAssembly输出的日志入手来了解OopMap会关联在哪些位置上,里面的内容有些什么。知道了如何解读OopMap的信息,就能很方便的知道一个被JIT编译过的方法会如何跟GC交互了,某些局部变量是否需要显式置null也会一目了然。
例如说,
[Verified Entry Point] 0x00007f3749ea3400: mov %eax,-0x6000(%rsp) # ... 0x00007f3749ea3453: callq 0x00007f3749e7c820 ; OopMap{[0]=Oop off=88} ;*invokevirtual intern ; - TestC2OopMapGeneration::doTest@19 (line 4) ; {optimized virtual_call} 0x00007f3749ea3458: inc %ebp ;*iinc
这段输出含有一条x86-64的callq指令,用来实现对intern()方法的调用。在后面的注释当中可以看到:
OopMap{[0]=Oop off=88}
它的意思是:
有一个OopMap与这条callq指令之后的一条指令(inc %ebp)关联在一起;
在该指令位置上只有一个活跃的引用,在栈顶上。
这是怎么解读的呢?
OopMap记录输出的日志的构成是:
OopMap{零到多个“数据位置=内容类型”的记录 off=该OopMap关联的指令的位置}
在这个例子中,
[0]表示栈顶指针+偏移量0,这里就是[rsp + 0],也就是栈顶;右边的"=Oop"说明这个位置存着一个普通对象指针(ordinary object pointer,HotSpot将指向GC堆中对象开头位置的指针称为Oop)。
除了Oop之外,可能出现的类型有:
hotspot/src/share/vm/compiler/oopMap.hpp
enum oop_types { // must fit in type_bits unused_value =0, // powers of 2, for masking OopMapStream oop_value = 1, value_value = 2, narrowoop_value = 4, callee_saved_value = 8, derived_oop_value= 16 };
对应的显示为:
hotspot/src/share/vm/compiler/oopMap.cpp
static void print_register_type(OopMapValue::oop_types x, VMReg optional, outputStream* st) { switch( x ) { case OopMapValue::oop_value: st->print("Oop"); break; case OopMapValue::value_value: st->print("Value" ); break; case OopMapValue::narrowoop_value: tty->print("NarrowOop" ); break; case OopMapValue::callee_saved_value: st->print("Callers_" ); optional->print_on(st); break; case OopMapValue::derived_oop_value: st->print("Derived_oop_" ); optional->print_on(st); break; default: ShouldNotReachHere(); } }
off=88就是这个OopMap记录关联的指令在方法的指令流中的偏移量,这个数字是十进制的。
可以看到,该方法的指令流是从地址0x00007f3749ea3400开始的;十进制的88就是十六进制的0x58;
0x00007f3749ea3400 + 0x58 = 0x00007f3749ea3458,正好就是例子中callq指令后的inc %ebp所在的位置。
-------------------------------------------------------------------
再举几个例子。
OopMap{off=228}
这个记录比较简单。它说明在该方法的指令流中,有一个OopMap与偏移量228的位置上的指令关联在一起。
该OopMap显示这个位置上没有任何活着的引用。
OopMap{rbp=Oop off=144}
在偏移量为144的指令上关联了一个OopMap的记录,有一个活跃的引用在寄存器RBP里。
OopMap{rbp=NarrowOop off=248}
在偏移量为248的指令上关联了一个OopMap的记录,有一个活跃的引用在寄存器RBP里,并且这个引用是压缩过的(NarrowOop)。
OopMap{[296]=Callers_eax [292]=Callers_ecx [288]=Callers_edx [284]=Callers_ebx [272]=Callers_esi [268]=Callers_edi [28]=Callers_xmm0 [32]=Callers_xmm0 [36]=Callers_xmm1 [40]=Callers_xmm1 [44]=Callers_xmm2 [48]=Callers_xmm2 [52]=Callers_xmm3 [56]=Callers_xmm3 [60]=Callers_xmm4 [64]=Callers_xmm4 [68]=Callers_xmm5 [72]=Callers_xmm5 [76]=Callers_xmm6 [80]=Callers_xmm6 [84]=Callers_xmm7 [88]=Callers_xmm7 off=674}
这个OopMap记录看起来比较壮观。其实是它关联的指令位置上正好所有的callee-save的寄存器都保存到栈上了而已。例如说,在[栈顶+296]位置上保存的是调用方的eax;至于它到底是不是一个Oop就得看调用方对应的OopMap是怎么说的了。
OopMap{[24]=Oop [28]=Derived_oop_[24] [32]=Derived_oop_[24] off=192}
这里可以看到derived oop类型的数据,在栈上[28]位置的是由栈上[24]开始的一个对象的派生引用。
===================================================================
实现例子:IBM Sovereign JVM
IBM在JDK 5之前主要提供的JVM是Sovereign VM,里面的GC是半保守式的(栈上保守,堆上准确);而从JDK 5开始主要提供J9 VM,里面的GC转为准确式。
关于IBM DK for Java 1.4.1中的Sovereign JVM所使用的半保守式GC设计,可以参考下面这段文字:
IBM JVM Garbage Collection and Storage Allocation techniques
引用
4.1 Mark Phase
In this phase, all the live objects are marked. Because unreachable objects cannot be identified singly, all the reachable objects must be identified. Therefore, everything else must be garbage. The process of marking all reachable objects is also known as tracing.
The active state of the JVM is made up of the saved registers for each thread, the set of stacks that represent the threads, the static’s that are in Java classes, and the set of local and global JNI references. All functions that are invoked in the JVM itself cause a frame on the C stack. This frame might contain instances of objects as a result of either an assignment to a local variable, or a parameter that is sent from the caller. All these references are treated equally by the tracing routines. The Garbage Collector views the stack of a thread as a set of 4-byte fields (8 bytes in 64-bit architecture) and scans them from the top to the bottom of each of the stacks. The Garbage Collector assumes that the stacks are 4-byte aligned (8-byte aligned in 64-bit architecture). Each slot is examined to see whether it points at an object that is in the heap. Note that this does not make it necessarily a pointer to an object, because it might be only an accidental combination of bits in a float or integer. So, when the Garbage Collector performs the scan of a thread stack, it handles conservatively anything that it finds. Anything that points at an object is assumed to be an object, but the object in question must not be moved during garbage collection. A slot is thought to be a pointer to an object if it meets these three requirements:
1. It is grained (aligned) on an 8-byte boundary.
2. It is inside the bounds of the heap.
3. The allocbit is on.
Objects that are referenced in this way are known as roots, and have their dosed bit set on to indicate that they cannot be moved. The setting of dosed bits is done only if the Garbage Collector is to perform a compaction. Tracing can now proceed accurately. That is, the Garbage Collector can find references in the roots to other objects and, because it knows that they are real references, it can move them during compaction because it can change the reference. The tracing process uses a stack that can hold 4 KB entries. All references that are pushed to the stack are marked at the same time by setting the relevant markbit to on. The roots are marked and pushed to the stack and then the Garbage Collector starts to pop entries off the stack and trace them. Normal objects (not arrays) are traced by using the mtpr to access the classblock, which tells where references to other objects are to be found in this object. As each reference is found, if it is not already marked, it is marked and pushed.
Array objects are traced by looking at each array entry and, if it is not already marked, it is marked and pushed. Some additional code traces a small portion of the array at a time, to try to avoid mark stack overflow.
The above process continues repeatedly until the mark stack eventually becomes empty.
In this phase, all the live objects are marked. Because unreachable objects cannot be identified singly, all the reachable objects must be identified. Therefore, everything else must be garbage. The process of marking all reachable objects is also known as tracing.
The active state of the JVM is made up of the saved registers for each thread, the set of stacks that represent the threads, the static’s that are in Java classes, and the set of local and global JNI references. All functions that are invoked in the JVM itself cause a frame on the C stack. This frame might contain instances of objects as a result of either an assignment to a local variable, or a parameter that is sent from the caller. All these references are treated equally by the tracing routines. The Garbage Collector views the stack of a thread as a set of 4-byte fields (8 bytes in 64-bit architecture) and scans them from the top to the bottom of each of the stacks. The Garbage Collector assumes that the stacks are 4-byte aligned (8-byte aligned in 64-bit architecture). Each slot is examined to see whether it points at an object that is in the heap. Note that this does not make it necessarily a pointer to an object, because it might be only an accidental combination of bits in a float or integer. So, when the Garbage Collector performs the scan of a thread stack, it handles conservatively anything that it finds. Anything that points at an object is assumed to be an object, but the object in question must not be moved during garbage collection. A slot is thought to be a pointer to an object if it meets these three requirements:
1. It is grained (aligned) on an 8-byte boundary.
2. It is inside the bounds of the heap.
3. The allocbit is on.
Objects that are referenced in this way are known as roots, and have their dosed bit set on to indicate that they cannot be moved. The setting of dosed bits is done only if the Garbage Collector is to perform a compaction. Tracing can now proceed accurately. That is, the Garbage Collector can find references in the roots to other objects and, because it knows that they are real references, it can move them during compaction because it can change the reference. The tracing process uses a stack that can hold 4 KB entries. All references that are pushed to the stack are marked at the same time by setting the relevant markbit to on. The roots are marked and pushed to the stack and then the Garbage Collector starts to pop entries off the stack and trace them. Normal objects (not arrays) are traced by using the mtpr to access the classblock, which tells where references to other objects are to be found in this object. As each reference is found, if it is not already marked, it is marked and pushed.
Array objects are traced by looking at each array entry and, if it is not already marked, it is marked and pushed. Some additional code traces a small portion of the array at a time, to try to avoid mark stack overflow.
The above process continues repeatedly until the mark stack eventually becomes empty.
===================================================================
实现例子:Mono
(不熟悉Mono的同学们:Mono不是一种JVM实现,而是一种CLI实现。不过CLI与JVM有许多相似之处,所以顺便拿Mono来举例)
早期版本的Mono采用Boehm GC,使用的是半保守模式。可以参考以下文档的描述:
Mono's use of Boehm GC
引用
Mono's use of Boehm GC
We are using the Boehm conservative GC in precise mode.
There are a few areas that the GC scans for pointers to managed objects:
1. The heap (where other managed objects are allocated)
2. thread stacks and registers
3. static data area
4. data structures allocated by the runtime
(1) is currently handled in mostly precise mode: almost always the GC will only consider memory words that contain only references to the heap, so there is very little chance of pointer misidentification and hence memory retention as a result. The new GC requires a fully precise mode here, so it will improve things marginally. The details about mostly precise have to do with large objects with sparse bitmaps of references and the handling of multiple appdomains safely.
(2) is always scanned conservatively. This will be true for the new GC, too, at least for the first versions, where I'll have my own share of fun at tracking the bugs that a moving generational GC will expose. Later we'll conservatively scan only the unmanaged part of the stacks.
(3) We already optimized this both with Boehm and the current GC to work in precise mode.
(4) I already optimized this to work in mostly precise mode (ie some data structures are dealt with precisely, others not yet). I'll need to do more work in this area, especially for the new GC, where having pinned objects can be a significant source of pain.
We are using the Boehm conservative GC in precise mode.
There are a few areas that the GC scans for pointers to managed objects:
1. The heap (where other managed objects are allocated)
2. thread stacks and registers
3. static data area
4. data structures allocated by the runtime
(1) is currently handled in mostly precise mode: almost always the GC will only consider memory words that contain only references to the heap, so there is very little chance of pointer misidentification and hence memory retention as a result. The new GC requires a fully precise mode here, so it will improve things marginally. The details about mostly precise have to do with large objects with sparse bitmaps of references and the handling of multiple appdomains safely.
(2) is always scanned conservatively. This will be true for the new GC, too, at least for the first versions, where I'll have my own share of fun at tracking the bugs that a moving generational GC will expose. Later we'll conservatively scan only the unmanaged part of the stacks.
(3) We already optimized this both with Boehm and the current GC to work in precise mode.
(4) I already optimized this to work in mostly precise mode (ie some data structures are dealt with precisely, others not yet). I'll need to do more work in this area, especially for the new GC, where having pinned objects can be a significant source of pain.
从Mono 2.8开始,一种名为SGen的新GC实现被包含在发布包中。但这个版本的SGen仍然是半保守式GC。
未来Mono将逐渐转为使用完全准确式的GC。这篇文档描述了设计思路:Precise Stack Marking
===================================================================
进一步阅读
Finding References in Java Stacks
1997年Sun Labs的一篇论文。主要讲解了在Java的栈里找出引用的特点、问题与解决思路,特别是与jsr带来的问题。其中前三章概括介绍了保守式GC与准确式GC的概念、准确式GC涉及的元数据(stack map)、生成stack map的方法等内容。
GC Points in a Threaded Environment
1998年Sun Labs的一篇论文。讲解了多线程条件下允许GC发生的位置,所谓的“GC point”,也称为“safepoint”。
Oracle JRockit: The Definitive Guide 第三章,84-87页,Livemaps
介绍了JRockit的GC是如何找到活动记录中的引用的。JRockit的livemap也是由JIT编译器生成出来的。
Mostly Accurate Stack Scanning
2001年IBM出的一篇论文。讲解了几种半保守式扫描栈的方法。
Support for Garbage Collection at Every Instruction in a Java Compiler
1999年Intel出的一篇论文。它的论点是通过采用压缩技术,即便为每条指令都生成GC map,消耗的空间也可以接受。
虽然如此,但现在主流的VM里没有一个是会为每条指令都生成GC map的;相反,多数是不但只在safepoint才生成GC map,而且平时还将GC map压缩起来放在内存里。
Accurate Garbage Collection in Uncooperative Environments Revisited
2002年的一篇论文,描述了Ovm是如何在缺乏编译器支持的情况下实现准确式GC。Ovm是一种比较特别的JVM实现,它先把Java字节码翻译为C/C++,然后再用GCC之类的C/C++编译器生成最终的代码。本来Java比较容易实现完全准确的GC,但经过中间C/C++这层事情就变得复杂了。
Runtime Tags Aren't Necessary
1988年Andrew W. Appel(虎书作者)写的一篇论文。它提出了在ML语言的静态多态类型系统的支持下,GC可以怎样在不使用tag的前提下将引用与非引用区分开。
Accurate Garbage Collection with LLVM
编译器框架LLVM的官方文档,介绍了如何将LLVM与准确式GC结合起来。后面这个演示稿介绍了VMKit是如何将MMTk与LLVM整合在一起实现准确式GC的:Precise and Efficient Garbage Collection in VMKit with MMTk。
Ruby Hacking Guide,第五章 垃圾收集,is_pointer_to_heap()
原版(日文)
ItEye专栏里的中文翻译
介绍了CRuby是如何区分一个数据是不是指向GC堆中的指针。CRuby的GC也是属于半保守式的。
ガベージコレクションのアルゴリズムと実装
介绍GC的算法与实现的一本书。其中11.5与12.4小节分别介绍了Rubinius与V8的准确式GC的一些设计。
Shared Source CLI Essentials, 249-250页, Scheduling Collection
Shared Source CLI 2.0 Internals, 253-254页, Scheduling Collection
介绍SSCLI的书的第一版与第二版(第二版是免费的电子版,点上面的链接可以下载到)。
这本书在之前一帖里介绍过,强力推荐对VM的实现感兴趣的同学阅读。前提是能接受SSCLI License。
上面提到的章节里介绍了SSCLI的safepoint与GC map。相关代码可以参考:
sscli20\sscli20\clr\src\vm\fjit_eetwain.cpp
sscli20\sscli20\clr\src\fjit\fjitencode.cpp
既然都提到了Mono和SSCLI,那顺带也提一下微软的CLI实现,CLR的玩法吧。
MSDN: SOS.dll (SOS Debugging Extension)
这篇MSDN文档介绍了随CLR一起发布的SOS扩展的命令。其中,
!CLRStack -l
可以查看托管方法的栈帧上局部变量的状态。这个命令显示的信息就是由CLR中JIT编译器生成的GC map所提供的。下面是例子
引用
!CLRStack -l OS Thread Id: 0x133c (4924) ESP EIP 0012f3cc 00de0136 ConsoleApplication16.Program.ImportString(System.String[,]) LOCALS: 0x0012f3f8 = 0x012b1d30 0x0012f3f4 = 0x00000000 0x0012f3f0 = 0x00000000 0x0012f3ec = 0x00000000 0x0012f3e8 = 0x00000000 0x0012f3e4 = 0x00000000 <CLR reg> = 0x00000000 0x0012f3dc = 0x00000000 0x0012f3d8 = 0x00000000 0x0012f3d4 = 0x00000000 0012f440 00de00b8 ConsoleApplication16.Program.Main(System.String[]) LOCALS: 0012f69c 79e88f63 [GCFrame: 0012f69c]
评论
6 楼
LeafInWind
2014-01-12
5 楼
RednaxelaFX
2014-01-11
4 楼
LeafInWind
2014-01-11
最近研究hotspot的GC实现,一直有一个疑问,就是FastScanClosure::do_oop(oop* p)这个方法仅仅移动了 p 直接引用的对象,那被 p 间接引用的对象呢,它们显然也是活着的,它们是如何被发现以及移动的。
3 楼
LeafInWind
2014-01-11
正在研究hotspot的GC相关代码,发现FastScanClosure::do_oop仅仅移动了被栈上的root直接引用的对象,而那些被栈上root间接引用的对象呢,这些对象是被如果移动的,怎么都没找到。
2 楼
RednaxelaFX
2011-05-16
bbf_sx 写道
弱弱的问一下,这鞋是什么代码,汇编吗?
不是,什么代码都不是。它只是!CLRStack这个命令显示的结果,内容是线程的调用栈的状况。可以把这个输出分解来看:
OS Thread Id: 0x133c (4924)
这是线程ID
ESP EIP
这是格式说明,告诉用户下面的两个数字是什么意思。
0012f3cc 00de0136 ConsoleApplication16.Program.ImportString(System.String[,])
这是这个调用栈最顶上的一个栈帧,它的栈顶指针(ESP)值是0x0012f3cc,当前执行到的指令地址(EIP)是0x00de0136。这个栈帧对应的方法是ConsoleApplication16.Program.ImportString(System.String[,])。
LOCALS: 0x0012f3f8 = 0x012b1d30 0x0012f3f4 = 0x00000000 0x0012f3f0 = 0x00000000 0x0012f3ec = 0x00000000 0x0012f3e8 = 0x00000000 0x0012f3e4 = 0x00000000 <CLR reg> = 0x00000000 0x0012f3dc = 0x00000000 0x0012f3d8 = 0x00000000 0x0012f3d4 = 0x00000000
该栈帧里的局部变量的状态。等号左手边的是地址,右手边的是在这个地址里存的值。
可以看到这个例子里有9个被记录下来的局部变量是在栈上的,有1个是在寄存器里的。(具体是哪个寄存器这里没写。我印象中这个是ESI…不太肯定。
1 楼
bbf_sx
2011-05-16
引用
!CLRStack -l OS Thread Id: 0x133c (4924) ESP EIP 0012f3cc 00de0136 ConsoleApplication16.Program.ImportString(System.String[,]) LOCALS: 0x0012f3f8 = 0x012b1d30 0x0012f3f4 = 0x00000000 0x0012f3f0 = 0x00000000 0x0012f3ec = 0x00000000 0x0012f3e8 = 0x00000000 0x0012f3e4 = 0x00000000 <CLR reg> = 0x00000000 0x0012f3dc = 0x00000000 0x0012f3d8 = 0x00000000 0x0012f3d4 = 0x00000000 0012f440 00de00b8 ConsoleApplication16.Program.Main(System.String[]) LOCALS: 0012f69c 79e88f63 [GCFrame: 0012f69c]
弱弱的问一下,这鞋是什么代码,汇编吗?
发表评论
-
The Prehistory of Java, HotSpot and Train
2014-06-02 08:18 0http://cs.gmu.edu/cne/itcore/vi ... -
MSJVM and Sun 1.0.x/1.1.x
2014-05-20 18:50 0当年的survey paper: http://www.sym ... -
Sun JDK1.4.2_28有TieredCompilation
2014-05-12 08:48 0原来以前Sun的JDK 1.4.2 update 28就已经有 ... -
IBM JVM notes (2014 ver)
2014-05-11 07:16 0Sovereign JIT http://publib.bou ... -
class data sharing by Apple
2014-03-28 05:17 0class data sharing is implement ... -
HotSpot Server VM与Server Class Machine
2014-02-18 13:21 0HotSpot VM历来有Client VM与Server V ... -
Java 8的lambda表达式在OpenJDK8中的实现
2014-02-04 12:08 0三月份JDK8就要发布首发了,现在JDK8 release c ... -
GC stack map与deopt stack map的异同
2014-01-08 09:56 0两者之间不并存在包含关系。它们有交集,但也各自有特别的地方。 ... -
HotSpot Server Compiler与data-flow analysis
2014-01-07 17:41 0http://en.wikipedia.org/wiki/Da ... -
基于LLVM实现VM的JIT的一些痛点
2014-01-07 17:25 0同事Philip Reames Sanjoy Das http ... -
tailcall notes
2013-12-27 07:42 0http://blogs.msdn.com/b/clrcode ... -
《自制编程语言》的一些笔记
2013-11-24 00:20 0http://kmaebashi.com/programmer ... -
字符串的一般封装方式的内存布局 (1): 元数据与字符串内容,整体还是分离?
2013-11-07 17:44 22410(Disclaimer:未经许可请 ... -
字符串的一般封装方式的内存布局 (0): 拿在手上的是什么
2013-11-04 18:22 21512(Disclaimer:未经许可请 ... -
字符串的一般封装方式的内存布局
2013-11-01 12:55 0(Disclaimer:未经许可请 ... -
关于string,内存布局,C++ std::string,CoW
2013-10-30 20:45 0(Disclaimer:未经许可请 ... -
Java的instanceof是如何实现的
2013-09-22 16:57 0Java语言规范,Java SE 7版 http://docs ... -
也谈类型: 数据, 类型, 标签
2013-08-18 01:59 0numeric tower http://en.wikiped ... -
oop、klass、handle的关系
2013-07-30 17:34 0oopDesc及其子类的实例 oop : oopDesc* ... -
Nashorn各种笔记
2013-07-15 17:03 0http://bits.netbeans.org/netbea ...
相关推荐
2. **找出单链表的倒数第4个元素** 这通常通过两次遍历来完成:第一次遍历获取链表长度,第二次遍历至特定位置(长度减去3)。更高效的方法是使用双指针技术,其中一个指针先向前移动三步,然后两个指针同步移动,...
- **调试技巧**:通过调试源代码,你可以深入理解数据结构的内部工作原理,找出潜在的逻辑错误。 在学习过程中,你可以逐步实现这些数据结构,然后通过编写测试用例来验证它们的正确性。这个过程将帮助你深化对数据...
4. 内存错误检测工具:如ASan(AddressSanitizer)、UBSan(UndefinedBehaviorSanitizer)等,它们在编译时插入额外的检查代码,可以在程序运行时捕获如缓冲区溢出、空指针解引用等错误,并提供详细的错误报告,包括...
2. **找出单链表的倒数第k个元素** - 双指针法:一个指针先向前移动k步,然后再一起同步移动,当先移动的指针到达末尾时,另一个指针就是目标位置。 3. **找出单链表的中间元素** - 快慢指针法:快指针每次走两步...
3. **分析调用堆栈**:找出导致崩溃的函数,通常是最顶部的函数,因为它是最后执行的。检查这个函数的代码,看是否存在潜在的问题,如空指针解引用、数组越界等。 4. **检查变量和寄存器**:通过gdb的`print`命令...
这些工具可以在编译阶段检查代码,找出可能的内存错误。 9. **运行时检查**:在调试模式下,可以启用某些运行时库的特性,如C++标准库的`std::addressof`,用于获取对象的真实地址,以及`std::bad_access`异常,...
3. **修饰符的位置**:对于`*`(指针)和`&`(引用),应将它们紧靠变量名放置,以减少理解上的混淆。 4. **if语句的使用**:布尔变量无需与其他值比较,整型变量需与相同类型的值比较,浮点数比较应有限制,指针应...
1. **查找特殊存储单元**:首先,在STC89C54RD的相关资料中找出那些区别于标准51系列单片机的特殊存储单元或寄存器。这些特殊功能寄存器是实现某些特定功能的关键。 2. **编写.txt格式文档**:根据这些特殊功能...
5. **指针**(阶段5):在这个阶段,学生可能需要理解指针的使用和操作,包括指针运算和解引用,以找出正确的输入。 6. **链表/指针/结构**(阶段6):可能涉及到链表数据结构的操作,需要理解指针如何遍历链表,...
- 引用本身不占用额外的存储空间,它只是一个指针的别名,因此没有指针的解引用操作。 - 引用可以作为函数参数和返回值,这样可以确保函数对传入的对象进行直接修改,而不是复制一份。 最后,两个C++笔试题涉及了位...
- 引用一旦初始化后就不能改变引用的对象,而指针可以改变所指的对象。 - 不存在空引用,但存在空指针。 3. 实时系统的基本特性: 实时系统需要在规定的时间内完成特定任务,并强调系统的可靠性和响应时间。 4....
- 使用调试器:如GDB(GNU Debugger)在类Unix系统中,Visual Studio Debugger在Windows上,可以帮助你单步执行代码,观察变量状态,找出错误发生的具体位置。 - 日志记录:添加日志输出,记录关键变量和操作,有助...
- 最小生成树用于找出连接所有节点的最小权值边集,最短路径则找出两点间的最短路径。 5. 字符串匹配算法:朴素BF算法、KMP算法、Boyer-Moore算法 - 用于在文本中查找特定子串,KMP和Boyer-Moore有更高的查找效率...
通过解答这些题目,你可以检验自己的知识水平,找出学习中的薄弱环节,并针对性地加强练习。 总的来说,理解和熟练掌握C/C++语言涉及的知识面广泛,需要持续的实践和学习。这份“C/C++笔试汇总”资料是提升技能、...
- **示例**:文档中给出了一些使用指针和引用的示例代码,展示了它们的使用场景和注意事项。 #### 十、面试准备建议 - **全面准备**:不仅要熟悉基础知识,还应该掌握一些高级主题,如模板元编程、智能指针等。 - ...
要求找出1、2、5的不同个数组合,使其和为100。可以使用动态规划或数学方法来解决。题目提供了两种解法,一种是三重循环,效率较低;另一种是数学分析,利用奇偶性,提高了效率。 这些题目覆盖了基础的编程概念、...
3.链表的操作:删除节点和找出中间的节点。链表是一种数据结构,它可以用来存储和操作大量数据。 4.CPU调度线程的方式:在Linux中,线程的调度是按照进程的调度方式来进行调度的,也就是说线程是调度单元。Linux中...
找出数组中第一个被修改过的数字问题,可以通过异或运算的性质来解决,即数组中所有元素和正常状态下的数组对应位置元素进行异或运算,结果为第一个被修改的数字。 设计DNS服务器中的Cache数据结构,需要综合考虑...
3. **基本数据类型与引用数据类型存储**:基本数据类型存储在栈中,而引用数据类型在栈中存储指针,实际数据存储在堆中。 4. **判断数据类型的方法**:除了typeof,还有instanceof用于判断对象是否为类的实例,...