购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

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/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方法时,发送通知事件,让线程真正运行起来。

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(&reg_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的栈帧作为根来遍历堆,对对象进行标记并收集垃圾。

2.6.2 句柄

实际上线程既可以支持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则是通过全局变量开始。

2.6.3 JVM本地方法栈中的对象

上节介绍本地方法栈是如何管理和链接对象的。每一个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, &reg_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;
}

2.6.4 Java本地方法栈中的对象

Java线程使用一个对象句柄存储块JNIHandleBlock来为其在本地方法中申请的临时对象创建对应的句柄,每个JNIHandleBlock里面有一个oop数组,长度为32,如果超过数组长度则申请新的Block并通过next指针形成链表。另外JNIHandleBlock中还有一个_pop_frame_link属性,用来保存Java线程切换方法时分配本地对象句柄的上下文环境,从而形成调用handle的链表。 TBgTdK7Cb5et1UncOno6mJkK7yCwXnmAZeLs/BlqJC5eYoi1B1Td2oAIUHbhKedp

点击中间区域
呼出菜单
上一章
目录
下一章
×