深入jvm内部掌握java线程的运行原理

深入jvm内部掌握java线程的运行原理

原文链接 mp.weixin.qq.com

上一章介绍了JNI的主要功能,本章通过一个实例演示JVM源码调试过程,从中可以看到JNI在Java多线程管理中起到的作用,更主要的是理解JVM的多线程模型。

调试环境

openjdk version “9.0.4-internal”

OpenJDK Runtime Environment (slowdebug build 9.0.4-internal+0-adhoc.$hostname.jdk9u-1b1226687b89)

OpenJDK 64-Bit Server VM (slowdebug build 9.0.4-internal+0-adhoc.$hostname.jdk9u-1b1226687b89, mixed mode)

mac OS high sierra version 10.13.4

clion 2018.1

lldb-470.99.0

调试目标

从源码层面认识java程序在JVM内部的启动过程

理解java线程在JVM内部的实现机制

理解java线程与内核线程的映射关系

测试程序

如下图:

测试程序Test1.java在主线程中打印线程名称,之后修改线程名称,调用startThread()方法声明一个新的线程,设定线程名称为“bbbb”,启动该线程。名为“bbbb”的线程中首先打印3次线程名称,然后修改线程名称为“aaaa”,并输出新的线程名称。

调试过程

编译Test1.java程序:

~/Desktop/jdk9u-1b1226687b89/build/macosx-x86_64-normal-server-slowdebug/images/jdk/bin/javac Test1.java

在clion中导入openjdk9u源码,调试中会用到jdk和hotspot两个分支,如下图:

配置debug环境,其中target对应工程名,executable对应~/Desktop/jdk9u-1b1226687b89/build/macosx-x86_64-normal-server-slowdebug/images/jdk/bin/java文件,Program argument对应executable的程序参数,包括class文件名和必要的执行参数,例如:-XX参数,class main函数的参数等,Working directory对应class文件的目录,放在~/Desktop,见下图:

编译Test1.java:~/Desktop/jdk9u-1b1226687b89/build/macosx-x86_64-normal-server-slowdebug/images/jdk/bin/javac Test1.java,运行~/Desktop/jdk9u-1b1226687b89/build/macosx-x86_64-normal-server-slowdebug/images/jdk/bin/java Test1查看执行结果:

启动debug,如下图,注意:启动后,需要设置lldb,process handle SIGSEGV –stop=false,否则出现segsegv错误时,会hang住debug过程。从图中可以看到,java程序从jdk launcher下的main.c启动,argv指向java的输入参数,此时启动一个本地线程Thread-1-<com.apple.main-thread>。

launcher调用hotspot的jni.cpp,创建java VM。从下图中可以看到它是通过jni接口调用创建JVM的,Thread-5是JDK的launcher创建的一个本地线程,继续后面的实验我们就会知道它对应的就是java中的main线程,而从java程序的层面看到的只是该线程的部分表象。

从jni.cpp跳转到thread.cpp,执行JVM的初始化,创建JVM的main线程,创建VMThread,创建GC线程等。一下子创建如此多的线程,想必大家有懵圈的感觉,的确如此,Thread.cpp中的jint Threads::create_vm(JavaVMInitArgs args, bool canTryAgain)方法非常重要,它承担了JVM初始化的工作,像java程序里的类加载,main函数的启动,GC的启动,java线程与内核线程的映射都是在这个方法中完成。因此该方法的调试是理解JVM中线程管理的关键,是深入理解各个线程工作过程,JVM线程调度等的入口。

创建main线程对应的内核线程,并把main线程attaching到新建的内核线程上。这里大家一定会有疑问,Thread-5不已经是主线程了吗,为何又要创建主线程?JavaThread又是什么鬼?对应的内核线程在哪呢?这几个问题的答案都在下图中的几行代码中了。首先Thread-5的确是主线程了,它负责创建VM实例,VM初始化,创建VM其他后台线程,但它暂时还没有生成内核线程(即OS线程或OSThread,两者在本文中等价);而JavaThread是thread类的一个子类,JVM中thread类是所有线程对象的根类。JavaThread类的对象main_thread创建时会把Thread-5和新创建一个OS线程关联起来。JavaThread对象是java层面线程在JVM内部的代理,它们之间为一一对应关系,JavaThread和OSThread也是一一对应关系,从而java线程和OS线程也是一一对应关系。JavaThread在JVM内部也被称为NativeThread。

修改主线程名称前,未显示调用java线程的setName之前,JVM会分配一个默认的java线程名,main线程的名称就是“main”,而对应的主线程Thread-5并不会改变,这是因为java线程和JavaThread直接对应,java线程的名称直接对应的是JavaThread的名称。而java中main线程比较特殊,它除了对应一个JavaThread线程还有一个本地线程即Thread-5线程,本地线程的名称格式为:“[java:$JavaThread名称]”。

修改主线程名称后,Thread-5的名称也已经改变。另外源码中jthread对应jni里面的java线程引用,它负责连接java线程和jvm。

另外一个java线程名称也已经修改,

最终运行结果,

调试结论

java程序的启动过程是先从jdk的launcher的main.c开始,xos下会启动两个线程,参见实验中的thread-1和thread-2;通过jni,launcher创建jvm实例,启动主线程,这个主线程也是jvm主线程,包含了java程序对应的main线程(对应一个JavaThread线程),并attaching到一个OSThread。

Java层面线程在JVM内部的实现过程是,首先java程序通过一个jni对象jobject,源码中字段名称为jthread,但事实上它不是一个thread,而且每次线程调用时,jobject都不同,只是通过jobject查找到oop(ordinary object pointer,普通对象指针,描述对象实例信息),通过oop定位到JavaThread对象,即整个的调用过程是:

java thread->jni object->oop->JavaThread(native thread)->OS thread。

JVM规范并未明确规定java线程和内核线程的映射关系,可以是一对一,多对一,或多对多。Hotspot采用的是一对一关系。

JVM中线程类型,继承关系如下图:

JavaThread的状态如下图几种(参见globalDefinitions.hpp),相对每一种state,_trans的值相应增1:

到此,JVM内部的线程调试告一段落。本系列文章的目的是关注java层面的多线程编程,理解其原理,揭示可能遇到的坑,因此JVM内部的多线程管理机制暂不过多研究,以后做为专题再深入学习。

本文调试视频:

本系列文章列表:

Java编程思想之多线程(一)

Java编程思想之多线程(二)

Java编程思想之多线程(三)

    原文作者:java虚拟机
    原文地址: https://segmentfault.com/a/1190000016415669
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞