首页 > 技术文章 > 【翻译练习】HotSpot和OpenJDK起步

slive 2013-09-07 18:04 原文

原文http://www.infoq.com/articles/Introduction-to-HotSpot
 
在这篇文章中,我们将看到HotSpot Java虚拟机(JVM)是如何开始工作的,还有它在OpenJDK开源项目的实现——它们都是来自一个虚拟机(VM)概念,并与Java 类库相互作用。
 
介绍HotSpot源码
让我们看一下JDK源码和包含Java概念在内的实现。这里有两种检查源码的主要方法:
  •  现代的IDE可以粘贴src.zip (来自 $JAVA_HOME),并准许从IDE访问,或者
  •  使用OpenJDK源代码和文件系统导航。
这两种方式都是有用的,但对于第二种方法来说,用得舒服很重要,同样第一种也是。OpenJDK源代码存储在Mercurial中(一个分布版本控制系统,类似于常用的Git版本控制系统)。如果不熟悉Mercurial,这里有一本的免费书(Version Control By Example”),它覆盖了基本的内容
 
检出OpenJDK 7源码前,请先安装Mercurial命令行工具,然后执行如下命令:
 
 hg clone http://hg.openjdk.java.net/jdk7/jdk7 jdk7_tl
 
这样会产生一个OpenJDK仓库的本地拷贝(copy)。这个仓库具有了项目的基本布局,但并没有包括所有的文件——因为OpenJDK项目延伸到好几个子仓库中:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
初始化克隆(clone)后,本地仓库看起来像下面所示:
ariel-2:jdk7_tl boxcat$ ls -l
total 664
-rw-r--r--  1 boxcat staff   1503 14 May 12:54 ASSEMBLY_EXCEPTION
-rw-r--r--  1 boxcat staff  19263 14 May 12:54 LICENSE
-rw-r--r--  1 boxcat staff  16341 14 May 12:54 Makefile
-rw-r--r--  1 boxcat staff   1808 14 May 12:54 README
-rw-r--r--  1 boxcat staff 110836 14 May 12:54 README-builds.html
-rw-r--r--  1 boxcat staff 172135 14 May 12:54 THIRD_PARTY_README
drwxr-xr-x 12 boxcat staff    408 14 May 12:54 corba
-rwxr-xr-x  1 boxcat staff   1367 14 May 12:54 get_source.sh
drwxr-xr-x 14 boxcat staff    476 14 May 12:55 hotspot
drwxr-xr-x 19 boxcat staff    646 14 May 12:54 jaxp
drwxr-xr-x 19 boxcat staff    646 14 May 12:55 jaxws
drwxr-xr-x 13 boxcat staff    442 16 May 16:01 jdk
drwxr-xr-x 13 boxcat staff    442 14 May 12:55 langtools
drwxr-xr-x 18 boxcat staff    612 14 May 12:54 make
drwxr-xr-x  3 boxcat staff    102 14 May 12:54 test
 
下一步,应该去运行get_source.sh脚本(script),这个脚本被下载下来作为初始化克隆的一部分。这样就会填入剩下的项目,并克隆所有的实际需要构建OpenJDK文件。
 
在我们深入研究源代码的全部讨论之前,说“别害怕平台开源代码”是很重要。在开发者中十分常见的想法是:JDK源码一定是让人在精神上产生敬畏和感到难以接近的,毕竟它是整个平台的核心。
 
虽然JDK源码是可靠的,严格复查和测试的,但是完全可以接近的。尤其源码不会一直更新始终在应用的Java语言特性。所以在JDK源码内部,找到那些比如仍然没有完善和始终使用未经加工类型的情况是相当常见的。
 
下面是几种主要的JDK源码(这些源码我们应该去熟悉)仓库
 
 jdk
这是类库生存的地方。它们大部分是Java(一些是为本地方法写的C代码)。
对于进入OpenJDK源码来说,这是一个很好的起点。这些JDK类文件在jdk/src/share/classes
 
hotspot
HotSpot VM——这是C/C++和汇编代码(一些是基于Java的VM开发工具)。它很高级,并且如果你不是硬件核心C/C++开发者,这可能会让你在开始的时候觉得有点气馁。以后我们会更详细地讨论一些如何了解它的好途径
 
langtools
对于对编译器和工具开发有兴趣的人来说,这是可以找到语言和平台工具的地方。大部分Java和C代码——不像jdk代码那样容易进入,但对于大部分开发者应该是可以访问的。
 
还有其它的仓库,包括比如corba,jaxp和jaxws。它们对于大部分开发者来说,可能不是很重要或者感兴趣的
 
构建OpenJDK
Oracle最近分出一个项目为的是做OpenJDK完整的修补,并且简化了基础架构的构建。这个项目(据说名为“build-dev”)是如今完整和标准的构建OpenJDK方法。对于许多在基于Unix系统的用户来说,构建工作现在已经同安装编译器和“bootstrap JDK”一样简单了,只需要运行下面三行命令:
./configure
make clean
make images
 
更多关于构建自己的OpenJDK和开始应对它的细节,查看AdoptOpenJDK programme(由伦敦的Java社区建立)。它是我们开始研究的好地方——由大概有100个草根的开发者组成的社区,这些开发者从事着如警告清除,小的bug修复和跟主要的开源项目一起进行OpenJDK 8性能测试等项目工作。
 
理解HotSpot运行时环境
由OpenJDK提供的Java运行时环境由与(大量捆绑到rt.jar的)类库结合的HotSpot JVM组成。
 
因为Java的环境是可移植,任何进入操作系统的请求最终都是被本地方法处理。此外,有些方法要求从JVM获取特定的支持(如类加载)。这些也是通过本地方法调用进入到JVM的。
 
比如,让我们看一下为原生的Object类本地方法写的C源码。这个Object本地源码包含在 jdk/src/share/native/java/lang/Object.c中,它包括了六个方法。
 
Java Native Interface(JNI)通常要求用本地方法的C实现,为的是它能以一个非常明确的方式命名。比如, 本地方法Object::getClass() 用通用的命名惯例,所以包含在C函数中的C实现用如下方式限定:

Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
 
JNI有另一种加载本地方法的方式,通过使用java.lang.Object中五个保留的本地方法来进行的:

static JNINativeMethod methods[] = { {"hashCode", "()I", (void *)&JVM_IHashCode}, {"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
 
这五个方法被映射到JVM入口点(在C方法名字中,通过JVM_ prefix指定这些入口点)——用 registerNatives()机制(这个机制允许开发者改变Java本地方法到C函数名字映射关系)。
 
对于一般的图片,只要有可能,用Java语言编写Java运行环境,并且仅仅一些小的地方需要牵涉到JVM而已。JVM的主要工作除了执行代码外,还包括内部管理和环境(存活的Java对象的运行时间表示法-Java堆)的维护工作 。
 

OOPs & KlassOOPs

在堆中,任意的Java对象都是由Ordinary Object Pointer (OOP)描述。就C/C++而言,一个OOP就是一个真实的指针——一个指向Java堆内部存储器位置的机器字。根据Java进程的虚拟地址空间分配一段单一连续的地址给Java堆,然后在用户空间里存储器完全由Java自身的进程管理,除非JVM因为某些原因需要重新调整堆的大小。
 
这意味着创建和Java对象集合通常不涉及系统调用来分配或者解除分配内存。
 
OOP由首部的两个机器字组成,这两个机器字叫做Mark 和Klass字,它们后面跟着这个实例的成员字段。在这些字段前,数组有一个额外的首部字——即数组的长度
 
稍后我们会讲更多关于Mark 和Klass字,不过它们的名字是有寓意的——Mark字是用在垃圾集合(标记-扫除中的标记部分)和Klass字用做类的元数据指针。
 
在紧跟着OOP首部的字节中,这个实例的字段被安排在一个非常特定的顺序中。至于明确的细节,查看Nitsan Wakart的优秀博客,发表在 "Know thy Java Object Memory Layout"。
 
原始的和引用的字段被安排在OOP首部的后面——对象引用当然也是一样的。让我们看一个例子,关于Entry类(比如在 java.util.HashMap使用

static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
// methods...
}
 
现在,让我们计算一个 Entry对象 (假设在32位 JVM上)的大小。
 
首部由一个Mark字和一个Klass字组成,所以在32位虚拟机中,OOP首部是8字节(在64位中则是16字节)。
用OOP的的定义,所有大小是2个机器字+所有的实例字段的大小。
 
引用类型的字段看作是指针——这些指针在任何的合理的处理器体系中都是一个机器字大小。
 
因此,有1个int类型字段,2个引用类型字段(涉及到K和V类型的对象)和1个Entry字段,总的大小是:2个字(首部)+ 1个字(int)+ 3个字(指针)。
 
总共有24个字节(6个字)存储到一个单一的HashMap.Entry对象中。
 

KlassOOP

首部Klass字是OOP最重要的部分之一。它是这个类元数据(它代表着C++类型,称为klassOop)的指针。在这个类的方法的元数据中,它特别的重要,以C++虚拟表(“vtable”)表示。
 
我们不想每一个实例都携带它们所有方法的细节——这会是非常没有效率的——所以在klassOop中vtable的使用在实例中分享信息中是一个很好的方法。
 
注意到在不同的Class对象中klassOop是不同的也是很重要的,这是加载类操作的结果。这些不同总结为以下两个方面:
  • Class 对象(如 String.class)只是规则的Java对象——像任何其它的Java对象(instanceOops)一样,它们被描述成OOP;也和任何其它对象一样,有同样的行为;并且他们可以放到Java变量中。
  • klassOops 是这些JVM类元数据的代表——在vtable结构体中,它们携带类的方法。它是不可能直接地从Java代码获得一个klassOop的引用 ——并且它们存在堆的Permgen区域中。
记住这个区别的简单方法是:把klassOop看做JVM水平有关于类的Class对象的“mirror”。
 

虚拟调度

klassOop的vtable结构体和Java的方法调度与单一的继承有直接地关系。记得Java的实体方法调度默认是虚拟的(用被调用实例对象的运行时类型信息来查找这些方法)。
 
在klassOop vtable中,通过“变量vtable偏移”的使用来实现它。这意味着当在父类(或者祖父类等)要被覆盖的时候,同时在vtable中重写的方法是同一偏移量。
 
通过简单地沿着继承等级(从类到超类再到超超类)和查找一直在同一vtable偏移量中的方法实现,虚拟调度是容易被实现的。
 
比如对于toString()方法,意味着对于每个类一直是在同一个vtable偏移量中的。当编译JIT代码时,这个vtable结构体有利于单继承,也允许使用一些非常强大的最优化方法。
 
 
 
 
 

MarkOOPs

OPP首部Mark字是一个结构体指针(实际上就是一个保留关于OOP管理信息位字段的集合)。
 
在32位JVM上,正常的环境下,mark结构体的位字段看起来像下面的(获取更多细节,请参见hotspot/src/share/vm/oops/markOop.hpp):
 
hash:25 —>| age:4 biased_lock:1 lock:2
 
高25位由hashCode()的对象值组成,紧下来4位是对象的存活期(根据仍存活的垃圾集合的数目)。保留3位用于指示对象的同步锁状态。
 
Java 5 介绍了一种新的对象同步途径,叫做Biased Locking(它在Java 6中是默认产生的)。这种观点是基于观察运行时对象的行为得来的——在许多种情况下,对象是仅仅被一个线程锁住的而已。
 
在biased locking中,对象是“biased”指向第一个锁住它的线程——然后这个线程获得更好的locking性能。已经获得bias的线程被记录在mark首部。
 
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2
 
如果另一个线程尝试锁住这个对象,biasing会被撤销(它不会去再获取)——并且从那以后,所有的线程必须明确地锁或者解锁对象。
 
对象可能的状态是:
  • Unlocked
  • Biased
  • Lightweight Locked
  • Heavyweight Locked
  • Marked (only possible during Garbage Collection)
 

在HotSpot Source中的OOP

在HotSpot中,OOP关联类型的继承是十分复杂的。那些被保留在hotspot/src/share/vm/oops中的类型包括了:
  • oop (abstract base)
  • instanceOop (instance objects)
  • methodOop (representations of methods)
  • arrayOop (array abstract base)
  • symbolOop (internal symbol / string class)
  • klassOop
  • markOop
这里有许地古怪的历史偶然——虚拟调度表(vtable)的内容从klassOop中分开,虽然markOop看起来不像其它oop,但是它仍然包含在同一个继承中。
 
一个有趣的地方是OOP可以从直接jmap的命令行工具查看。这地方给出一个堆内容的快照,这堆包括了任何出现在permgen(包括了子类和klassOop要求的支持结构)中的oop。
 

$ jmap -histo 150 | head -18 num #instances #bytes class name ---------------------------------------------- 1: 10555 21650048 [I 2: 272357 6536568 java.lang.Double 3: 25163 5670768 [Ljava.lang.Object; 4: 229099 5498376 com.jclarity.censum.dataset.CensumXYDataItem 5: 39021 5470944 <constMethodKlass> 6: 39021 5319320 <methodKlass> 7: 8269 4031248 [B 8: 3161 3855136 <constantPoolKlass> 9: 119759 2874216 org.jfree.data.xy.XYDataItem 10: 3161 2773120 <instanceKlassKlass> 11: 2894 2451648 <constantPoolCacheKlass> 12: 34012 2271576 [C 13: 87065 2089560 java.lang.Long 14: 20897 2006112 [Lcom.jclarity.censum.CollectionType; 15: 33798 1081536 java.util.HashMap$Entry
 
在尖括号中,入口是各种类型的oop,同时入口如[I和[B分别涉及到int和byte类型的数组。
 
 

HotSpot中断

HotSpot是一个比过于简单的“switch in a while loop”中断风格更高级的中断,后者对于开发者来说通常是更加熟悉的。
 
相反,HotSpot是一个模板中断。这意味着一个最优机器代码的动态调度表会被构建——在使用过程中,对于操作系统和CPU是明确的。多数的字节码指令用汇编语言代码来实现,汇编有着更加复杂的指令,比如查找一个从委派到VM的类文件的常量池的入口。
 
以使它更难于将VM转到新的体系结构和操作系统的为代价,来提高了HotSpot的中断性能。对于新的开发者来说,它也使中断更难去理解。
 
对于开发者来说,就开始而言,通过OpenJDK获取来运行时环境基本的理解,这往往是更好的:
 
  • 大部分环境是用Java写成的
  • 操作系统的可移植性通过本地方法来完成
  • 在堆中,Java对象用OOP描述
  • 在JVM中,Class元数据表达为KlassOOP
  • 在中断模式中,高级的样本中断是为了更高的性能
到这,开发者可以开始研究在jdk仓库中的Java代码了,或者设法掌握它们的C/C++和汇编知识来到HotSpot中做更深层次的探究。
 
关于作者     

Ben Evans
jClarity公司(通过交付性能工具来帮助开发&合作团队的新兴公司)的CEO。他是LJC(伦敦JUC)的组织者和JCP执行委员会(为帮助Java生态圈定义标准的组织)的成员。他是个Java捍卫者,JavaOne中的摇滚明星; 《The Well-Grounded Java Developer》的合著者,还是一个在Java平台,性能,并发和相关主题的定期的发言人。
 
 

推荐阅读