JVM Attach机制实现
Attach是什么
??在講這個之前,我們先來點大家都知道的東西,當我們感覺線程一直卡在某個地方,想知道卡在哪里,首先想到的是進行線程dump,而常用的命令是jstack?,我們就可以看到如下線程棧了
2014-06-18 12:56:14 Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.51-b03 mixed mode):"Attach Listener" daemon prio=5 tid=0x00007fb0c6800800 nid=0x440b waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Service Thread" daemon prio=5 tid=0x00007fb0c584d800 nid=0x5303 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread1" daemon prio=5 tid=0x00007fb0c482e000 nid=0x5103 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread0" daemon prio=5 tid=0x00007fb0c482c800 nid=0x4f03 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Signal Dispatcher" daemon prio=5 tid=0x00007fb0c4815800 nid=0x4d03 runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Finalizer" daemon prio=5 tid=0x00007fb0c4813800 nid=0x3903 in Object.wait() [0x00000001187d2000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x00000007aaa85568> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)- locked <0x00000007aaa85568> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:189)"Reference Handler" daemon prio=5 tid=0x00007fb0c4800000 nid=0x3703 in Object.wait() [0x00000001186cf000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x00000007aaa850f0> (a java.lang.ref.Reference$Lock)at java.lang.Object.wait(Object.java:503)at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)- locked <0x00000007aaa850f0> (a java.lang.ref.Reference$Lock)"main" prio=5 tid=0x00007fb0c5800800 nid=0x1903 waiting on condition [0x0000000107962000]java.lang.Thread.State: TIMED_WAITING (sleeping)at java.lang.Thread.sleep(Native Method)at Test.main(Test.java:5)"VM Thread" prio=5 tid=0x00007fb0c583d800 nid=0x3503 runnable"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fb0c401e000 nid=0x2503 runnable"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fb0c401e800 nid=0x2703 runnable"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fb0c401f800 nid=0x2903 runnable"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fb0c4020000 nid=0x2b03 runnable"GC task thread#4 (ParallelGC)" prio=5 tid=0x00007fb0c4020800 nid=0x2d03 runnable"GC task thread#5 (ParallelGC)" prio=5 tid=0x00007fb0c4021000 nid=0x2f03 runnable"GC task thread#6 (ParallelGC)" prio=5 tid=0x00007fb0c4022000 nid=0x3103 runnable"GC task thread#7 (ParallelGC)" prio=5 tid=0x00007fb0c4022800 nid=0x3303 runnable"VM Periodic Task Thread" prio=5 tid=0x00007fb0c5845000 nid=0x5503 waiting on condition??大家是否注意過上面圈起來的兩個線程,”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)如下
static AttachOperationFunctionInfo funcs[] = {{ "agentProperties", get_agent_properties },{ "datadump", data_dump },{ "dumpheap", dump_heap },{ "load", JvmtiExport::load_agent_library },{ "properties", get_system_properties },{ "threaddump", thread_dump },{ "inspectheap", heap_inspection },{ "setflag", set_flag },{ "printflag", print_flag },{ "jcmd", jcmd },{ NULL, NULL } };??后面是命令對應的處理函數。
Attach在jvm里如何實現的
Attach Listener線程的創建
??前面也提到了,jvm在啟動過程中可能并沒有啟動Attach Listener這個線程,可以通過jvm參數來啟動,代碼 (Threads::create_vm)如下:
if (!DisableAttachMechanism) {if (StartAttachListener || AttachListener::init_at_startup()) {AttachListener::init();}} bool AttachListener::init_at_startup() {if (ReduceSignalUsage) {return true;} else {return false;} }??其中DisableAttachMechanism,StartAttachListener ,ReduceSignalUsage均默認是false(globals.hpp)
product(bool, DisableAttachMechanism, false, \"Disable mechanism that allows tools to Attach to this VM”) product(bool, StartAttachListener, false, \"Always start Attach Listener at VM startup") product(bool, ReduceSignalUsage, false, \"Reduce the use of OS signals in Java and/or the VM”)??因此AttachListener::init()并不會被執行,而Attach Listener線程正是在此方法里創建的
// Starts the Attach Listener thread void AttachListener::init() {EXCEPTION_MARK;klassOop k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);instanceKlassHandle klass (THREAD, k);instanceHandle thread_oop = klass->allocate_instance_handle(CHECK);const char thread_name[] = "Attach Listener";Handle string = java_lang_String::create_from_str(thread_name, CHECK);// Initialize thread_oop to put it into the system threadGroupHandle thread_group (THREAD, Universe::system_thread_group());JavaValue result(T_VOID);JavaCalls::call_special(&result, thread_oop,klass,vmSymbols::object_initializer_name(),vmSymbols::threadgroup_string_void_signature(),thread_group,string,CHECK);KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());JavaCalls::call_special(&result,thread_group,group,vmSymbols::add_method_name(),vmSymbols::thread_void_signature(),thread_oop, // ARG 1CHECK);{ MutexLocker mu(Threads_lock);JavaThread* listener_thread = new JavaThread(&Attach_listener_thread_entry);// Check that thread and osthread were createdif (listener_thread == NULL || listener_thread->osthread() == NULL) {vm_exit_during_initialization("java.lang.OutOfMemoryError","unable to create new native thread");}java_lang_Thread::set_thread(thread_oop(), listener_thread);java_lang_Thread::set_daemon(thread_oop());listener_thread->set_threadObj(thread_oop());Threads::add(listener_thread);Thread::start(listener_thread);} }??既然在啟動的時候不會創建這個線程,那么我們在上面看到的那個線程是怎么創建的呢,這個就要關注另外一個線程“Signal Dispatcher”了,顧名思義是處理信號的,這個線程是在jvm啟動的時候就會創建的,具體代碼就不說了。
??下面以jstack的實現來說明觸發Attach這一機制進行的過程,jstack命令的實現其實是一個叫做JStack.java的類,查看jstack代碼后會走到下面的方法里
private static void runThreadDump(String pid, String args[]) throws Exception {VirtualMachine vm = null;try {vm = VirtualMachine.Attach(pid);} catch (Exception x) {String msg = x.getMessage();if (msg != null) {System.err.println(pid + ": " + msg);} else {x.printStackTrace();}if ((x instanceof AttachNotSupportedException) &&(loadSAClass() != null)) {System.err.println("The -F option can be used when the target " +"process is not responding");}System.exit(1);}// Cast to HotSpotVirtualMachine as this is implementation specific// method.InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args);// read to EOF and just print outputbyte b[] = new byte[256];int n;do {n = in.read(b);if (n > 0) {String s = new String(b, 0, n, "UTF-8");System.out.print(s);}} while (n > 0);in.close();vm.detach();}??請注意VirtualMachine.Attach(pid);這行代碼,觸發Attach pid的關鍵,如果是在linux下會走到下面的構造函數
LinuxVirtualMachine(AttachProvider provider, String vmid)throws AttachNotSupportedException, IOException{super(provider, vmid);// This provider only understands pidsint pid;try {pid = Integer.parseInt(vmid);} catch (NumberFormatException x) {throw new AttachNotSupportedException("Invalid process identifier");}// Find the socket file. If not found then we attempt to start the// Attach mechanism in the target VM by sending it a QUIT signal.// Then we attempt to find the socket file again.path = findSocketFile(pid);if (path == null) {File f = createAttachFile(pid);try {// On LinuxThreads each thread is a process and we don't have the// pid of the VMThread which has SIGQUIT unblocked. To workaround// this we get the pid of the "manager thread" that is created// by the first call to pthread_create. This is parent of all// threads (except the initial thread).if (isLinuxThreads) {int mpid;try {mpid = getLinuxThreadsManager(pid);} catch (IOException x) {throw new AttachNotSupportedException(x.getMessage());}assert(mpid >= 1);sendQuitToChildrenOf(mpid);} else {sendQuitTo(pid);}// give the target VM time to start the Attach mechanismint i = 0;long delay = 200;int retries = (int)(AttachTimeout() / delay);do {try {Thread.sleep(delay);} catch (InterruptedException x) { }path = findSocketFile(pid);i++;} while (i <= retries && path == null);if (path == null) {throw new AttachNotSupportedException("Unable to open socket file: target process not responding " +"or HotSpot VM not loaded");}} finally {f.delete();}}// Check that the file owner/permission to avoid Attaching to// bogus processcheckPermissions(path);// Check that we can connect to the process// - this ensures we throw the permission denied error now rather than// later when we attempt to enqueue a command.int s = socket();try {connect(s, path);} finally {close(s);}}??這里要解釋下代碼了,首先看到調用了createAttachFile方法在目標進程的cwd目錄下創建了一個文件/proc//cwd/.Attach_pid,這個在后面的信號處理過程中會取出來做判斷(為了安全),另外我們知道在linux下線程是用進程實現的,在jvm啟動過程中會創建很多線程,比如我們上面的信號線程,也就是會看到很多的pid(應該是LWP),那么如何找到這個信號處理線程呢,從上面實現來看是找到我們傳進去的pid的父進程,然后給它的所有子進程都發送一個SIGQUIT信號,而jvm里除了信號線程,其他線程都設置了對此信號的屏蔽,因此收不到該信號,于是該信號就傳給了“Signal Dispatcher”,在傳完之后作輪詢等待看目標進程是否創建了某個文件,AttachTimeout默認超時時間是5000ms,可通過設置系統變量sun.tools.Attach.AttachTimeout來指定,下面是Signal Dispatcher線程的entry實現
static void signal_thread_entry(JavaThread* thread, TRAPS) {os::set_priority(thread, NearMaxPriority);while (true) {int sig;{// FIXME : Currently we have not decieded what should be the status// for this java thread blocked here. Once we decide about// that we should fix this.sig = os::signal_wait();}if (sig == os::sigexitnum_pd()) {// Terminate the signal threadreturn;}switch (sig) {case SIGBREAK: {// Check if the signal is a trigger to start the Attach Listener - in that// case don't print stack traces.if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {continue;}// Print stack traces// Any SIGBREAK operations added here should make sure to flush// the output stream (e.g. tty->flush()) after output. See 4803766.// Each module also prints an extra carriage return after its output.VM_PrintThreads op;VMThread::execute(&op);VM_PrintJNI jni_op;VMThread::execute(&jni_op);VM_FindDeadlocks op1(tty);VMThread::execute(&op1);Universe::print_heap_at_SIGBREAK();if (PrintClassHistogram) {VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */,true /* need_prologue */);VMThread::execute(&op1);}if (JvmtiExport::should_post_data_dump()) {JvmtiExport::post_data_dump();}break;}….}}} }??當信號是SIGBREAK(在jvm里做了#define,其實就是SIGQUIT)的時候,就會觸發 AttachListener::is_init_trigger()的執行,
bool AttachListener::is_init_trigger() {if (init_at_startup() || is_initialized()) {return false; // initialized at startup or already initialized}char fn[PATH_MAX+1];sprintf(fn, ".Attach_pid%d", os::current_process_id());int ret;struct stat64 st;RESTARTABLE(::stat64(fn, &st), ret);if (ret == -1) {snprintf(fn, sizeof(fn), "%s/.Attach_pid%d",os::get_temp_directory(), os::current_process_id());RESTARTABLE(::stat64(fn, &st), ret);}if (ret == 0) {// simple check to avoid starting the Attach mechanism when// a bogus user creates the fileif (st.st_uid == geteuid()) {init();return true;}}return false; }??一開始會判斷當前進程目錄下是否有個.Attach_pid文件(前面提到了),如果沒有就會在/tmp下創建一個/tmp/.Attach_pid,當那個文件的uid和自己的uid是一致的情況下(為了安全)再調用init方法
// Starts the Attach Listener thread void AttachListener::init() {EXCEPTION_MARK;klassOop k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);instanceKlassHandle klass (THREAD, k);instanceHandle thread_oop = klass->allocate_instance_handle(CHECK);const char thread_name[] = "Attach Listener";Handle string = java_lang_String::create_from_str(thread_name, CHECK);// Initialize thread_oop to put it into the system threadGroupHandle thread_group (THREAD, Universe::system_thread_group());JavaValue result(T_VOID);JavaCalls::call_special(&result, thread_oop,klass,vmSymbols::object_initializer_name(),vmSymbols::threadgroup_string_void_signature(),thread_group,string,CHECK);KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());JavaCalls::call_special(&result,thread_group,group,vmSymbols::add_method_name(),vmSymbols::thread_void_signature(),thread_oop, // ARG 1CHECK);{ MutexLocker mu(Threads_lock);JavaThread* listener_thread = new JavaThread(&Attach_listener_thread_entry);// Check that thread and osthread were createdif (listener_thread == NULL || listener_thread->osthread() == NULL) {vm_exit_during_initialization("java.lang.OutOfMemoryError","unable to create new native thread");}java_lang_Thread::set_thread(thread_oop(), listener_thread);java_lang_Thread::set_daemon(thread_oop());listener_thread->set_threadObj(thread_oop());Threads::add(listener_thread);Thread::start(listener_thread);} }??此時水落石出了,看到創建了一個線程,并且取名為Attach Listener。再看看其子類LinuxAttachListener的init方法
int LinuxAttachListener::init() {char path[UNIX_PATH_MAX]; // socket filechar initial_path[UNIX_PATH_MAX]; // socket file during setupint listener; // listener socket (file descriptor)// register function to cleanup::atexit(listener_cleanup);int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",os::get_temp_directory(), os::current_process_id());if (n < (int)UNIX_PATH_MAX) {n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);}if (n >= (int)UNIX_PATH_MAX) {return -1;}// create the listener socketlistener = ::socket(PF_UNIX, SOCK_STREAM, 0);if (listener == -1) {return -1;}// bind socketstruct sockaddr_un addr;addr.sun_family = AF_UNIX;strcpy(addr.sun_path, initial_path);::unlink(initial_path);int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));if (res == -1) {RESTARTABLE(::close(listener), res);return -1;}// put in listen mode, set permissions, and rename into placeres = ::listen(listener, 5);if (res == 0) {RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);if (res == 0) {res = ::rename(initial_path, path);}}if (res == -1) {RESTARTABLE(::close(listener), res);::unlink(initial_path);return -1;}set_path(path);set_listener(listener);return 0; }??看到其創建了一個監聽套接字,并創建了一個文件/tmp/.java_pid,這個文件就是客戶端之前一直在輪詢等待的文件,隨著這個文件的生成,意味著Attach的過程圓滿結束了。
Attach listener接收請求
??看看它的entry實現Attach_listener_thread_entry
static void Attach_listener_thread_entry(JavaThread* thread, TRAPS) {os::set_priority(thread, NearMaxPriority);thread->record_stack_base_and_size();if (AttachListener::pd_init() != 0) {return;}AttachListener::set_initialized();for (;;) {AttachOperation* op = AttachListener::dequeue();if (op == NULL) {return; // dequeue failed or shutdown}ResourceMark rm;bufferedStream st;jint res = JNI_OK;// handle special detachall operationif (strcmp(op->name(), AttachOperation::detachall_operation_name()) == 0) {AttachListener::detachall();} else {// find the function to dispatch tooAttachOperationFunctionInfo* info = NULL;for (int i=0; funcs[i].name != NULL; i++) {const char* name = funcs[i].name;assert(strlen(name) <= AttachOperation::name_length_max, "operation <= name_length_max");if (strcmp(op->name(), name) == 0) {info = &(funcs[i]);break;}}// check for platform dependent Attach operationif (info == NULL) {info = AttachListener::pd_find_operation(op->name());}if (info != NULL) {// dispatch to the function that implements this operationres = (info->func)(op, &st);} else {st.print("Operation %s not recognized!", op->name());res = JNI_ERR;}}// operation complete - send result and output to clientop->complete(res, &st);} }??從代碼來看就是從隊列里不斷取AttachOperation,然后找到請求命令對應的方法進行執行,比如我們一開始說的jstack命令,找到 { “threaddump”, thread_dump }的映射關系,然后執行thread_dump方法
??再來看看其要調用的AttachListener::dequeue(),
AttachOperation* AttachListener::dequeue() {JavaThread* thread = JavaThread::current();ThreadBlockInVM tbivm(thread);thread->set_suspend_equivalent();// cleared by handle_special_suspend_equivalent_condition() or// java_suspend_self() via check_and_wait_while_suspended()AttachOperation* op = LinuxAttachListener::dequeue();// were we externally suspended while we were waiting?thread->check_and_wait_while_suspended();return op; }??最終調用的是LinuxAttachListener::dequeue(),
LinuxAttachOperation* LinuxAttachListener::dequeue() {for (;;) {int s;// wait for client to connectstruct sockaddr addr;socklen_t len = sizeof(addr);RESTARTABLE(::accept(listener(), &addr, &len), s);if (s == -1) {return NULL; // log a warning?}// get the credentials of the peer and check the effective uid/guid// - check with jeff on this.struct ucred cred_info;socklen_t optlen = sizeof(cred_info);if (::getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void*)&cred_info, &optlen) == -1) {int res;RESTARTABLE(::close(s), res);continue;}uid_t euid = geteuid();gid_t egid = getegid();if (cred_info.uid != euid || cred_info.gid != egid) {int res;RESTARTABLE(::close(s), res);continue;}// peer credential look okay so we read the requestLinuxAttachOperation* op = read_request(s);if (op == NULL) {int res;RESTARTABLE(::close(s), res);continue;} else {return op;}} }??我們看到如果沒有請求的話,會一直accept在那里,當來了請求,然后就會創建一個套接字,并讀取數據,構建出LinuxAttachOperation返回并執行。
??整個過程就這樣了,從Attach線程創建到接收請求,處理請求。
****************************************************************************************************************
我們已經提到過了JDK自帶的jstack是使用的Dynamic Attach機制。下面是官方文檔對該機制的描述,
This is a Sun private mechanism that allows an external process to start a thread in HotSpot that can then be used to launch an agent to run in that HotSpot, and to send information about the state of HotSpot back to the external process.
那么現在的問題也就變成了external process跟target VM process間的IPC問題了,那么到底是使用了什么樣的IPC方式呢,
On Linux and Solaris, the client creates a file named?.attach_pid<pid>?and sends a?SIGQUIT?to the target JVM process. The existence of this file causes the?SIGQUIT?handler in HotSpot to start the attach listener thread. On Windows, the client uses the Win32?CreateRemoteThread?function to create a new thread in the target process. The attach listener thread then communicates with the source JVM in an OS dependent manner:
- On Solaris, the?Doors IPC?mechanism is used. The door is attached to a file in the file system so that clients can access it.
- On Linux, a?Unix domain socket?is used. This socket is bound to a file in the filesystem so that clients can access it.
- On Windows, the created thread is given the name of a?pipe?which is served by the client. The result of the operations are written to this pipe by the target JVM.
也就是說,以Linux平臺為栗,使用了兩種IPC方式,
這個過程中,會有兩個文件被創建,
- .attach_pid<pid>,external process會創建這個文件,為的是觸發Attach Listener線程的創建,因為SIGQUIT信號不是只有external process才會發的,通過這個文件來告訴target VM,有attach請求過來了。相關代碼在LinuxVirtualMachine.java中;
- .java_pid<pid>,target VM會創建這個文件,這個是因為Unix domain socket本身的實現機制需要去創建一個文件,通過這個文件來進行IPC。相關代碼在attachListener_linux.cpp中;
其中的<pid>都是target VM的pid。
源地址:http://lovestblog.cn/blog/2014/06/18/jvm-attach/?spm=5176.100239.blogcont2946.9.7b8977f7vohTwX
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的JVM Attach机制实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 3DS MAX怎么制作极品奔驰跑车 3D
- 下一篇: 当前火热的短视频,背后有着哪些黑科技技术