前言
之前看JVM内存结构时,看到了《深入理解JVM》这本书说“每个线程都有一个程序计数器,记录了当前执行字节码的位置”。但是想起来JVM的线程是委托OS实现的,或者说,Java线程映射到了OS线程,那这个PC记录的字节码指令位置到底是什么?
OS的线程那可是正儿八经的C线程,C线程的PC保存二进制指令的位置,他们是如何一一对应的?难道是每个字节码对应一个本地机器码吗?但是Java还有解释执行器这个东西,他是一条一条翻译的。?属实想了好久,后来在谷歌上翻了不少时间,勉强找到一些解释。
先声明一下,以下内容部分是我自己理解叙述的,可能会有谬误。
要求
- 1⃣️一个可以debug的JDK,一个谷歌。
- 2⃣️可以阅读C++代码。
过程
首先,我们先试着定义自己的Java线程。因为我昵称是CodeWithBuff嘛,所以前缀就是CWB:
/**
* @author CodeWithBuff(给代码来点Buff)
* @device iMacPro
* @time 2021/6/29 1:27 下午
*/
public class CWBThread {
private String msg;
public CWBThread(String msg) {
this.msg = msg;
}
public void run() {
System.out.println(msg);
}
public void start() {
start0();
}
private native void start0();
}
复制代码
我们模仿JVM,整个了start0()方法,它是native的,所以我们需要在C++里实现。使用它很简单:
new CWBThread("aaa").start();
复制代码
即可。
既然我们要模仿JVM,那就把线程的创建,销毁等操作,也学JVM一样,丢给OS去做!在这里我不打算学JVM在JDK里面添加动态链接库,而是通过JNI的方式来实现。
在终端输入:
javac /.../CWBThread.java -h /.../[目录]
复制代码
来生成我们需要实现的本地方法的头文件,不出意外你会得到这样的.h文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_codewithbuff_javathread_CWBThread */
#ifndef _Included_com_codewithbuff_javathread_CWBThread
#define _Included_com_codewithbuff_javathread_CWBThread
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_codewithbuff_javathread_CWBThread
* Method: start0
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_codewithbuff_javathread_CWBThread_start0
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
复制代码
然后就是实现它的方法了,我是用CLion编写,在此之前需要链接jni.h和jni_md.h两个文件,否则会失败。所以我的CMakeLists文件如下:
cmake_minimum_required(VERSION 3.19)
project(JavaThreadLearn)
set(CMAKE_CXX_STANDARD 20)
# 这里改成你自己的文件位置和文件名
add_executable(JavaThreadLearn src/main/cpp/com_codewithbuff_javathread_CWBThread.h src/main/cpp/cwb_thread.cpp)
# 这里记得修改成你自己的JDK目录
include_directories(/Library/Java/JavaVirtualMachines/jdk-15.0.2.jdk/Contents/Home/include)
include_directories(/Library/Java/JavaVirtualMachines/jdk-15.0.2.jdk/Contents/Home/include/darwin)
复制代码
之后呢,我们就可以编写对应的cpp文件了,编写之后如下:
//
// Created by joker on 2021/6/29.
//
#include <iostream>
#include <pthread.h>
#include "com_codewithbuff_javathread_CWBThread.h"
using namespace std;
class CWBThreadWrapper {
private:
JavaVM* javaVm;
jobject cwbThreadObject;
JNIEnv* attachToJVM();
public:
CWBThreadWrapper(JNIEnv *env, jobject obj);
void callRunMethod();
~CWBThreadWrapper();
};
JNIEnv *CWBThreadWrapper::attachToJVM() {
JNIEnv *jniEnv;
if (javaVm->AttachCurrentThread((void **)&jniEnv, nullptr) != 0) {
cout << "Attach failed.\n";
}
return jniEnv;
}
CWBThreadWrapper::CWBThreadWrapper(JNIEnv *env, jobject obj) {
env->GetJavaVM(&(this->javaVm));
this->cwbThreadObject = env->NewGlobalRef(obj);
}
CWBThreadWrapper::~CWBThreadWrapper() {
javaVm->DetachCurrentThread();
}
void CWBThreadWrapper::callRunMethod() {
JNIEnv *env = attachToJVM();
jclass clazz = env->GetObjectClass(this->cwbThreadObject);
jmethodID methodId = env->GetMethodID(clazz, "run", "()V");
if (methodId != nullptr) {
env->CallVoidMethod(this->cwbThreadObject, methodId);
} else {
cout << "Can't find run() method.\n";
}
}
void *thread_entry_pointer(void *args) {
cout << "Start set thread entry pointer.\n";
CWBThreadWrapper *cwbThreadWrapper = (CWBThreadWrapper *) args;
cwbThreadWrapper->callRunMethod();
delete cwbThreadWrapper;
return nullptr;
}
JNIEXPORT void JNICALL Java_com_codewithbuff_javathread_CWBThread_start0(JNIEnv *jniEnv, jobject cswThreadObject) {
CWBThreadWrapper *cwbThreadWrapper = new CWBThreadWrapper(jniEnv, cswThreadObject);
pthread_attr_t pthreadAttr;
pthread_attr_init(&pthreadAttr);
pthread_attr_setdetachstate(&pthreadAttr, PTHREAD_CREATE_DETACHED);
pthread_t pthread;
if (pthread_create(&pthread, &pthreadAttr, thread_entry_pointer, cwbThreadWrapper)) {
cout << "Create error.\n";
} else {
cout << "Start a linux thread.\n";
}
}
复制代码
这代码阅读起来问题不大,就是一个简单的JNI本地方法调用。因为我们把线程的创建交给了pthread来完成,我们也不需要考虑为线程插入安全点,安全区域,解释器入口等JVM才有的操作,所以总代码不多,功能单一。
现在一切就绪,我们把这个cpp文件编译成平台相关的动态链接文件,因为我是macOS,所以后缀是jnilib,输入指令:
g++ -I[你的JDK的位置]/jdk-15.0.2.jdk/Contents/Home/include -I[你的JDK的位置]/jdk-15.0.2.jdk/Contents/Home/include/darwin -dynamiclib [你的cpp文件的位置]/cwb_thread.cpp -o libCWBThread.jnilib
复制代码
最后会生成一个jnilib的文件在你的C++工程文件夹下面。然后我们通过System.load()方法加载这个文件到JVM中去,JVM就可以为我们的native的start0()方法调用C++方法了。
如下所示:
public class Main {
public static void main(String[] args) {
System.load("[你的C++工程位置]/libCWBThread.jnilib");
new CWBThread("aaa").start();
new CWBThread("bbb").start();
}
}
复制代码
我们可以看到这样的输出:
此时我们通过在C++中创建一个线程,在线程中传入CWBThread的对象,然后通过这个对象调用它的run()方法来实现类似JVM的Thread创建运行。
总结
到此结束了吗?那我们一开始提的问题,又怎么回答呢?
现在我们通过我们创建的这个小的自定义线程来理一理:首先,pthread属于C的方法,JVM也无非是调用OS的thread create来创建线程,OS的底层实现也是pthread;其次,我们通过在pthread创建时传入我们的JavaThread对象,然后调用它的run()方法来实现线程中执行run()的功能;最后执行完毕,线程因为是C线程,由OS负责销毁,我们啥也没干就结束了。
现在我们忽略JVM在为JavaThread创建OS线程时插入的安全点等操作,单纯考虑最简单的功能。
众所周知Java是需要解释器的,一个字节码解释一下,执行一下(翻译成机器码执行)。但是解释器属于JVM的,线程又是映射到了OS线程,run()跑在pthread,也就是跑在C里,解释器是怎么工作的呢?这里我们先不谈JIT,JIT是为了翻译成热点代码提升效率存在的。
后来我在StackOverflow和知乎上找到了答案。
每个线程的执行分为两种:解释器直接执行+本地方法执行。如果是本地方法执行,解释器不做任何行为;如果是解释器执行,则解释执行当前字节码,如有必要,由JIT翻译成本地机器码运行。
Java字节码必须通过解释器执行,所以即使是在pthread中的run()方法,它所包含的字节码也必须由解释器执行。
(我的理解)解释器+run()是一起运行在pthread中的,它们俩组合运行,而不是run()单独跑在操作系统线程中,也不是多个run()共享一个解释器。解释器只是一个程序,相当于在run()之前插入一些代码,大概是这样的。
那些答案在强调Java只能以字节码+解释器执行,JIT只是把字节码中的热点代码编译成本地码加快速度,但不等于Java字节码可以直接跑在机器上(翻译之后可以)。既然Java字节码只能通过解释器运行,而run()里面保存的是字节码,那么由pthread运行的run()必然需要解释器介入。
顺带一提,对于方法调用,在同一线程中,只是解释器入口处的栈帧+字节码发生了替换而已。不同线程拥有自己的解释器,彼此独立;这里需要强调的是解释器是一个程序,不是一个实例对象,它位于线程最开始的地方,接受一个方法的栈帧+字节码作为参数,然后解释执行字节码。
参考
对于OpenJDK而言,是不是每个Java线程都对应一个执行引擎线程? – ETIN的回答 – 知乎