JVM Attach机制实现

简介:

感谢支付宝同事【寒泉子】的投稿

attach是什么

在讲这个之前,我们先来点大家都知道的东西,当我们感觉线程一直卡在某个地方,想知道卡在哪里,首先想到的是进行线程dump,而常用的命令是jstack <pid>,


大家是否注意过上面圈起来的两个线程,”Attach Listener”和“Signal Dispatcher”,这两个线程是我们这次要讲的attach机制的关键,先偷偷告诉各位,其实Attach Listener这个线程在jvm起来的时候可能并没有的,后面会细说。

那attach机制是什么?说简单点就是jvm提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作,比如说我们为了让另外一个jvm进程把线程dump出来,那么我们跑了一个jstack的进程,然后传了个pid的参数,告诉它要哪个进程进行线程dump,既然是两个进程,那肯定涉及到进程间通信,以及传输协议的定义,比如要执行什么操作,传了什么参数等。

attach能做些什么
     总结起来说,比如内存dump,线程dump,类信息统计(比如加载的类及大小以及实例个数等),动态加载agent(使用过btrace的应该不陌生),动态设置vm flag(但是并不是所有的flag都可以设置的,因为有些flag是在jvm启动过程中使用的,是一次性的),打印vm flag,获取系统属性等,这些对应的源码(attachListener.cpp)如下


01 static AttachOperationFunctionInfo funcs[] = {
02   { &quot;agentProperties&quot;,  get_agent_properties },
03   { &quot;datadump&quot;,         data_dump },
04   { &quot;dumpheap&quot;,         dump_heap },
05   { &quot;load&quot;,             JvmtiExport::load_agent_library },
06   { &quot;properties&quot;,       get_system_properties },
07   { &quot;threaddump&quot;,       thread_dump },
08   { &quot;inspectheap&quot;,      heap_inspection },
09   { &quot;setflag&quot;,          set_flag },
10   { &quot;printflag&quot;,        print_flag },
11   { &quot;jcmd&quot;,             jcmd },
12   { NULL,               NULL }
13 };

后面是命令对应的处理函数。
attach在jvm里如何实现的

Attach Listener线程的创建

   前面也提到了,jvm在启动过程中可能并没有启动Attach Listener这个线程,可以通过jvm参数来启动,代码(Threads::create_vm)如下:


01 if (!DisableAttachMechanism) {
02     if (StartAttachListener || AttachListener::init_at_startup()) {
03       AttachListener::init();
04     }
05   }
06 bool AttachListener::init_at_startup() {
07   if (ReduceSignalUsage) {
08     return true;
09   } else {
10     return false;
11   }
12 }

其中DisableAttachMechanism,StartAttachListener ,ReduceSignalUsage均默认是false(globals.hpp)


1 product(bool, DisableAttachMechanism, false,                              \
2          &quot;Disable mechanism that allows tools to attach to this VM&rdquo;)
3 product(bool, StartAttachListener, false,                                 \
4           &quot;Always start Attach Listener at VM startup&quot;)
5 product(bool, ReduceSignalUsage, false,                                   \
6           &quot;Reduce the use of OS signals in Java and/or the VM&rdquo;)

因此AttachListener::init()并不会被执行,而Attach Listener线程正是在此方法里创建的


01 // Starts the Attach Listener thread
02 void AttachListener::init() {
03   EXCEPTION_MARK;
04   klassOop k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);
05   instanceKlassHandle klass (THREAD, k);
06   instanceHandle thread_oop = klass-&gt;allocate_instance_handle(CHECK);
07  
08   const char thread_name[] = &quot;Attach Listener&quot;;
09   Handle string = java_lang_String::create_from_str(thread_name, CHECK);
10  
11   // Initialize thread_oop to put it into the system threadGroup
12   Handle thread_group (THREAD, Universe::system_thread_group());
13   JavaValue result(T_VOID);
14   JavaCalls::call_special(&amp;result, thread_oop,
15                        klass,
16                        vmSymbols::object_initializer_name(),
17                        vmSymbols::threadgroup_string_void_signature(),
18                        thread_group,
19                        string,
20                        CHECK);
21  
22   KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
23   JavaCalls::call_special(&amp;result,
24                         thread_group,
25                         group,
26                         vmSymbols::add_method_name(),
27                         vmSymbols::thread_void_signature(),
28                         thread_oop,             // ARG 1
29                         CHECK);
30  
31   { MutexLocker mu(Threads_lock);
32     JavaThread* listener_thread = new JavaThread(&amp;attach_listener_thread_entry);
33  
34     // Check that thread and osthread were created
35     if (listener_thread == NULL || listener_thread-&gt;osthread() == NULL) {
36       vm_exit_during_initialization(&quot;java.lang.OutOfMemoryError&quot;,
37                                     &quot;unable to create new native thread&quot;);
38     }
39  
40     java_lang_Thread::set_thread(thread_oop(), listener_thread);
41     java_lang_Thread::set_daemon(thread_oop());
42  
43     listener_thread-&gt;set_threadObj(thread_oop());
44     Threads::add(listener_thread);
45     Thread::start(listener_thread);
46   }
47 }

    既然在启动的时候不会创建这个线程,那么我们在上面看到的那个线程是怎么创建的呢,这个就要关注另外一个线程“Signal Dispatcher”了,顾名思义是处理信号的,这个线程是在jvm启动的时候就会创建的,具体代码就不说了。

     下面以jstack的实现来说明触发attach这一机制进行的过程,jstack命令的实现其实是一个叫做JStack.java的类,查看jstack代码后会走到下面的方法里


01 private static void runThreadDump(String pid, String args[]) throws Exception {
02         VirtualMachine vm = null;
03         try {
04             vm = VirtualMachine.attach(pid);
05         } catch (Exception x) {
06             String msg = x.getMessage();
07             if (msg != null) {
08                 System.err.println(pid + &quot;: &quot; + msg);
09             } else {
10                 x.printStackTrace();
11             }
12             if ((x instanceof AttachNotSupportedException) &amp;&amp;
13                 (loadSAClass() != null)) {
14                 System.err.println(&quot;The -F option can be used when the target &quot; +
15                     &quot;process is not responding&quot;);
16             }
17             System.exit(1);
18         }
19  
20         // Cast to HotSpotVirtualMachine as this is implementation specific
21         // method.
22         InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args);
23  
24         // read to EOF and just print output
25         byte b[] = new byte[256];
26         int n;
27         do {
28             n = in.read(b);
29             if (n &gt; 0) {
30                 String s = new String(b, 0, n, &quot;UTF-8&quot;);
31                 System.out.print(s);
32             }
33         } while (n &gt; 0);
34         in.close();
35         vm.detach();
36     }

    请注意VirtualMachine.attach(pid);这行代码,触发attach pid的关键,如果是在linux下会走到下面的构造函数


01 LinuxVirtualMachine(AttachProvider provider, String vmid)
02         throws AttachNotSupportedException, IOException
03     {
04         super(provider, vmid);
05  
06         // This provider only understands pids
07         int pid;
08         try {
09             pid = Integer.parseInt(vmid);
10         } catch (NumberFormatException x) {
11             throw new AttachNotSupportedException(&quot;Invalid process identifier&quot;);
12         }
13  
14         // Find the socket file. If not found then we attempt to start the
15         // attach mechanism in the target VM by sending it a QUIT signal.
16         // Then we attempt to find the socket file again.
17         path = findSocketFile(pid);
18         if (path == null) {
19             File f = createAttachFile(pid);
20             try {
21                 // On LinuxThreads each thread is a process and we don't have the
22                 // pid of the VMThread which has SIGQUIT unblocked. To workaround
23                 // this we get the pid of the &quot;manager thread&quot; that is created
24                 // by the first call to pthread_create. This is parent of all
25                 // threads (except the initial thread).
26                 if (isLinuxThreads) {
27                     int mpid;
28                     try {
29                         mpid = getLinuxThreadsManager(pid);
30                     } catch (IOException x) {
31                         throw new AttachNotSupportedException(x.getMessage());
32                     }
33                     assert(mpid &gt;= 1);
34                     sendQuitToChildrenOf(mpid);
35                 } else {
36                     sendQuitTo(pid);
37                 }
38  
39                 // give the target VM time to start the attach mechanism
40                 int i = 0;
41                 long delay = 200;
42                 int retries = (int)(attachTimeout() / delay);
43                 do {
44                     try {
45                         Thread.sleep(delay);
46                     } catch (InterruptedException x) { }
47                     path = findSocketFile(pid);
48                     i++;
49                 } while (i &lt;= retries &amp;&amp; path == null);
50                 if (path == null) {
51                     throw new AttachNotSupportedException(
52                         &quot;Unable to open socket file: target process not responding &quot; +
53                         &quot;or HotSpot VM not loaded&quot;);
54                 }
55             } finally {
56                 f.delete();
57             }
58         }
59  
60         // Check that the file owner/permission to avoid attaching to
61         // bogus process
62         checkPermissions(path);
63  
64         // Check that we can connect to the process
65         // - this ensures we throw the permission denied error now rather than
66         // later when we attempt to enqueue a command.
67         int s = socket();
68         try {
69             connect(s, path);
70         } finally {
71             close(s);
72         }
73     }

     这里要解释下代码了,首先看到调用了createAttachFile方法在目标进程的cwd目录下创建了一个文件/proc/<pid>/cwd/.attach_pid<pid>,这个在后面的信号处理过程中会取出来做判断(为了安全),另外我们知道在linux下线程是用进程实现的,在jvm启动过程中会创建很多线程,比如我们上面的信号线程,也就是会看到很多的pid(应该是LWP),那么如何找到这个信号处理线程呢,从上面实现来看是找到我们传进去的pid的父进程,然后给它的所有子进程都发送一个SIGQUIT信号,而jvm里除了vm thread,其他线程都设置了对此信号的屏蔽,因此收不到该信号,于是该信号就传给了“Signal Dispatcher”,在传完之后作轮询等待看目标进程是否创建了某个文件,attachTimeout默认超时时间是5000ms,可通过设置系统变量sun.tools.attach.attachTimeout来指定,下面是Signal Dispatcher线程的entry实现


01 static void signal_thread_entry(JavaThread* thread, TRAPS) {
02   os::set_priority(thread, NearMaxPriority);
03   while (true) {
04     int sig;
05     {
06       // FIXME : Currently we have not decieded what should be the status
07       //         for this java thread blocked here. Once we decide about
08       //         that we should fix this.
09       sig = os::signal_wait();
10     }
11     if (sig == os::sigexitnum_pd()) {
12        // Terminate the signal thread
13        return;
14     }
15  
16     switch (sig) {
17       case SIGBREAK: {
18         // Check if the signal is a trigger to start the Attach Listener - in that
19         // case don't print stack traces.
20         if (!DisableAttachMechanism &amp;&amp; AttachListener::is_init_trigger()) {
21           continue;
22         }
23         // Print stack traces
24         // Any SIGBREAK operations added here should make sure to flush
25         // the output stream (e.g. tty-&gt;flush()) after output.  See 4803766.
26         // Each module also prints an extra carriage return after its output.
27         VM_PrintThreads op;
28         VMThread::execute(&amp;op);
29         VM_PrintJNI jni_op;
30         VMThread::execute(&amp;jni_op);
31         VM_FindDeadlocks op1(tty);
32         VMThread::execute(&amp;op1);
33         Universe::print_heap_at_SIGBREAK();
34         if (PrintClassHistogram) {
35           VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */,
36                                    true /* need_prologue */);
37           VMThread::execute(&amp;op1);
38         }
39         if (JvmtiExport::should_post_data_dump()) {
40           JvmtiExport::post_data_dump();
41         }
42         break;
43       }
44       &hellip;.
45       }
46     }
47   }
48 }

    当信号是SIGBREAK(在jvm里做了#define,其实就是SIGQUIT)的时候,就会触发AttachListener::is_init_trigger()的执行


01 bool AttachListener::is_init_trigger() {
02   if (init_at_startup() || is_initialized()) {
03     return false;               // initialized at startup or already initialized
04   }
05   char fn[PATH_MAX+1];
06   sprintf(fn, &quot;.attach_pid%d&quot;, os::current_process_id());
07   int ret;
08   struct stat64 st;
09   RESTARTABLE(::stat64(fn, &amp;st), ret);
10   if (ret == -1) {
11     snprintf(fn, sizeof(fn), &quot;%s/.attach_pid%d&quot;,
12              os::get_temp_directory(), os::current_process_id());
13     RESTARTABLE(::stat64(fn, &amp;st), ret);
14   }
15   if (ret == 0) {
16     // simple check to avoid starting the attach mechanism when
17     // a bogus user creates the file
18     if (st.st_uid == geteuid()) {
19       init();
20       return true;
21     }
22   }
23   return false;
24 }

    一开始会判断当前进程目录下是否有个.attach_pid<pid>文件(前面提到了),如果没有就会在/tmp下创建一个/tmp/.attach_pid<pid>,当那个文件的uid和自己的uid是一致的情况下(为了安全)再调用init方法


01 // Starts the Attach Listener thread
02 void AttachListener::init() {
03   EXCEPTION_MARK;
04   klassOop k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);
05   instanceKlassHandle klass (THREAD, k);
06   instanceHandle thread_oop = klass-&gt;allocate_instance_handle(CHECK);
07  
08   const char thread_name[] = &quot;Attach Listener&quot;;
09   Handle string = java_lang_String::create_from_str(thread_name, CHECK);
10  
11   // Initialize thread_oop to put it into the system threadGroup
12   Handle thread_group (THREAD, Universe::system_thread_group());
13   JavaValue result(T_VOID);
14   JavaCalls::call_special(&amp;result, thread_oop,
15                        klass,
16                        vmSymbols::object_initializer_name(),
17                        vmSymbols::threadgroup_string_void_signature(),
18                        thread_group,
19                        string,
20                        CHECK);
21  
22   KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
23   JavaCalls::call_special(&amp;result,
24                         thread_group,
25                         group,
26                         vmSymbols::add_method_name(),
27                         vmSymbols::thread_void_signature(),
28                         thread_oop,             // ARG 1
29                         CHECK);
30  
31   { MutexLocker mu(Threads_lock);
32     JavaThread* listener_thread = new JavaThread(&amp;attach_listener_thread_entry);
33  
34     // Check that thread and osthread were created
35     if (listener_thread == NULL || listener_thread-&gt;osthread() == NULL) {
36       vm_exit_during_initialization(&quot;java.lang.OutOfMemoryError&quot;,
37                                     &quot;unable to create new native thread&quot;);
38     }
39  
40     java_lang_Thread::set_thread(thread_oop(), listener_thread);
41     java_lang_Thread::set_daemon(thread_oop());
42  
43     listener_thread-&gt;set_threadObj(thread_oop());
44     Threads::add(listener_thread);
45     Thread::start(listener_thread);
46   }
47 }

此时水落石出了,看到创建了一个线程,并且取名为Attach Listener。再看看其子类LinuxAttachListener的init方法


01 int LinuxAttachListener::init() {
02   char path[UNIX_PATH_MAX];          // socket file
03   char initial_path[UNIX_PATH_MAX];  // socket file during setup
04   int listener;                      // listener socket (file descriptor)
05  
06   // register function to cleanup
07   ::atexit(listener_cleanup);
08  
09   int n = snprintf(path, UNIX_PATH_MAX, &quot;%s/.java_pid%d&quot;,
10                    os::get_temp_directory(), os::current_process_id());
11   if (n &lt; (int)UNIX_PATH_MAX) {
12     n = snprintf(initial_path, UNIX_PATH_MAX, &quot;%s.tmp&quot;, path);
13   }
14   if (n &gt;= (int)UNIX_PATH_MAX) {
15     return -1;
16   }
17  
18   // create the listener socket
19   listener = ::socket(PF_UNIX, SOCK_STREAM, 0);
20   if (listener == -1) {
21     return -1;
22   }
23  
24   // bind socket
25   struct sockaddr_un addr;
26   addr.sun_family = AF_UNIX;
27   strcpy(addr.sun_path, initial_path);
28   ::unlink(initial_path);
29   int res = ::bind(listener, (struct sockaddr*)&amp;addr, sizeof(addr));
30   if (res == -1) {
31     RESTARTABLE(::close(listener), res);
32     return -1;
33   }
34  
35   // put in listen mode, set permissions, and rename into place
36   res = ::listen(listener, 5);
37   if (res == 0) {
38       RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);
39       if (res == 0) {
40           res = ::rename(initial_path, path);
41       }
42   }
43   if (res == -1) {
44     RESTARTABLE(::close(listener), res);
45     ::unlink(initial_path);
46     return -1;
47   }
48   set_path(path);
49   set_listener(listener);
50  
51   return 0;
52 }

     看到其创建了一个监听套接字,并创建了一个文件/tmp/.java_pid<pid>,这个文件就是客户端之前一直在轮询等待的文件,随着这个文件的生成,意味着attach的过程圆满结束了。

attach listener接收请求

      看看它的entry实现attach_listener_thread_entry


01 static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
02   os::set_priority(thread, NearMaxPriority);
03  
04   thread-&gt;record_stack_base_and_size();
05  
06   if (AttachListener::pd_init() != 0) {
07     return;
08   }
09   AttachListener::set_initialized();
10  
11   for (;;) {
12     AttachOperation* op = AttachListener::dequeue();
13     if (op == NULL) {
14       return;   // dequeue failed or shutdown
15     }
16  
17     ResourceMark rm;
18     bufferedStream st;
19     jint res = JNI_OK;
20  
21     // handle special detachall operation
22     if (strcmp(op-&gt;name(), AttachOperation::detachall_operation_name()) == 0) {
23       AttachListener::detachall();
24     } else {
25       // find the function to dispatch too
26       AttachOperationFunctionInfo* info = NULL;
27       for (int i=0; funcs[i].name != NULL; i++) {
28         const char* name = funcs[i].name;
29         assert(strlen(name) &lt;= AttachOperation::name_length_max, &quot;operation &lt;= name_length_max&quot;);
30         if (strcmp(op-&gt;name(), name) == 0) {
31           info = &amp;(funcs[i]);
32           break;
33         }
34       }
35  
36       // check for platform dependent attach operation
37       if (info == NULL) {
38         info = AttachListener::pd_find_operation(op-&gt;name());
39       }
40  
41       if (info != NULL) {
42         // dispatch to the function that implements this operation
43         res = (info-&gt;func)(op, &amp;st);
44       } else {
45         st.print(&quot;Operation %s not recognized!&quot;, op-&gt;name());
46         res = JNI_ERR;
47       }
48     }
49  
50     // operation complete - send result and output to client
51     op-&gt;complete(res, &amp;st);
52   }
53 }

      从代码来看就是从队列里不断取AttachOperation,然后找到请求命令对应的方法进行执行,比如我们一开始说的jstack命令,找到 { “threaddump”,       thread_dump }的映射关系,然后执行thread_dump方法  再来看看其要调用的AttachListener::dequeue()


01 AttachOperation* AttachListener::dequeue() {
02   JavaThread* thread = JavaThread::current();
03   ThreadBlockInVM tbivm(thread);
04  
05   thread-&gt;set_suspend_equivalent();
06   // cleared by handle_special_suspend_equivalent_condition() or
07   // java_suspend_self() via check_and_wait_while_suspended()
08  
09   AttachOperation* op = LinuxAttachListener::dequeue();
10  
11   // were we externally suspended while we were waiting?
12   thread-&gt;check_and_wait_while_suspended();
13  
14   return op;
15 }

     最终调用的是LinuxAttachListener::dequeue()

 
01 LinuxAttachOperation* LinuxAttachListener::dequeue() {
02   for (;;) {
03     int s;
04  
05     // wait for client to connect
06     struct sockaddr addr;
07     socklen_t len = sizeof(addr);
08     RESTARTABLE(::accept(listener(), &amp;addr, &amp;len), s);
09     if (s == -1) {
10       return NULL;      // log a warning?
11     }
12  
13     // get the credentials of the peer and check the effective uid/guid
14     // - check with jeff on this.
15     struct ucred cred_info;
16     socklen_t optlen = sizeof(cred_info);
17     if (::getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void*)&amp;cred_info, &amp;optlen) == -1) {
18       int res;
19       RESTARTABLE(::close(s), res);
20       continue;
21     }
22     uid_t euid = geteuid();
23     gid_t egid = getegid();
24  
25     if (cred_info.uid != euid || cred_info.gid != egid) {
26       int res;
27       RESTARTABLE(::close(s), res);
28       continue;
29     }
30  
31     // peer credential look okay so we read the request
32     LinuxAttachOperation* op = read_request(s);
33     if (op == NULL) {
34       int res;
35       RESTARTABLE(::close(s), res);
36       continue;
37     } else {
38       return op;
39     }
40   }
41 }

     我们看到如果没有请求的话,会一直accept在那里,当来了请求,然后就会创建一个套接字,并读取数据,构建出LinuxAttachOperation返回并执行。
   整个过程就这样了,从attach线程创建到接收请求,处理请求,希望对大家有帮助。

目录
相关文章
|
7月前
|
监控 Java 编译器
JVM运行命令
JVM运行命令
59 0
|
8月前
|
Java 调度
start()方法和run()方法区别与多线程抢占式运行原理
我们通过一个例子来进行总结,我们写一个利用Thread创建的简单的多线程例子,然后分别执行start()与run()方法,执行结果如下所示:
57 0
|
9月前
|
Java C++
JVM学习日志(九) 对象的finalization机制
对象的finalization机制 简述
80 0
|
11月前
|
Java
JVM - 应用JVM核心参数推荐设置
JVM - 应用JVM核心参数推荐设置
215 0
|
安全 Java 编译器
JVM 的 noverify 启动参数
警告的原因为: 你的 JDK 使用了高于 13 的版本,但是你还是使用了-noverify 运行参数。
263 0
|
安全 Java Linux
JVM源码分析之Attach机制实现完全解读
JVM源码分析之Attach机制实现完全解读
JVM源码分析之Attach机制实现完全解读
|
存储 缓存 监控
JVM学习(五):JVM运行时参数
JVM学习(五):JVM运行时参数
211 0
JVM学习(五):JVM运行时参数
|
存储 缓存 监控
JVM03--JVM垃圾收集机制的一些基本概念
今天来学习下与JVM垃圾收集机制相关的一些基本概念。
58 0
JVM03--JVM垃圾收集机制的一些基本概念
|
存储 缓存 Java
JVM--JVM运行时数据区域详解
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图
66 0
JVM--JVM运行时数据区域详解
|
存储 Java 编译器
初识JVM(JVM运行流程,JVM运行时数据区,内存布局中的异常)
JVM(Java Virtual Machine),为Java虚拟机,虚拟机是指通过软件模拟一个具有完整的硬件功能并且运行在完全隔离的环境中的完整的计算机系统,JVM是一台被定制过的现实中不存在的计算机。
初识JVM(JVM运行流程,JVM运行时数据区,内存布局中的异常)