微信号:infoqchina

介绍:有内容的技术社区媒体

HotSpot和OpenJDK入门

2013-10-18 21:04 孙镜涛

在本文中,我们将会介绍如何开始使用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项目分别分布在几个子仓库中。


接下来,你应该运行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个本地方法就使用了这种方式。


这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对象的大小(在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所需的支持结构)。


HotSpot解释器


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


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


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


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

  • 环境中的大部分都是使用Java编写的

  • 通过本地方法实现操作系统的可移植性

  • 堆中的Java对象由OOP表示

  • JVM中的类元数据用KlassOOP表示

  • 有一个先进的高性能模板解释器,哪怕是解释执行模式下的性能

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


关于作者


Ben Evans 是jClarity公司的CEO,这是一家为开发和运营团队提供性能工具的新公司。他是LJC(伦敦JUG)的组织者之一,同时也是JCP执行委员会中的一员,他帮助Java生态系统定义标准,是一个JavaOne巨星,《Java程序员修炼之道》一书的合著者,他会定期地针对Java平台、性能、并行处理等相关主题发表公开演讲。


注:原文较长,这儿为部分节选内容,请点击原文链接查看全文。


***********************************

本文来自InfoQ微信公众账号:infoqchina

1、回复“今日新闻”,查看今天更新的新闻;

2、回复“今日英文”,查看今天英文站的更新;

3、回复“文章 +关键词”,搜索关键词相关内容;

4、回复“QCon”,了解QCon大会相关信息;

5、回复“活动”,了解最近InfoQ组织的线下沙龙;

6、回复“架构师”,获取《架构师》下载地址;

7、回复“投稿”,了解投稿和加入编辑团队的流程。

***********************************

 
InfoQ 更多文章 Facebook如何实现PB级别数据库自动化备份 学术派Google软件工程师Matt Welsh谈移动开发趋势 Spotify为什么要使用一些“无聊”的技术? 妹纸们放假了,汉纸们做啥? 大多数重构可以避免
猜您喜欢 敏捷破冰之旅(六) 【干活推荐】哪本书是对程序员最有影响、每个程序员都该阅读的书? 锤子营销:做精英手机恶心“屌丝” 开源3D手绘软件Cashew 神解释:向外行介绍程序员工作的复杂程度