JVM 源码分析

搭建源码调试环境

源码下载地址:unofficial-openjdk/openjdk

  1. OpenJDK9 Hotspot Ubuntu 编译和调试
    mac 环境下搭建:mac 下编译 openjdk1.9 及集成 clion 动态调试

遇到的问题

  1. g++和 gcc 版本不匹配
  2. 一些 Warning 被当作错误报错了
    大部分 Warning 不会影响最终结果,可以关掉:https://blog.csdn.net/desiyonan/article/details/80802066

阅读

  1. 如何找 JDK 中 native 代码的位置?
  2. OpenJDK 源码阅读导航

编译 openjdk

  1. 下载
    代码的下载见 building 文档,使用一个 Mercurial 工具进行源码控制。
  2. 准备工具
    找到这种开放源码的软件,第一步当然是查看 README 了,openjdk 的 README 中告知了 building 文档的位置,相当详细。
    硬件要求
    操作系统要求
    编译器要求,需要 gcc 和 clang,他们的区别见Clang 比 GCC 好在哪里?
    JDK 要求,虽然挺矛盾的,但是一部分模块是用 JDK 写的,所以 Java 编译器也是需要的,一般编译 JDK8 时需要的 JDK 版本是 7 就够了,但是 openjdk 需要 8 才行(不知道为什么),这个 jdk 可以直接使用 apt-get 安装。
    外部库要求,
    其他工具,make、bash、autoconf
  3. configure
    准备所有配置文件
  4. make
    编译源码,默认下只会编译一些必要的部分,如果编译成功可以在 openjdk/build 文件夹下找到目标文件,我的成品所在目录是${openjdk}/build/linux-x86_64-normal-server-release/jdk/bin
  5. run test
    openjdk 使用了一个叫 jtreg 的测试框架,文档有这里还有这里,我设置了一个愚蠢的环境变量 JAVA18HOME 然后尝试着去make -C make编译它,但是还是提示缺少什么org.testing,可能还有什么依赖库没装,这个框架可能是给 oracle 的内部员工或者外星人用的吧?

定位 Object 的 native 方法

  1. 搜索“Object”打开 jdk 下的 Object.c 文件
    看到下面这个方法数组:
    1
    2
    3
    4
    5
    6
    7
    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},
    };
  2. 然后搜索“JVM_MonitorWait”打开 hotspot 下的 jvm.cpp 文件
    里面有下面这条语句:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
    JVMWrapper("JVM_MonitorWait");
    Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
    JavaThreadInObjectWaitState jtiows(thread, ms != 0);
    if (JvmtiExport::should_post_monitor_wait()) {
    JvmtiExport::post_monitor_wait((JavaThread *)THREAD, (oop)obj(), ms);

    // The current thread already owns the monitor and it has not yet
    // been added to the wait queue so the current thread cannot be
    // made the successor. This means that the JVMTI_EVENT_MONITOR_WAIT
    // event handler cannot accidentally consume an unpark() meant for
    // the ParkEvent associated with this ObjectMonitor.
    }
    ObjectSynchronizer::wait(obj, ms, CHECK);
    JVM_END
    我们暂时不知道这几个宏到底什么意思,不过核心大概是最后的那个ObjectSynchronizer::wait吧,这个类定义在 C++文件 synchronizer.cpp 中。
  3. 综上所述
    可以看到如果一个类定义了 native 方法,那么基本可以在 jdk 目录下找到该类的定义(类所在包结构和其在 openjdk 中的定义的路径有什么关联???),至于其实现,从知乎上可以找到一段说明,我不知道对错:

    如果间接调用了 hotspot 的实现(jvm 会以动态库的形式被加载,prims 文件夹里定义了 hotspot 与其他模块的接口及其实现),那么从 hotspot/src/share/vm/prims/jvm.cpp 中可以找到 JVM_Xxx 函数的实现。

为 Object 类添加 native 方法 nop()

根据这里的说法,openjdk 有两种方式来定义 native 函数,一种是按一种特别的方式命名函数,这样加载后就可以和 Java 中的 native 方法对上,另一种是使用 registerNatives()的方式,观察 Object.c 文件可以发现,这种方法先定义了一个 JNINativeMethod 类型的静态数组来做 Java 方法和本地函数的对应,然后使用 JNI 方法注册了一个叫 Java_java_lang_Object_registerNatives 的本地方法来实施这种映射。
当使用第一种方式在 Object.c 中定义一个特殊命名的 nop()函数时,编译不通过,报错显示Incompatible definition of java.lang.Object,我定义的 nop 函数:

1
2
3
4
JNIEXPORT void JNICALL
Java_java_lang_Object_nop(JNIEnv *env, jobject this)
{
}

然后下面是我使用第二种方式定义 nop()函数的过程(虽然也不行):

  1. 修改 Object.c
    methods 数组中添加一行:

    1
    "nop", "()V", (void*)&

    这种定义方法参照了这里

  2. 修改 Object.java
    其所在目录为${openjdk}/jdk/src/java.base/share/classes/java/lang/Object.java,为其添加一个 nop()方法:

    1
    2
    3
    4
    5
    /**
    *
    */
    @HotSpotIntrinsicCandidate
    public native void nop();

    为什么要加注释?为什么不能加 final?

  3. 重编译 make
    虽然只是添加了一个方法,但是几乎所有包下的都要重新编译了,所以编译过程会稍微有点长。

  4. 测试
    使用javap命令反编译可以看到 Object 类中确实已经有了一个 nop()方法:

    1
    javap ${openjdk}/build/linux-x86_64-normal-server-release/jdk/modules/java.base/java/lang/Object.class

    但是实际编译时却报错了,错误大概是Object类不兼容???

  5. native 方法的实现机制
    参见 JNI 原理。

JNI

生成.h头文件

使用 javah 命令,在项目的 src 目录下运行,其中-d 指定目标目录,-jni 是默认选项,生成 jni 头文件,可以加-classpath 指定 class 路径,但必须是绝对路径

1
javah -d ../jni -jni com.tallate.HelloJNI

接下来编译.c 文件,-c 可以生成目标文件(未链接),-I 可以指定#include 查找的目录,不然 jni 有些头文件找不到

1
gcc -c -I "/usr/lib/jvm/java-8-openjdk-amd64/include" -I "/usr/lib/jvm/java-8-openjdk-amd64/include/linux"  HelloJNI.c 

接下来编译成一个.so 链接库

1
gcc HelloJNI.c -I "/usr/lib/jvm/java-8-openjdk-amd64/include" -I "/usr/lib/jvm/java-8-openjdk-amd64/include/linux" -fPIC -shared -o libhello.so

接下来可以在Java中加载

1
2
// 在linux下动态库必须有前缀lib
System.loadLibrary("hello");

调用 native 方法的流程

  1. native 方法编译后多一个 ACC_NATIVE 标志
  2. loadLibrary 方法调用 JVM 的 load 本地方法
  3. load 方法最终调用系统调用 dlopen 加载动态链接库
  4. 调用本地方法时,实际上调用了 JVM 中加载的函数,栈帧压入本地方法栈中(HotSpot 中本地方法栈=虚拟机栈)

修改 Object 类,为其添加一个 native 方法 nop()

锁(内置锁)

  1. 锁的存储结构?
  2. 获取锁的时机?
  3. 释放锁的时机?

线程

和 JDK 的线程操作相关的类有 Object 和 Thread,Object 中包括 wait、notify 和 notifyAll,Thread 中包括 yield、sleep、exit、interrupt、join、start,run 的主要任务是执行用户定义的逻辑,还有一些方法已经被 Deprecated,不必再提

源码中和线程操作相关的一些数据结构

  1. ObjectMonitor
    对象监视器,定义和声明在 objectMonitor.*中,主要包含

  2. ObjectWaiter

  3. ObjectSynchronizer

wait

wait 是 native 方法,实现在 synchronizer.cpp 中,ObjectSynchronizer::wait:

  1. 通过ObjectSynchronizer::inflate方法找到 object 中的ObjectMonitor monitor

  2. 调用 monitor.wait
    wait 方法比较复杂,但是核心过程只有 3 步:

    • ObjectMonitor::AddWaiter(..)将新建立的 ObjectWaiter 对象插入_WaitSet 队列的末尾
    • ObjectMonitor::exit(..)释放锁
    • Self->_ParkEvent->park等待

参考

JNI

  1. 介绍
  2. 生成.so 方法
  3. 修改 java.library.path 方法
  4. Java JNI 实现原理初探
  5. Java Native Interface (JNI) 工作原理
  6. dlopen

线程

  1. java sleep 和 wait 的区别的疑惑?
  2. java 中的 wait 和 notify 实现的源码分析
  3. JVM Thread stop 的源码分析