《JVM G1源码分析和调优》 —2.6 线程
2.6 线程
线程是程序执行的基本单元,在JVM中也定义封装了线程。图2-1是JVM的线程类图。
图2-1 JVM线程类结构图
这里只介绍G1中涉及的几类线程:
JavaThread:就是要执行Java代码的线程,比如Java代码的启动会创建一个JavaThread运行;对于Java代码的启动,可以通过JNI_CreateJavaVM来创建一个JavaThread,而对于一般的Java线程,都是调用java.lang.thread中的start方法,这个方法通过JNI调用创建JavaThread对象,完成真正的线程创建。
CompilerThread:执行JIT的线程。
WatcherThread:执行周期性任务,JVM里面有很多周期性任务,例如内存管理中对小对象使用了ChunkPool,而这种管理需要周期性的清理动作ChunkPool
Cleaner;JVM中内存抽样任务MemProfilerTask等都是周期性任务。
NameThread:是JVM内部使用的线程,分类如图2-1所示。
VMThread:JVM执行GC的同步线程,这个是JVM最关键的线程之一,主要是用于处理垃圾回收。简单地说,所有的垃圾回收操作都是从VMThread触发的,如果是多线程回收,则启动多个线程,如果是单线程回收,则使用VMThread
进行。VMThread提供了一个队列,任何要执行GC的操作都实现了VM_GC_Operation,在JavaThread中执行VMThread::execute(VM_GC_Operation)把GC操作放入到队列中,然后再用VMThread的run方***询这个队列就可以了。当这个队列有内容的时候它就开始尝试进入安全点,然后执行相应的GC任务,完成GC任务后会退出安全点。
ConcurrentGCThread:并发执行GC任务的线程,比如G1中的ConcurrentMark
Thread和ConcurrentG1RefineThread,分别处理并发标记和并发Refine,这两个线程将在混合垃圾收集和新生代垃圾回收中介绍。
WorkerThread:工作线程,在G1中使用了FlexibleWorkGang,这个线程是并行执行的(个数一般和CPU个数相关),所以可以认为这是一个线程池。线程池里面的线程是为了执行任务(在G1中是G1ParTask),也就是做GC工作的地方。VMThread会触发这些任务的调度执行(其实是把G1ParTask放入到这些工作线程中,然后由工作线程进行调度)。
从线程的实现角度来看,JVM中的每一个线程都对应一个操作系统(OS)线程。JVM为了提供统一的处理,设计了JVM线程状态,代码如下所示:
hotspot/src/share/vm/classfile/javaClasses.hpp
NEW // 新创建线程
RUNNABLE // 可运行或者正在运行
SLEEPING // 调用Thread.sleep()进入睡眠
IN_OBJECT_WAIT // 调用Object.wait()进入等待
IN_OBJECT_WAIT_TIMED // 调用Object.wait(long)进入等待,带有过期时间
PARKED // JVM内部使用LockSupport.park()进入等待
PARKED_TIMED // JVM内部使用LockSupport.park(long)进入等待,
// 带有过期时间
BLOCKED_ON_MONITOR_ENTER // 进入一个同步块
TERMINATED // 终止
JVM可以运行在不同的操作系统之上,所以它也统一定义了操作系统线程的状态,代码如下所示:
hotspot/src/share/vm/runtime/osThread.hpp
ALLOCATED, // 线程已经分配但还没初始化
INITIALIZED, // 线程已经初始化,还没开始启动
RUNNABLE, // 线程已经启动并可被执行或者正在运行
MONITOR_WAIT, // 等待一个Monitor
CONDVAR_WAIT, // 等待一个条件变量
OBJECT_WAIT, // 通过调用Object.wait()等待对象
BREAKPOINTED, // 调试状态
SLEEPING, // 通过调用Thread.sleep()而进入睡眠
ZOMBIE // 僵死状态,待回收
这里定义不同的线程状态有两个目的:第一、统一管理,第二、根据状态可以做一些同步处理,相关内容在VMThread进入安全点时会有涉及。关于安全点的内容并不影响G1的阅读,后文将会详细介绍。
当线程创建时,它的状态为NEW,当执行时转变为RUNNABLE。线程在Windows
和Linux上的实现稍有区别。在Linux上创建线程后,虽然设置成NEW,但是Linux的线程创建完之后就可以执行,所以为了让线程只有在执行Java代码的start之后才能执行,当线程初始化之后,通过等待一个信号将线程暂停,代码如下所示:
hotspot/src/os/linux/vm/os_linux.cpp
{
Monitor* sync_with_child = osthread->startThread_lock();
MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
while ((state = osthread->get_state()) == ALLOCATED) {
sync_with_child->wait(Mutex::_no_safepoint_check_flag);
}
}
在调用start方法时,发送通知事件,让线程真正运行起来。
2.6.1 栈帧
栈帧(frame)在线程执行时和运行过程中用于保存线程的上下文数据,JVM设计了Java栈帧,这是垃圾回收中最重要的根,栈帧的结构在不同的CPU中并不相同,在x86中代码如下所示:
hotspot/src/cpu/x86/vm/frame_x86.inline.hpp
_pc = NULL; // 程序计数器,指向下一个要执行的代码地址
_sp = NULL; // 栈顶指针
_unextended_sp = NULL; // 异常栈顶指针
_fp = NULL; // fp是栈底指针
_cb = NULL; // cb是代码块的地址
_deopt_state = unknown; // 这个字段描述从编译代码到解释代码反优化的状态
在实际应用中主要使用vframe,它包含了栈帧的字段和线程对象。在JaveThread中定义了JavaFrameAnchor,这个结构保存的是最后一个栈帧的sp、fp。每一个JavaThread都有一个JavaFrameAnchor,即最后一次调用栈的sp、fp。而通过这两个值可以构造栈帧结构,并且根据栈帧的内容,能够遍历整个JavaThread运行时的所有调用链。获取的方法就是根据JavaFrameAnchor里面的sp、fp构造栈帧,再根据栈帧构造vframe结构,代码如下所示:
vframe* start_vf = last_java_vframe(®_map);
for (vframe* f = start_vf; f; f = f->sender() ) {
……
}
在遍历的时候主要通过sender获得下一个栈,其中sender位于栈帧中,其具体的位置依赖于栈的布局,比如汇编解释器在执行时栈帧的代码如下:
hotspot/src/cpu/x86/vm/frame_x86.hpp
栈帧也是和GC密切相关的,在GC过程中,通常第一步就是遍历根,Java线程栈帧就是根元素之一,遍历整个栈帧的方式是通过StackFrameStream,其中封装了一个next指针,其原理和上述的代码一样,通过sender获得调用者的栈帧。
值得一提的是,我们将Java的栈帧作为根来遍历堆,对对象进行标记并收集垃圾。
- 点赞
- 收藏
- 关注作者
评论(0)