读书笔记之inside JVM(4)

在我们常用的Jstack, Jmap 用于分析java虚拟机的状态的工具,通过起另一个虚拟机通过运行sun.tools包下的java文件,去跟踪另一个虚拟机的状态。

如果让你设计一个跟踪另一个进程的方法,你也通常会考虑这几种常用的方式。

第一种,就是通知被跟踪的进程,让进程执行相应的消息,同时对该消息做出反应。

第二种,就是通过内核的调用,直接能够访问进程的内存,堆栈情况,通过分析被跟踪的进程的内存结构,从而知道当前被跟踪的进程的状态。

第一种方式

优势:

对调用者和被调用者只要达成简单的通讯协议,调用者无需知道被调用者的逻辑,结构,只需要简单的发送命令的方式,被调用者能够接受到命令,并且对该命令进行回应就可以。

缺点:

如果被调用者当时的状态本来就不正常,或者繁忙,没办法对该命令做出响应,那这个跟踪进程往往是在规定的等待时间里,无法返回正确的需要的信息。其次被调用者在分析的过程中,有可能需要暂停进程中的其他的线程,而对被跟踪的进程有一定的影响。

第二种方式

优势:

通过内核的支持,访问被跟踪的内存,并作出快照,后台分析,很少影响被跟踪的进程。

缺点:

这种方式需要对被跟踪程的内存分配和使用非常的了解,无法解耦,而本身系统内核调用也会出问题。

Java工具类中也是大致实现了这2中方式,工具中会先选择第一种方式,如果发现第一种方式不能成功,将会建议使用-F参数,也就是第二种方式。

我们先讲第一种方式。

既然是需要向被跟踪进程发出命令,在linux中可以选择多种方式进行进程中通讯 共享内存,文件之类,其中创建socket的文件实现通讯是比较简单的方法。

下面是整个的流程图:

当java虚拟机启动的时候,会启动很多内部的线程,这些线程主要在thread.cpp里的create_vm方法体里实现

而在thread.cpp里主要起了2个线程来处理信号相关的

[cpp]
 
view plain
copy

  1. JvmtiExport::enter_live_phase();
  2. // Signal Dispatcher needs to be started before VMInit event is posted
  3. os::signal_init();
  4. // Start Attach Listener if +StartAttachListener or it can‘t be started lazily
  5. if (!DisableAttachMechanism) {
  6. if (StartAttachListener || AttachListener::init_at_startup()) {
  7. AttachListener::init();
  8. }
  9. }

1. Signal Dispatcher 线程

在os.cpp中的signal_init()函数中,启动了signal dispatcher 线程,对signal dispather 线程主要是用于处理信号,等待信号并且分发处理,可以详细看signal_thread_entry的方法

[cpp]
 
view plain
copy

  1. static void signal_thread_entry(JavaThread* thread, TRAPS) {
  2. os::set_priority(thread, NearMaxPriority);
  3. while (true) {
  4. int sig;
  5. {
  6. // FIXME : Currently we have not decieded what should be the status
  7. //         for this java thread blocked here. Once we decide about
  8. //         that we should fix this.
  9. sig = os::signal_wait();
  10. }
  11. if (sig == os::sigexitnum_pd()) {
  12. // Terminate the signal thread
  13. return;
  14. }
  15. switch (sig) {
  16. case SIGBREAK: {
  17. // Check if the signal is a trigger to start the Attach Listener - in that
  18. // case don‘t print stack traces.
  19. if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {
  20. continue;
  21. }
  22. // Print stack traces
  23. // Any SIGBREAK operations added here should make sure to flush
  24. // the output stream (e.g. tty->flush()) after output.  See 4803766.
  25. // Each module also prints an extra carriage return after its output.
  26. VM_PrintThreads op;
  27. VMThread::execute(&op);
  28. VM_PrintJNI jni_op;
  29. VMThread::execute(&jni_op);
  30. VM_FindDeadlocks op1(tty);
  31. VMThread::execute(&op1);
  32. Universe::print_heap_at_SIGBREAK();
  33. if (PrintClassHistogram) {
  34. VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */,
  35. true /* need_prologue */);
  36. VMThread::execute(&op1);
  37. }
  38. if (JvmtiExport::should_post_data_dump()) {
  39. JvmtiExport::post_data_dump();
  40. }
  41. break;
  42. }
  43. default: {
  44. // Dispatch the signal to java
  45. HandleMark hm(THREAD);
  46. klassOop k = SystemDictionary::resolve_or_null(vmSymbolHandles::sun_misc_Signal(), THREAD);
  47. KlassHandle klass (THREAD, k);
  48. if (klass.not_null()) {
  49. JavaValue result(T_VOID);
  50. JavaCallArguments args;
  51. args.push_int(sig);
  52. JavaCalls::call_static(
  53. &result,
  54. klass,
  55. vmSymbolHandles::dispatch_name(),
  56. vmSymbolHandles::int_void_signature(),
  57. &args,
  58. THREAD
  59. );
  60. }
  61. if (HAS_PENDING_EXCEPTION) {
  62. // tty is initialized early so we don‘t expect it to be null, but
  63. // if it is we can‘t risk doing an initialization that might
  64. // trigger additional out-of-memory conditions
  65. if (tty != NULL) {
  66. char klass_name[256];
  67. char tmp_sig_name[16];
  68. const char* sig_name = "UNKNOWN";
  69. instanceKlass::cast(PENDING_EXCEPTION->klass())->
  70. name()->as_klass_external_name(klass_name, 256);
  71. if (os::exception_name(sig, tmp_sig_name, 16) != NULL)
  72. sig_name = tmp_sig_name;
  73. warning("Exception %s occurred dispatching signal %s to handler"
  74. "- the VM may need to be forcibly terminated",
  75. klass_name, sig_name );
  76. }
  77. CLEAR_PENDING_EXCEPTION;
  78. }
  79. }
  80. }
  81. }
  82. }

可以看到通过os::signal_wait();等待信号,而在linux里是通过sem_wait()来实现,接受到SIGBREAK(linux 中的QUIT)信号的时候(关于信号处理请参考笔者的另一篇博客:java 中关于信号的处理在linux下的实现),第一次通过调用 AttachListener::is_init_trigger()初始化attach listener线程,详细见2.Attach Listener 线程。

  1. 第一次收到信号,会开始初始化,当初始化成功,将会直接返回,而且不返回任何线程stack的信息(通过socket file的操作返回),并且第二次将不在需要初始化。如果初始化不成功,将直接在控制台的outputstream中打印线程栈信息。
  2. 第二次收到信号,如果已经初始化过,将直接在控制台中打印线程的栈信息。如果没有初始化,继续初始化,走和第一次相同的流程。

2. Attach Listener 线程

Attach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并且吧结果返回给发送者。在jvm启动的时候,如果没有指定+StartAttachListener,该线程是不会启动的,刚才我们讨论到了在接受到quit信号之后,会调用 AttachListener::is_init_trigger()通过调用用AttachListener::init()启动了Attach Listener 线程,同时在不同的操作系统下初始化,在linux中 是在attachListener_Linux.cpp文件中实现的。

在linux中如果发现文件.attach_pid#pid存在,才会启动attach listener线程,同时初始化了socket 文件,也就是通常jmap,jstack tool干的事情,先创立attach_pid#pid文件,然后发quit信号,通过这种方式暗式的启动了Attach Listener线程(见博客:http://blog.csdn.net/raintungli/article/details/7023092)。

线程的实现在 attach_listener_thread_entry 方法体中实现

[cpp]
 
view plain
copy

  1. static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
  2. os::set_priority(thread, NearMaxPriority);
  3. if (AttachListener::pd_init() != 0) {
  4. return;
  5. }
  6. AttachListener::set_initialized();
  7. for (;;) {
  8. AttachOperation* op = AttachListener::dequeue();
  9. if (op == NULL) {
  10. return;   // dequeue failed or shutdown
  11. }
  12. ResourceMark rm;
  13. bufferedStream st;
  14. jint res = JNI_OK;
  15. // handle special detachall operation
  16. if (strcmp(op->name(), AttachOperation::detachall_operation_name()) == 0) {
  17. AttachListener::detachall();
  18. } else {
  19. // find the function to dispatch too
  20. AttachOperationFunctionInfo* info = NULL;
  21. for (int i=0; funcs[i].name != NULL; i++) {
  22. const char* name = funcs[i].name;
  23. assert(strlen(name) <= AttachOperation::name_length_max, "operation <= name_length_max");
  24. if (strcmp(op->name(), name) == 0) {
  25. info = &(funcs[i]);
  26. break;
  27. }
  28. }
  29. // check for platform dependent attach operation
  30. if (info == NULL) {
  31. info = AttachListener::pd_find_operation(op->name());
  32. }
  33. if (info != NULL) {
  34. // dispatch to the function that implements this operation
  35. res = (info->func)(op, &st);
  36. } else {
  37. st.print("Operation %s not recognized!", op->name());
  38. res = JNI_ERR;
  39. }
  40. }
  41. // operation complete - send result and output to client
  42. op->complete(res, &st);
  43. }
  44. }

在AttachListener::dequeue(); 在liunx里的实现就是监听刚才创建的socket的文件,如果有请求进来,找到请求对应的操作,调用操作得到结果并把结果写到这个socket的文件,如果你把socket的文件删除,jstack/jmap会出现错误信息 unable to open socket file:........

我们经常使用 kill -3 pid的操作打印出线程栈信息,我们可以看到具体的实现是在Signal Dispatcher 线程中完成的,因为kill -3 pid 并不会创建.attach_pid#pid文件,所以一直初始化不成功,从而线程的栈信息被打印到控制台中。

信号转发线程,Attach Listener 线程都只是操作socket文件,并没有去执行比如stack 分析,或者heap的分析,真正的工作线程其实是vm thread.

(一)启动vm thread

[cpp]
 
view plain
copy

  1. jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
  2. ...
  3. // Create the VMThread
  4. { TraceTime timer("Start VMThread", TraceStartupTime);
  5. VMThread::create();
  6. Thread* vmthread = VMThread::vm_thread();
  7. if (!os::create_thread(vmthread, os::vm_thread))
  8. vm_exit_during_initialization("Cannot create VM thread. Out of system resources.");
  9. // Wait for the VM thread to become ready, and VMThread::run to initialize
  10. // Monitors can have spurious returns, must always check another state flag
  11. {
  12. MutexLocker ml(Notify_lock);
  13. os::start_thread(vmthread);
  14. while (vmthread->active_handles() == NULL) {
  15. Notify_lock->wait();
  16. }
  17. }
  18. }
  19. ...
  20. }

我们可以看到,在thread.cpp里启动了线程vm thread,在这里我们同时也稍微的略带的讲一下jvm在linux里如何启动线程的。

通常在linux中启动线程,是调用

[cpp]
 
view plain
copy

  1. int pthread_create((pthread_t *__thread, __const pthread_attr_t *__attr,void *(*__start_routine) (void *), void *__arg));

而在java里却增加了os:create_thread --初始化线程 和os:start_thread--启动线程

我们去看一下jvm里面是如何在linux里做到的

在os_linux.cpp中来看create_thread的方法

[cpp]
 
view plain
copy

  1. bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
  2. ....
  3. int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
  4. ....
  5. }

继续看java_start方法

[cpp]
 
view plain
copy

  1. static void *java_start(Thread *thread) {
  2. ....
  3. // handshaking with parent thread
  4. {
  5. MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);
  6. // notify parent thread
  7. osthread->set_state(INITIALIZED);
  8. sync->notify_all();
  9. // wait until os::start_thread()
  10. while (osthread->get_state() == INITIALIZED) {
  11. sync->wait(Mutex::_no_safepoint_check_flag);
  12. }
  13. }
  14. // call one more level start routine
  15. thread->run();
  16. return 0;
  17. }

首先jvm先设置了当前线程的状态是Initialized, 然后notify所有的线程,

while (osthread->get_state() == INITIALIZED) {
      sync->wait(Mutex::_no_safepoint_check_flag);
    }

不停的查看线程的当前状态是不是Initialized, 如果是的话,调用了sync->wait()的方法等待。

来看os:start_thread的方法 os.cpp

[cpp]
 
view plain
copy

  1. void os::start_thread(Thread* thread) {
  2. // guard suspend/resume
  3. MutexLockerEx ml(thread->SR_lock(), Mutex::_no_safepoint_check_flag);
  4. OSThread* osthread = thread->osthread();
  5. osthread->set_state(RUNNABLE);
  6. pd_start_thread(thread);
  7. }

这时候设置了线程的状态为runnable,但没有notify线程

在 pd_start_thread(thread)中, os_linux.cpp中

[cpp]
 
view plain
copy

  1. void os::pd_start_thread(Thread* thread) {
  2. OSThread * osthread = thread->osthread();
  3. assert(osthread->get_state() != INITIALIZED, "just checking");
  4. Monitor* sync_with_child = osthread->startThread_lock();
  5. MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
  6. sync_with_child->notify();
  7. }

这时候我们看到了notify 线程的操作

也就是这时候notify了线程,因为这时候的线程的状态是RUNNABLE, 方法java_start继续往下执行,于是调用了thread->run()的方法

对于线程vm Thread 也就是调用了vmthread::run方法

vmThread.cpp

[cpp]
 
view plain
copy

  1. void VMThread::run() {
  2. ...
  3. this->loop();
  4. ...
  5. }

调用了loop函数,处理了VM_Operation 的queue 关于queue的级别和优先级处理算法:可以参考 另一篇博客:http://blog.csdn.net/raintungli/article/details/6553337

(二)Jstack 运行在vm thread里的VM_Operation

jstack 处理也就是在前面博客所提到的attach Listener 线程所做的 operation

[cpp]
 
view plain
copy

  1. static jint thread_dump(AttachOperation* op, outputStream* out) {
  2. bool print_concurrent_locks = false;
  3. if (op->arg(0) != NULL && strcmp(op->arg(0), "-l") == 0) {
  4. print_concurrent_locks = true;
  5. }
  6. // thread stacks
  7. VM_PrintThreads op1(out, print_concurrent_locks);
  8. VMThread::execute(&op1);
  9. // JNI global handles
  10. VM_PrintJNI op2(out);
  11. VMThread::execute(&op2);
  12. // Deadlock detection
  13. VM_FindDeadlocks op3(out);
  14. VMThread::execute(&op3);
  15. return JNI_OK;
  16. }

简单看一下类VM_PrintThreads 它 继承了VM_Operation

[cpp]
 
view plain
copy

  1. class VM_PrintThreads: public VM_Operation {
  2. private:
  3. outputStream* _out;
  4. bool _print_concurrent_locks;
  5. public:
  6. VM_PrintThreads()                                                { _out = tty; _print_concurrent_locks = PrintConcurrentLocks; }
  7. VM_PrintThreads(outputStream* out, bool print_concurrent_locks)  { _out = out; _print_concurrent_locks = print_concurrent_locks; }
  8. VMOp_Type type() const                                           {  return VMOp_PrintThreads; }
  9. void doit();
  10. bool doit_prologue();
  11. void doit_epilogue();
  12. };

当调用VMThread::execute()也就是将VM_PrintThreads 放入了_vm_queue中,交给vm thread 处理,对vm thread来说取出queue里的VM_Operation,并且调用doit方法。

在jstack里,attach listener 的线程产生了VM_PrintThreads,VM_PrintJNI,VM_FindDeadlocks 3个operations,交给了vm thread  的线程处理。

时间: 2024-10-09 04:49:34

读书笔记之inside JVM(4)的相关文章

读书笔记之inside JVM(5)

调用JNI的时候,通常我们使用System.loadLibrary(String libname)来load JNI library, 同样也可以使用System.load(String fileName)来load JNI library,两者的区别是一个只需要设置库的名字,比如如果libA.so 只要输入A就可以了,而libA.so的位置可以同过设置 java.library.path 或者 sun.boot.library.path,后者输入的是完整路经的文件名. 而不论用什么方法,最后J

读书笔记-深入理解JVM虚拟机-1.OOM初探

Java堆OOM(Out-Of-Memory)异常 执行例如以下程序,爆出异常 java.lang.OutOfMemoryError: Java heap space /** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * @author zzm */ public class HeapOOM { static class OOMObject { } public static void main(String[] args

读书笔记-深入理解JVM虚拟机-1.JVM-Stack造成的OOM的理解

-Xss128k:这个JVM参数用来配置栈的大小为128k 因为栈是线程私有的(不清楚的可以去了解下JVM虚拟机结构),所以如果我们启动一个线程,并且在这个线程中调用一个递归,就会产生该异常. /** * VM Args:-Xss128k * */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public sta

java内存区域——深入理解JVM读书笔记

本内容由<深入理解java虚拟机>的部分读书笔记整理而成,本读者计划连载. 通过如下图和文字介绍来了解几个运行时数据区的概念. 方法区:它是各个线程共享的区域,用于内存已被VM加载的类信息.常量.静态变量.即时编译器编译的代码等数据.JVM规范对这个区域的限制很宽松,如同堆一样不需要连续的内存.可选择固定大小.可扩展的大小外,还可以选择不实现垃圾收集.因为在些区域的垃圾收集必要性不高且效果较差.如果回收也是常量池的回收和类型的卸载,但此操作异常困难.当方法区无法满足内存的分配时,抛OutOfM

深入JVM读书笔记(一)——jvm数据区基础知识

最近得空,就把<深入理解Java虚拟机>重新看了一遍,特写下现在的读书笔记,总结知识点,记录现在的理解,便于以后的回顾.下面的内容也会按照这本书的章节来划分知识点! Let's go! 想要了解Java虚拟机,一定要先明白Java运行时划分为哪些数据区域,具体的可以参考下图,按照是否为线程私有可以划分为: 线程私有:虚拟机栈.本地方法栈.程序计数器 线程共有:方法区.堆   下面详细说一下各个数据区的作用: 1. 程序计数器(Program Counter Register) 程序计数器是一块

TJI读书笔记10-复用类

body, td { font-family: 微软雅黑; font-size: 10pt; } TJI读书笔记10-复用类 组合语法 继承语法 代理 final关键字 final的数据 final的参数 final的方法 final的类 初始化和类的加载 乱七八糟不知道怎么归类的知识点 代码复用是java众多牛逼哄哄的功能之一(好像OOP语言都可以呢-),代码复用的理想状态是,使用类但是又不破坏现有代码. 当然住了复制粘贴以外还有其他的方法. 一个叫组合,一个叫继承. 组合语法 其实刚开始我非

TJI读书笔记16-异常处理

TJI读书笔记16-异常处理 概念 基本异常情形 异常的捕获 自定义异常 异常说明 捕获所有异常 栈轨迹 重新抛出异常 Java标准异常 使用finally 异常的限制 构造器 异常的匹配 其他乱七八糟 概念 在早期没有专门的异常处理机制的时候,比如C语言,会通过一些约定俗成的东西来处理异常. 比如让程序返回某个特殊的值或者设置某个标记. 然后对返回值进行检查以判断程序是否出错. 还记得以前C语言的时候,return 0和return -1对异常处理的实现可以追溯到BASIC中的on error

Struts2技术内幕 读书笔记一 框架的本质

本读书笔记系列,主要针对陆舟所著<<Struts2技术内幕 深入解析Strtus2架构设计与实现原理>>一书.笔记中所用的图片若无特殊说明,就都取自书中,特此声明. 什么是框架?我们为什么要用框架?框架能给我们带来什么? 这几个问题既简单又复杂.说它简单,是因为框架确实存在在软件设计中,说它复杂是因为我们现在所使用的框架不论是spring还是struts都是经过多年的发展,其内部已经十分庞杂了,因此想一句话两句话说清楚一个框架就不是那么简单了. OK,既然现有的框架都很复杂,那我们

think in java 读书笔记

java中没有单独函数的概念,依赖类的方法. java中优化了向前引用,类可以在调用者之后. java中包的命名方法实际上是网址的倒转. c++中因为存在全局变量和函数所以会存在一个变量名冲突的问题,但是java中不存在全局变量,不同程序设计者通过不同的类将相同名字的变量和方法隔离. static关键字 通常,我们创建类时会指出那个类的对象的外观与行为.除非用new 创建那个类的一个对象,否则实际上并 未得到任何东西.只有执行了new 后,才会正式生成数据存储空间,并可使用相应的方法. 但在两种