JVM线程生命周期的native实现
本文不是入门文章,对读者的理论基础与折腾要求都有点高
- 了解Java层Thread的各种玩法
- 知道什么是pthread,什么是系统调用,什么是Glibc/NPTL(Native POSIX Thread Library)
- 了解操作系统内核中线程的理论流程
- 需要会配置Clion/VSCode
配置编译参数并测试
首先还是按照上次讲的使用Clion(GDB)调试小型JVM源码方法编译,只是编译参数改一下,以免日志过多
- ./configure --with-classpath-install-dir=/tmp/classpath --enable-trace
+ ./configure --with-classpath-install-dir=/tmp/classpath --enable-tracethread
编译成功后,分别用直接运行与strace进行测试
➜ cd $HOME
➜ $HOME/Desktop/jamvm-2.0.0/src/jamvm -cp . A
....
Hello
➜ strace $HOME/Desktop/jamvm-2.0.0/src/jamvm -cp . A
....
clone(child_stack=0x7f96bf542ff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f96bf5439d0, tls=0x7f96bf543700, child_tidptr=0x7f96bf5439d0) = 17140
....
new Thread创建的源码分析
首先构造一个Java代码创建一个线程,并编译为Class文件
//A.java
class A{
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"233");
}
}).start();
}
}
首先在Java层中使用普通JDK进行断点,并观察断点运行时的线程名称,位置如下
- start()
- run()
- exit()
- ThreadGroup.remove()断点
接着看Java中线程的源码
通过对Thread的<init>
与start
进行分析,可以发现start
中调用了native方法。这里的start
需要注意,在OracleJVM中使用了start0作为native方法,而在GNU Classpath/Android中出使用了java.lang.VMThread.create(this, stacksize);
实现,本文主要分析java.lang.VMThread.create
的实现createJavaThread。
在JVM上通过断点调试可以发现,此native方法被JVM路由到thread.c的createJavaThread
中
我们在这里打断点
// src/thread.c
void createJavaThread(Object *jThread, long long stack_size) {
...
Thread *thread = sysMalloc(sizeof(Thread));
...
// native 与 Java 线程的上下文环境进行绑定
thread->ee = ee;
ee->thread = jThread;
ee->stack_size = stack_size;
...
disableSuspend(self);
// 通过系统调用创建native线程, `threadStart`是创建成功后的回掉函数指针
if(pthread_create(&thread->tid, &attributes, threadStart, thread)) {
...
return;
}
/* 自旋等待线程变更状态,这里一般来说将会变为 RUNNING */
pthread_mutex_lock(&lock);
/* Wait for thread to start */
while(classlibGetThreadState(thread) == CREATING)
pthread_cond_wait(&cv, &lock);
pthread_mutex_unlock(&lock);
enableSuspend(self);
}
当通过系统调用创建线程完成后,在新的线程中调用threadStart
函数指针
// 新线程启动后将立刻调用的函数
void *threadStart(void *arg) {
Thread *thread = (Thread *)arg;
Object *jThread = thread->ee->thread;
/* Parent thread created thread with suspension disabled.
This is inherited so we need to enable */
enableSuspend(thread);
/* Complete initialisation of the thread structure, create the thread
stack and add the thread to the thread list */
initThread(thread, INST_DATA(jThread, int, daemon_offset), &thread);
/* Add thread to thread ID map hash table. */
addThreadToHash(thread);
/* Set state to running and notify creating thread */
// 设置状态为 RUNNING,通过`pthread_cond_broadcast`结束`pthread_cond_wait`的状态
signalThreadRunning(thread);
/* Execute the thread's run method */
/* 开始执行Java代码 class=java.lang.Thread,nam=run,sign=`()v` */
executeMethod(jThread, CLASS_CB(jThread->class)->method_table[run_mtbl_idx]);
/* Run has completed. Detach the thread from the VM and exit */
/* 这里执行了java.lang.ThreadGroup: removeThread(Ljava/lang/Thread;)V */
detachThread(thread);
TRACE("Thread %p id: %d exited\n", thread, thread->id);
return NULL;
}
如何通过Class引用打印名称 slash2DotsDup(CLASS_CB(class)->name)
可以发现,总体流程是非常清晰的
- 主线程通过pthread_create创建新线程
- 新线程执行
Thread.run()
的Java代码段,执行完成后detach,移除ThreadGroup,结束线程
总的来说,这里分析并不是非常细致,主要是把流程先通起来,后续有具体例子(比如线程池)时再分析
总结
在JamVM中
- Java线程与pthread线程一一对应,通过内核调度实现。但是不同厂商不一定。
- 在Java代码中Runnable中的代码段的执行实际上通过
pthread_create
的函数指针threadStart
进行包装后,最终调用executeMethod
实现解释字节码
附录
pthead相关知识
在线程模型的综述中已经讲过pthread相关知识,这里再回顾一下。JamVM使用pthread的头文件进行开发,在Linux中一般由Glibc实现,最后通过SYSCALL系统调用通过内核去创建线程。
java.lang.Thread
|
POSIX thread(user mode){
1. pthread_create(pthread.h,标准库头文件)
2. bionic/glibc等标准库下的so文件,进行SystemCall
3. 用户态陷入内核态
}
|
Kernal thread(kernal mode)
pthead_create参数解释
入参如下
extern int pthread_create (pthread_t *__restrict __newthread,
const pthread_attr_t *__restrict __attr,
void *(*__start_routine) (void *),
void *__restrict __arg) __THROWNL __nonnull ((1, 3));
相关参数
_newthread
: 即tid, 通过某种算法计算出唯一的Id_attr
: 线程类型,初学者可以配置为默认,即NULL_start_routine
: 是一个函数指针,类似于动态语言中的闭包(Closure)。线程创建后将执行此函数_arg
: 供_start_routine
使用的入参
Glibc中NPTL的pthread_create的实现
下次有人再问你线程与进程的区别这种烂大街的问题,你就回答它们就是flag不同,导致(通过COW)共享的内存也不同而已
// glibc-2.25/sysdeps/unix/sysv/linux/createthread.c
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);
TLS_DEFINE_INIT_TP (tp, pd);
// 此部分调用 external `__clone`
if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS,
clone_flags, pd, &pd->tid, tp, &pd->tid)
== -1))
return errno;
__clone
的实现在内核中,通过 x86_64 ABI与网上找的例子,最终汇编代码如下
eax = 120 (syscall number for sys_clone)
ebx = unsigned long flags
ecx = void *child_stack
edx = void *ptid
esi = void *ctid
edi = struct pt_regs *regs
int 80H
内核收到80H中断请求后,将进行内核态线程的创建与调度,后面内核线程以后再写...
JVM中自动启动的线程
在JVM中,创建了main线程(Native线程,对应 JVM下的用户主线程) + 如下4个Java-level的线程 ,可以通过对createVMThread
进行findUsage定位
1. GC线程
通过WhileLoop与Sleep每隔1000ms进行一次GC,这里后期将专门分析GC流程(当初买的一本GC实现终于可以用啦)
/* Create and start VM thread for asynchronous GC */
if(args->asyncgc)
createVMThread("Async GC", asyncGCThreadLoop);
2. Reference处理线程
这里配合GC的线程,共2个,日后在详细讲
/* Create and start VM threads for the reference handler and finalizer */
createVMThread("Finalizer", finalizerThreadLoop);
createVMThread("Reference Handler", referenceHandlerThreadLoop);
3. 信号处理线程
通过WhileLoop+sigwait()
系统阻塞监听信号实现处理 SIGINT
与 SIGQUIT
/* Create the signal handler thread. It is responsible for
catching and handling SIGQUIT (thread dump) and SIGINT
(user-termination of the VM, e.g. via Ctrl-C). Note it
must be a valid Java-level thread as it needs to run the
shutdown hooks in the event of user-termination */
createVMThread("Signal Handler", classlibSignalThread);
参考
- 操作系统真象还原: 类似于读书笔记吧,功力在普通国产书之上
- http://syscalls.kernelgrok.com/: 在线查阅各种系统调用