HotSpot和OpenJDK入门

在本文中,我们将会介绍如何开始使用HotSpot Java虚拟机以及它在OpenJDK开源项目中的实现——我们将会从两个方面进行介绍,分别是虚拟机和虚拟机与Java类库的交互。

HotSpot源码介绍

首先让我们看看JDK源码和它所包含的相关Java概念的实现。检查源码的方式主要有两种:

  • 现代IDE能够附加src.zip(在$JAVA_HOME目录),能够从IDE中访问
  • 使用OpenJDK的源码并导航到文件系统

这两种方式都非常有用,但是重要的是哪种方式比较舒适一点。OpenJDK的源码存储在Mercurial(一个分布式的版本控制系统,与流行的Git版本控制系统相似)中。如果你不熟悉Mercurial,可以查看这本名为“版本控制示例”的免费书,该书介绍了相关的基础内容。

为了检出OpenJDK 7的源码,你需要安装Mercurial命令行工具,然后执行以下命令:

hg clone http://hg.openjdk.java.net/jdk7/jdk7 jdk7_tl

该命令会在本地生成一个OpenJDK仓库的副本。该仓库含有项目的基础布局,但是并没有包含所有的文件——因为OpenJDK项目分别分布在几个子仓库中。

完成克隆之后,本地仓库应该有类似于下面的内容:

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  3boxcat staff    102 14 May 12:54 test

接下来,你应该运行get_source.sh脚本,该脚本是初始克隆内容的一部分。该脚本会填充项目的剩余部分,克隆构建OpenJDK所需要的所有文件。

在我们深入并详细地介绍源码之前,我们必须要有“不惧怕平台源码”的信念。开发者通常会认为JDK源码一定是令人振奋且难以接近的,但这毕竟是整个平台的核心。

JDK源码是固定的、经过良好的审核和测试的,但是并不是那么无法接近。特别是这些源码并不是始终包含Java语言的最新特性。所以我们经常会在其内部找到那些依然没有泛型化的、使用原始类型的类。

对于JDK源码而言,有几个主要的仓库是你应该熟悉的:

jdk

这是类库存在的地方。几乎所有的内容都是Java(本地方法会使用一些C代码)。这是深入学习OpenJDK源码的一个非常好的起点。JDK的类在jdk/src/share/classes目录中。

hotspot

HotSpot虚拟机——这里面是C/C++和汇编代码(还有一些基于Java的虚拟机开发工具)。这些内容非常高级,如果你并不是一个专业的C/C++开发人员那么这些内容会让人有一点难以入手。稍后我们会更加详细地讨论一些入门的好方法。

langtools

对于那些对编译器和工具开发感兴趣的人而言,可以从这里找到语言和平台工具。大部分是Java和C代码——学习这些内容比学习JDK代码要难,但是对于大多数开发者而言还是可以接受的。

还有一些其他的仓库,但是它们可能没有那么重要或者对大多数开发者而言没什么吸引力,这些仓库包括corba、jaxp和jaxws等内容。

构建OpenJDK

Oracle最近开始了一个项目对OpenJDK做了一次全面的修整,并且简化了构建过程。这个项目称为“build-dev”,目前该项目已经完成并且成为了构建OpenJDK的标准方式。对于很多使用基于Unix系统的用户而言,构建过程现在就和安装一个编译器和一个“引导JDK”然后运行三个命令那么简单:

./configure
make clean
make images

如果你想获取更多与构建自己的OpenSDK相关的信息,那么AdoptOpenJDK计划(由伦敦的Java社团创建)是一个不错的起点——这是一个由100多位草根开发者组成的社团,他们都工作在警告清理、小bug解决和OpenJDK 8对主要开源项目的兼容性测试等项目上。

理解HotSpot运行时环境

Java运行时环境正如OpenJDK所提供的那样,由HotSpot JVM和类库(大部分都捆绑到了rt.jar里面)组成。

因为Java是一个可移植的环境,所有需要调用操作系统的内容最终都会由一个本地方法处理。另外,还有一些方法需要JVM的特殊处理(例如类的加载)。这些内容也会通过一个本地调用移交给JVM。

例如,让我们看看原始Object类中本地方法的C代码。Object类的本地源码包含在jdk/src/share/native/java/lang/Object.c文件中,它有六个方法。

Java本地接口(JNI)通常会要求本地方法的C实现按照一种非常特别的方式命名。例如,本地方法Object::getClass()使用通用的命名约定,因此C实现被包含在一个具有如下签名的C函数中:

Java_java_lang_Object_getClass(JNIEnv *env, jobject this)

JNI还有另一种加载本地方法的方式,java.lang.Object类中剩余的5个本地方法就使用了这种方式:

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},};

这5个方法被映射到了JVM的入口点(它们是通过在C方法名上使用JVM_前缀来指定的),——使用registerNatives()的方式(开发人员能够通过这种方式改变Java本地方法到C函数名称的映射)。

Java运行时环境是用Java编写的,仅有很少的与JVM相关的小地方不是。除了代码的执行之外,JVM的主要工作是运行时环境的内务处理和维护,这里是活动Java对象运行时表示赖以生存的地方——Java堆。

OOP和KlassOOP

堆中的任何Java对象都是由一个普通的对象指针(OOP)表示的。在C/C++中一个OOP是一个真正的指针——一个指向Java堆里面某个内存位置的机器字。在JVM进程的虚拟地址空间中,会为Java堆分配一个单独的连续的地址范围,然后用户空间中的这块内存就会完全由JVM进程自己管理,直到JVM因为某些原因需要调整堆大小为止。

这意味着Java对象的创建和收集并不会牵扯到分配和释放内存的系统调用。

一个OOP由两个机器字头组成,它们被称为Mark和Klass字,之后是这个实例的成员字段。对于数组而言,在成员字段之前还有一个额外的字头——数组的长度。

之后我们会更加详细地介绍Mark和Klass字,但是它们的名字也暗示了一些内容——Mark字用于垃圾收集(用于标记——扫描的标记部分),而Klass字则是一个指向类元数据的指针。

在OOP头之后,实例字段会按照它在字节码中的特定顺序进行排列。如果你想了解更精确的细节,可以阅读NitsanWakart的博客文章“理解Java对象的内存分布”。

基本字段和引用字段都会排列在OOP头后面——当然,对象的引用也是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位的HotSpot上OOP头会占用8个字节(在64位HotSpot上占用16个字节)。

一个OOP定义的总体大小是2个机器字加上所有实例字段的大小。

引用类型的字段实际上是指针——在所有健全的处理器架构中该指针都将占用一个机器字。

因此,因为我们有一个int字段,两个引用字段(对类型为K和V的对象的引用)和一个Entry字段,所以整个大小为2个字(头)+1个字(int)+3个字(指针)。

存储一个HashMap.Entry对象总共需要24个字节(6个字)。

KlassOOP

Klass字是OOP头中最重要的部分之一。它是指向这个类元数据的指针(它由一个称为KlassOOP的C++类型表示)。在这些元数据当中最重要的是这个类的方法,它们被表示为一个C++虚拟方法表(一个“vtable”)。

我们并不想让所有的实例都携带着方法的所有细节,因为这样做效率会非常低,所以使用了一个vtable在实例之间共享这些信息。

需要注意的是,KlassOOP和类加载操作所产生的类对象是不同的。这两者之间的区别可以概括为下面两个方面:

  • Class对象(例如String.class)仅仅是普通的Java对象——它们和任何其他的Java对象(实例OOP)一样都是OOP,和所有其他的对象那样拥有同样的行为,同时它们也能够被放入Java变量中。
  • KlassOOP是类元数据的JVM表示——它们通过一个vtable结构携带类的方法信息。我们不能直接从Java代码中获得到KlassOOP的引用——它们存在于堆的Permgen区域。

记住这个区别最容易的方式是,将KlassOOP当作是类对象的JVM级别的“镜像”。

虚拟调度

KlassOOP的vtable结构直接与Java的方法调度和单继承相关。要记住,默认情况下Java的实例方法调度是虚拟的(它使用被调用实例对象的运行时类型信息查找方法)。

在KlassOOPvtable中这是通过“常量vtable偏移”实现的。这意味着,重载方法在vtable中的偏移和它所重载的父类(包括祖父等)中的方法实现具有相同的偏移。

在这种情况下虚拟调度就很容易实现了,只需要简单地追溯继承层次(按照类——父类——祖父类的层次追溯)并寻找方法的实现就可以了(在vtable中的偏移始终相同)。

例如,这意味着在所有的类中toString()方法在vtable中的偏移始终相同。这个vtable结构有助于单继承,同时在使用JIT编译代码的时候也能够做一些非常好的优化。

(单击图片放大)

OOP头的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引入了一种新的对象同步方式,称为偏向锁(在Java 6中是默认的锁机制)。该方案的灵感来源于对对象运行时行为的观察——在很多情况下对象永远只会被一个线程锁定。

在偏向锁中,一个对象会“偏向于”锁定它的第一个线程——然后这个线程会实现更好的锁性能。获得偏向的线程会被记录在Mark头中。

JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2

如果另一个线程试图锁定对象,那么这个偏向就会被取消(并且不会被重新获得),并且自此之后所有的线程都必须明确地锁定和解锁对象。

对象的状态可能会是:

  • 未锁定
  • 偏向的
  • 轻量级锁定
  • 重量级锁定
  • 标记的(仅在垃圾收集期间有效)

HotSpot源码中的OOP

HotSpot源中相关的OOP类型层次非常复杂。这些类型被保存在hotspot/src/share/vm/oops中,包括:

  • oop (抽象基础)
  • instanceOop (实例对象)
  • methodOop (方法表示)
  • arrayOop (数组抽象基础)
  • symbolOop (内部符号/字符串类)
  • klassOop
  • markOop

有一些稍微奇怪的历史性事件——虚拟调度表(vtable)的内容和klassOOP是分开保存的,markOOP和其他OOP看起来完全不同,但是它依然包含在同样的层次中。

一个非常有趣的地方是,我们可以从jmap命令行工具中直接看到OOP。它对堆中的内容做了一个快照,包括出现在permgen中的所有OOP(包括子类和KlassOOP所需的支持结构)。

$ 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解释器

开发者通常会比较熟悉那种“在一个while循环中切换”的解释器,但是HotSpot比这种类型的解释器要更加先进。

HotSpot是一个模板解释器。这意味着它会构建一个动态的、优化的机器码调度表——特定于用户所使用的操作系统和CPU。大部分的字节码指令都是使用汇编语言代码实现的,仅有非常复杂的指令会被委托给虚拟机处理,例如从一个类文件的常量池中查找一个入口。

这提升了HotSpot解释器的性能,但是代价是难以将虚拟机移植到新的架构和操作系统上。同是对于新开发者而言也增加了他们理解解释器的难度。

对于新手开发者而言,对OpenJDK所提供的运行时环境有一个基础的理解是非常必要的:

  • 环境中的大部分都是使用Java编写的
  • 通过本地方法实现操作系统的可移植性
  • 堆中的Java对象由OOP表示
  • JVM中的类元数据用KlassOOP表示
  • 有一个先进的高性能模板解释器,哪怕是解释执行模式下的性能

到现在为止,开发者已经能够开始探索JDK仓库中的Java代码了,也能够尝试着积累自己的C/C++和汇编知识去深入学习HotSpot了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值