写点什么

HotSpot 和 OpenJDK 入门

2013 年 10 月 14 日

在本文中,我们将会介绍如何开始使用 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 了。

关于作者

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

查看英文原文 Getting Started with HotSpot and OpenJDK

2013 年 10 月 14 日 22:398345
用户头像

发布了 321 篇内容, 共 102.2 次阅读, 收获喜欢 6 次。

关注

评论

发布
暂无评论
发现更多内容

首次排查 OOM 实录

AI乔治

Java 架构 OOM

架构师3期3班-week1-作业

zbest

作业 week1

极客时间架构师培训 1 期 - 第 9 周作业

Kaven

架构师训练营第 1 期第九周总结

Leo乐

极客大学架构师训练营

微服务手册:分库分表从分析到实践,不再停留只会说分库分表

互联网应用架构

分库分表

native关键字作用到底是什么?

秦怀杂货店

Java 源码 源码刨析 native

架构一期第九周作业

Airs

关于开发排期

张明森

JVM垃圾回收及秒杀系统

天天向上

极客大学架构师训练营

架构师训练营第 1 期 - 第 9 周课后练习

Anyou Liu

极客大学架构师训练营

极客时间架构师训练营 1 期 - 第 9 周总结

Kaven

Week5 - 技术选型 - 缓存,队列,负载均衡

evildracula

学习 架构

架构师3期3班-week1-总结

zbest

总结 week1

架構師訓練營 week9 作業

ilake

架构作业--第九周

Nick~毓

斐波那契查找

ilovealt

算法和数据结构

Java核心基础——动态代理、静态代理

老农小江

java基础 代理模式

架构师训练营第五周作业

李日盛

Python进阶——如何实现一个装饰器?

Kaito

Python

架构师训练营第五周作业

丁乐洪

架构师训练营第 1 期 - 第 9 周学习总结

Anyou Liu

极客大学架构师训练营

深入理解r2dbc-mysql

程序那些事

响应式编程 R2DBC 程序那些事 响应式数据库 r2dbc-myql

第五周作业一

lithium

架构师训练营第 1 期第九周作业

Leo乐

极客大学架构师训练营

架构师入门学习感悟五

莫问

架構師訓練營第 1 期 - 第 09 周總結

Panda

架構師訓練營第 1 期

第九周课后练习

高兵

架構師訓練營第 1 期 - 第 09 周作業

Panda

架構師訓練營第 1 期

架構師訓練營 week9 總結

ilake

架构师训练营第五周学习笔记

李日盛

笔记

架构师训练营第九周总结

_

极客大学架构师训练营 第九周总结

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

HotSpot和OpenJDK入门-InfoQ