线程是程序执行的基本单元,在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/classf ile/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方法时,发送通知事件,让线程真正运行起来。
栈帧 (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的栈帧作为根来遍历堆,对对象进行标记并收集垃圾。
实际上线程既可以支持Java代码的执行也可以执行本地代码,如果本地代码(这里的本地代码指的是JVM里面的本地代码,而不是用户自定义的本地代码)引用了堆里面的对象该如何处理?是不是也是通过栈?理论上是可行的,实际上JVM并没有区分Java栈和本地方法栈,如果通过栈进行处理则必须要区分这两种情况。JVM设计了另一个概念,handleArea,这是一块线程的资源区,在这个区域分配句柄(handle),并且管理所有的句柄,如果函数还在调用中,那么句柄有效,句柄关联的对象也就是活跃对象。为了管理句柄的生命周期,引入了HandleMark,通常HandleMark分配在栈上,在创建HandleMark的时候标记handleArea对象有效,在HandleMark对象析构的时候,从HandleArea中删除对象的引用。由于所有句柄都形成了一个链表,那么访问这个句柄链表就可以获得本地代码执行中对堆对象的引用。
句柄和OOP对象关联,在HandleArea中有一个slot用于指向OOP对象。
本节源码都在下面两个文件中,为了便于阅读和减少篇幅,我们对其中的类代码进行了重组,代码如下所示:
hotspot/src/share/vm/runtime/handles.cpp hotspot/src/share/vm/runtime/handles.hpp class Handle VALUE_OBJ_CLASS_SPEC { private: oop* _handle; …… inline Handle(oop obj) { if (obj == NULL) { _handle = NULL; } else { _handle = Thread::current()->handle_area()->allocate_handle(obj); } } …… }
在HandleMark中标记Chunk的地址,这个就是找到当前本地方法代码中活跃的句柄,因此也就可以找到对应的活跃的OOP对象。下面是HandleMark的构造函数和析构函数,它们的主要工作就是构建句柄链表,代码如下所示:
class HandleMark { private: Thread *_thread; // 这个HandleMark归属的线程 HandleArea *_area; // 保存句柄的区域 Chunk *_chunk; // Chunk 和Area配合,获得准确的内存地址 char *_hwm, *_max; // 句柄区域的属性 size_t _size_in_bytes; // 句柄区域的大小 // HandleMark形成链表的字段 HandleMark* _previous_handle_mark; HanldeMark(THread* thread) _thread = thread; // 获得句柄区域 _area = thread->handle_area(); // 获取handleArea的信息,用于标记句柄分配的状态 _chunk = _area->_chunk; _hwm = _area->_hwm; _max = _area->_max; _size_in_bytes = _area->_size_in_bytes; // 形成链表,注意HandleMark是通过线程访问,所以这里会关联到线程中 set_previous_handle_mark(thread->last_handle_mark()); thread->set_last_handle_mark(this); } HandleMark::~HandleMark() { HandleArea* area = _area; // 用于编译优化别名分析 // 删除最后加入的chuanks if( _chunk->next() ) { // 恢复缓存的信息 _chunk->next_chop(); } // 恢复handleArea的信息 area->_chunk = _chunk; area->_hwm = _hwm; area->_max = _max; // 删除handlemark _thread->set_last_handle_mark(previous_handle_mark()); } …… };
在这里我们提到了Chunk,Chunk的回收是通过前面我们提到的周期性线程Watcher Thread完成的。
还需要提到一点,就是JVM中的本地代码指的是JVM内部的代码,除了JVM内部的本地代码,还有JNI代码也是本地代码。对于本地代码,并不归JVM直接管理,在执行JNI代码的时候,也有可能访问堆中的OOP对象。所以也需要一个机制进行管理,JVM引入了类似的句柄机制,称为JNIHandle。JNIHandle分为两种,全局和局部对象引用,大部分对象的引用属于局部对象引用,最终还是调用了JNIHandleBlock来管理,因为JNIHandle没有设计一个JNIHandleMark的机制,所以在创建时需要明确调用make_local,在回收时也需要明确调用destory_local。对于全局对象,比如在编译任务compilerTask中会访问Method对象,这时候就需要把这些对象设置为全局的(否则在GC时可能会被回收的)。这两部分在垃圾回收时的处理是不同的,局部JNIhandle是通过线程,全局JNIhandle则是通过全局变量开始。
上节介绍本地方法栈是如何管理和链接对象的。每一个Java线程都私有一个句柄区_handle_area来存储其运行过程中创建的临时对象,这个句柄区是随着Java线程的栈帧变化的,我们看一下HandleMark是如何管理的。HandleArea的作用上一节已经介绍过了,这里我们先看一下它们的结构图(如图2-2所示),然后再通过代码演示如何管理句柄。
图2-2 句柄结构图
Java线程每调用一个Java方法就会创建一个对应HandleMark来保存已分配的对象句柄,然后等调用返回后即行恢复,代码如下所示:
hotspot/src/share/vm/runtime/javaCalls.cpp JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) { …… { HandleMark hm(thread);
所以当Java线程运行一段时间之后,通过HandleMark构建的对象识别链如图2-3所示:
图2-3 本地方法栈对象的管理
这里Chunk的管理是动态变化的,第一个Chunk可能为256或者1024个字节,每一个Chunk都有一个额外空间,主要是调用malloc时会有一段额外的信息,比如地址的长度等,在32位机器上一般为20个字节,所以每一个Chunk都会比最大值少5个OOP对象。另外,一般的Chunk块通常为32KB。最后还需要提一点的就是,Handle Mark通常都是分配在线程栈中,也意味着无需额外的管理,只需要找到HandleMark就能找到哪些对象是存活的。我们来看一个简单的例子,看看如何遍历堆空间。
下面这个代码片段是为了输出堆空间里面的对象,例如我们执行jmap命令来获取堆空间对象的时候最终会调用到VM_HeapDumper::do_thread()来遍历所有的对象。通过下面的代码我们能非常清楚地看到,如果JavaThread执行的是Java代码,则直接通过StackValueCollection访问局部变量,如果执行的是本地代码,线程则通过active_handles()访问句柄而访问对象。
hotspot/src/share/vm/services/heapDumper.cpp int VM_HeapDumper::do_thread(JavaThread* java_thread, u4 thread_serial_num) { JNILocalsDumper blk(writer(), thread_serial_num); oop threadObj = java_thread->threadObj(); int stack_depth = 0; if (java_thread->has_last_Java_frame()) { Thread* current_thread = Thread::current(); ResourceMark rm(current_thread); HandleMark hm(current_thread); RegisterMap reg_map(java_thread); frame f = java_thread->last_frame(); vframe* vf = vframe::new_vframe(&f, ®_map, java_thread); frame* last_entry_frame = NULL; int extra_frames = 0; if (java_thread == _oome_thread && _oome_constructor != NULL) { extra_frames++; } while (vf != NULL) { blk.set_frame_number(stack_depth); if (vf->is_java_frame()) { // Java线程栈,包括(interpreted, compiled, ...) javaVFrame *jvf = javaVFrame::cast(vf); if (!(jvf->method()->is_native())) { StackValueCollection* locals = jvf->locals(); for (int slot=0; slot<locals->size(); slot++) { if (locals->at(slot)->type() == T_OBJECT) { oop o = locals->obj_at(slot)(); if (o != NULL) { writer()->write_u1(HPROF_GC_ROOT_JAVA_FRAME); writer()->write_objectID(o); writer()->write_u4(thread_serial_num); writer()->write_u4((u4) (stack_depth + extra_frames)); } } } } else { // 本地方法栈 if (stack_depth == 0) { java_thread->active_handles()->oops_do(&blk); } else { if (last_entry_frame != NULL) { last_entry_frame->entry_frame_call_wrapper()->handles()-> oops_do(&blk); } } } stack_depth++; last_entry_frame = NULL; } else { frame* fr = vf->frame_pointer(); assert(fr != NULL, "sanity check"); if (fr->is_entry_frame()) { last_entry_frame = fr; } } vf = vf->sender(); } } else { java_thread->active_handles()->oops_do(&blk); } return stack_depth; }
Java线程使用一个对象句柄存储块JNIHandleBlock来为其在本地方法中申请的临时对象创建对应的句柄,每个JNIHandleBlock里面有一个oop数组,长度为32,如果超过数组长度则申请新的Block并通过next指针形成链表。另外JNIHandleBlock中还有一个_pop_frame_link属性,用来保存Java线程切换方法时分配本地对象句柄的上下文环境,从而形成调用handle的链表。