轉載:https://www.cnblogs.com/scofield-1987/p/9347586.html
前言
本文旨在從理論上分析JVM 在 Linux 環境下 Attach 操作的前因后果,以及 JVM 為此而設計並實現的解決方案,通過本文,我希望能夠講述清楚如下三個主要方面的內容。
原發布:我的博客
一、Attach 為什么而出現
Attach的出現究其根本原因,應該就是為了實現 Java 進程(A)與進程(B)之間的本地通信。一旦這個通信通道能夠成功建立,那么進程 A 就能通知進程 B 去執行某些操作,從而達到監控進程 B 或者控制進程 B 的某些行為的目的。如 jstack、jmap等 JDK 自帶的工具,基本都是通過 Attach 機制去達成各自想要的目的的。至於 jstack、jmap 能做什么、怎么做,就不再本文的討論范圍了,請自行百度或者 Google。
二、Attach 在 JVM 底層實現的根本原理是什么
Attach 實現的根本原理就是使用了 Linux 下是文件 Socket 通信(詳情可以自行百度或 Google)。有人也許會問,為什么要采用文件 socket 而不采用網絡 socket?我個人認為也許一方面是為了效率(避免了網絡協議的解析、數據包的封裝和解封裝等),另一方面是為了減少對系統資源的占用(如網絡端口占用)。采用文件 socket 通信,就好比兩個進程通過事先約定好的協議,對同一個文件進行讀寫操作,以達到信息的交互和共享。簡單理解成如下圖所示的模型
通過/tmp/.java.pid2345這個文件,實現客戶進程與目標進程2345的通信。
三、Attach 在 JVM 中實現的源碼分析
源碼的分析主要分三階段進行,這里要達到的目的是,弄 Attach 的清楚來龍去脈,本文的所有源碼都是基於 Open JDK 1.8的,大家可以自行去下載 Open JDK 1.8 的源碼。
3.1、目標JVM 對OS信號監聽的實現
或許你會想,在最開始的時候,目標 JVM 是怎么知道有某個進程想 attach 它自己的?答案很簡單,就是目標 JVM 在啟動的時候,在 JVM 內部啟動了一個監聽線程,這個線程的名字叫“Signal Dispatcher”,該線程的作用是,監聽並處理 OS 的信號。至於什么是 OS 的信號(可以自行百度或 Google),簡單理解就是,Linux系統允許進程與進程之間通過過信號的方式進行通信,如觸發某個操作(操作由接受到信號的進程自定義)。如平常我們用的最多的就是 kill -9 ${pid}來殺死某個進程,kill進程通過向${pid}的進程發送一個編號為“9”號的信號,來通知系統強制結束${pid}的生命周期。
接下來我們就通過源碼截圖的方式來呈現一下“Signal Dispatcher”線程的創建過程。
首先進入 JVM 的啟動類:/jdk/src/share/bin/main.c
1 int 2 main(int argc, char **argv) 3 { 4 int margc; 5 char** margv; 6 const jboolean const_javaw = JNI_FALSE; 7 #endif /* JAVAW */ 8 #ifdef _WIN32 9 { 10 int i = 0; 11 if (getenv(JLDEBUG_ENV_ENTRY) != NULL) { 12 printf("Windows original main args:\n"); 13 for (i = 0 ; i < __argc ; i++) { 14 printf("wwwd_args[%d] = %s\n", i, __argv[i]); 15 } 16 } 17 } 18 JLI_CmdToArgs(GetCommandLine()); 19 margc = JLI_GetStdArgc(); 20 // add one more to mark the end 21 margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *))); 22 { 23 int i = 0; 24 StdArg *stdargs = JLI_GetStdArgs(); 25 for (i = 0 ; i < margc ; i++) { 26 margv[i] = stdargs[i].arg; 27 } 28 margv[i] = NULL; 29 } 30 #else /* *NIXES */ 31 margc = argc; 32 margv = argv; 33 #endif /* WIN32 */ 34 return JLI_Launch(margc, margv, 35 sizeof(const_jargs) / sizeof(char *), const_jargs, 36 sizeof(const_appclasspath) / sizeof(char *), const_appclasspath, 37 FULL_VERSION, 38 DOT_VERSION, 39 (const_progname != NULL) ? const_progname : *margv, 40 (const_launcher != NULL) ? const_launcher : *margv, 41 (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE, 42 const_cpwildcard, const_javaw, const_ergo_class); 43 }
這個類里邊最重要的一個方法就是最后的JLI_Launch,這個方法的實現存在於jdk/src/share/bin/java.c 中(大家應該都不陌生平時我們運行 java 程序時,都是采用 java com.***.Main來啟動的吧)。
1 /* 2 * Entry point. 3 */ 4 int 5 JLI_Launch(int argc, char ** argv, /* main argc, argc */ 6 int jargc, const char** jargv, /* java args */ 7 int appclassc, const char** appclassv, /* app classpath */ 8 const char* fullversion, /* full version defined */ 9 const char* dotversion, /* dot version defined */ 10 const char* pname, /* program name */ 11 const char* lname, /* launcher name */ 12 jboolean javaargs, /* JAVA_ARGS */ 13 jboolean cpwildcard, /* classpath wildcard*/ 14 jboolean javaw, /* windows-only javaw */ 15 jint ergo /* ergonomics class policy */ 16 ) 17 { 18 int mode = LM_UNKNOWN; 19 char *what = NULL; 20 char *cpath = 0; 21 char *main_class = NULL; 22 int ret; 23 InvocationFunctions ifn; 24 jlong start, end; 25 char jvmpath[MAXPATHLEN]; 26 char jrepath[MAXPATHLEN]; 27 char jvmcfg[MAXPATHLEN]; 28 29 _fVersion = fullversion; 30 _dVersion = dotversion; 31 _launcher_name = lname; 32 _program_name = pname; 33 _is_java_args = javaargs; 34 _wc_enabled = cpwildcard; 35 _ergo_policy = ergo; 36 37 InitLauncher(javaw); 38 DumpState(); 39 if (JLI_IsTraceLauncher()) { 40 int i; 41 printf("Command line args:\n"); 42 for (i = 0; i < argc ; i++) { 43 printf("argv[%d] = %s\n", i, argv[i]); 44 } 45 AddOption("-Dsun.java.launcher.diag=true", NULL); 46 } 47 48 /* 49 * Make sure the specified version of the JRE is running. 50 * 51 * There are three things to note about the SelectVersion() routine: 52 * 1) If the version running isn't correct, this routine doesn't 53 * return (either the correct version has been exec'd or an error 54 * was issued). 55 * 2) Argc and Argv in this scope are *not* altered by this routine. 56 * It is the responsibility of subsequent code to ignore the 57 * arguments handled by this routine. 58 * 3) As a side-effect, the variable "main_class" is guaranteed to 59 * be set (if it should ever be set). This isn't exactly the 60 * poster child for structured programming, but it is a small 61 * price to pay for not processing a jar file operand twice. 62 * (Note: This side effect has been disabled. See comment on 63 * bugid 5030265 below.) 64 */ 65 SelectVersion(argc, argv, &main_class); 66 67 CreateExecutionEnvironment(&argc, &argv, 68 jrepath, sizeof(jrepath), 69 jvmpath, sizeof(jvmpath), 70 jvmcfg, sizeof(jvmcfg)); 71 72 ifn.CreateJavaVM = 0; 73 ifn.GetDefaultJavaVMInitArgs = 0; 74 75 if (JLI_IsTraceLauncher()) { 76 start = CounterGet(); 77 } 78 79 if (!LoadJavaVM(jvmpath, &ifn)) { 80 return(6); 81 } 82 83 if (JLI_IsTraceLauncher()) { 84 end = CounterGet(); 85 } 86 87 JLI_TraceLauncher("%ld micro seconds to LoadJavaVM\n", 88 (long)(jint)Counter2Micros(end-start)); 89 90 ++argv; 91 --argc; 92 93 if (IsJavaArgs()) { 94 /* Preprocess wrapper arguments */ 95 TranslateApplicationArgs(jargc, jargv, &argc, &argv); 96 if (!AddApplicationOptions(appclassc, appclassv)) { 97 return(1); 98 } 99 } else { 100 /* Set default CLASSPATH */ 101 cpath = getenv("CLASSPATH"); 102 if (cpath == NULL) { 103 cpath = "."; 104 } 105 SetClassPath(cpath); 106 } 107 108 /* Parse command line options; if the return value of 109 * ParseArguments is false, the program should exit. 110 */ 111 if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath)) 112 { 113 return(ret); 114 } 115 116 /* Override class path if -jar flag was specified */ 117 if (mode == LM_JAR) { 118 SetClassPath(what); /* Override class path */ 119 } 120 121 /* set the -Dsun.java.command pseudo property */ 122 SetJavaCommandLineProp(what, argc, argv); 123 124 /* Set the -Dsun.java.launcher pseudo property */ 125 SetJavaLauncherProp(); 126 127 /* set the -Dsun.java.launcher.* platform properties */ 128 SetJavaLauncherPlatformProps(); 129 130 return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret); 131 }
這個方法中,進行了一系列必要的操作,如libjvm.so的加載、參數解析、Classpath 的獲取和設置、系統屬性的設置、JVM 初始化等等,不過和本文相關的主要是130行的 JVMInit 方法,接下來我們看下這個方法的實現(位於/jdk/src/solaris/bin/java_md_solinux.c)。
1 int 2 JVMInit(InvocationFunctions* ifn, jlong threadStackSize, 3 int argc, char **argv, 4 int mode, char *what, int ret) 5 { 6 ShowSplashScreen(); 7 return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret); 8 }
這里請關注兩個點,ContinueInNewThread方法 和 ifn 入參。ContinueInNewThread位於 java.c中,而 ifn 則攜帶了libjvm.so中的幾個非常重要的函數(CreateJavaVM/GetDefaultJavaVMInitArgs/GetCreatedJavaVMs),這里我們重點關注CreateJavaVM
1 int 2 ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize, 3 int argc, char **argv, 4 int mode, char *what, int ret) 5 { 6 7 /* 8 * If user doesn't specify stack size, check if VM has a preference. 9 * Note that HotSpot no longer supports JNI_VERSION_1_1 but it will 10 * return its default stack size through the init args structure. 11 */ 12 if (threadStackSize == 0) { 13 struct JDK1_1InitArgs args1_1; 14 memset((void*)&args1_1, 0, sizeof(args1_1)); 15 args1_1.version = JNI_VERSION_1_1; 16 ifn->GetDefaultJavaVMInitArgs(&args1_1); /* ignore return value */ 17 if (args1_1.javaStackSize > 0) { 18 threadStackSize = args1_1.javaStackSize; 19 } 20 } 21 22 { /* Create a new thread to create JVM and invoke main method */ 23 JavaMainArgs args; 24 int rslt; 25 26 args.argc = argc; 27 args.argv = argv; 28 args.mode = mode; 29 args.what = what; 30 args.ifn = *ifn; 31 32 rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args); 33 /* If the caller has deemed there is an error we 34 * simply return that, otherwise we return the value of 35 * the callee 36 */ 37 return (ret != 0) ? ret : rslt; 38 } 39 }
可以看出,這里進行了 JavaMainArgs 參數設置,設置完成之后,在32行處調用了 ContinueInNewThread0 (位於/jdk/src/solaris/bin/java_md_solinux.c)方法,該方法中傳入了 JavaMain 函數指針和 args 參數,這二者至關重要。接下來看下其源碼
1 /* 2 * Block current thread and continue execution in a new thread 3 */ 4 int 5 ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) { 6 int rslt; 7 #ifdef __linux__ 8 pthread_t tid; 9 pthread_attr_t attr; 10 pthread_attr_init(&attr); 11 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); 12 13 if (stack_size > 0) { 14 pthread_attr_setstacksize(&attr, stack_size); 15 } 16 17 if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) { 18 void * tmp; 19 pthread_join(tid, &tmp); 20 rslt = (int)tmp; 21 } else { 22 /* 23 * Continue execution in current thread if for some reason (e.g. out of 24 * memory/LWP) a new thread can't be created. This will likely fail 25 * later in continuation as JNI_CreateJavaVM needs to create quite a 26 * few new threads, anyway, just give it a try.. 27 */ 28 rslt = continuation(args); 29 } 30 31 pthread_attr_destroy(&attr); 32 #else /* ! __linux__ */ 33 thread_t tid; 34 long flags = 0; 35 if (thr_create(NULL, stack_size, (void *(*)(void *))continuation, args, flags, &tid) == 0) { 36 void * tmp; 37 thr_join(tid, NULL, &tmp); 38 rslt = (int)tmp; 39 } else { 40 /* See above. Continue in current thread if thr_create() failed */ 41 rslt = continuation(args); 42 } 43 #endif /* __linux__ */ 44 return rslt; 45 }
這里最關鍵的點在於,如果是 linux 環境下,則創建了一個 pthread_t 的線程來運行傳入的 JavaMain 函數,並且將 args 參數也一並傳入了。這時候,我們唯一要關注的便是 JavaMain (在jdk/src/share/bin/java.c )函數,請看源碼
1 int JNICALL 2 JavaMain(void * _args) 3 { 4 JavaMainArgs *args = (JavaMainArgs *)_args; 5 int argc = args->argc; 6 char **argv = args->argv; 7 int mode = args->mode; 8 char *what = args->what; 9 InvocationFunctions ifn = args->ifn; 10 11 JavaVM *vm = 0; 12 JNIEnv *env = 0; 13 jclass mainClass = NULL; 14 jclass appClass = NULL; // actual application class being launched 15 jmethodID mainID; 16 jobjectArray mainArgs; 17 int ret = 0; 18 jlong start, end; 19 20 RegisterThread(); 21 22 /* Initialize the virtual machine */ 23 start = CounterGet(); 24 if (!InitializeJVM(&vm, &env, &ifn)) { 25 JLI_ReportErrorMessage(JVM_ERROR1); 26 exit(1); 27 } 28 29 if (showSettings != NULL) { 30 ShowSettings(env, showSettings); 31 CHECK_EXCEPTION_LEAVE(1); 32 } 33 34 if (printVersion || showVersion) { 35 PrintJavaVersion(env, showVersion); 36 CHECK_EXCEPTION_LEAVE(0); 37 if (printVersion) { 38 LEAVE(); 39 } 40 } 41 42 /* If the user specified neither a class name nor a JAR file */ 43 if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) { 44 PrintUsage(env, printXUsage); 45 CHECK_EXCEPTION_LEAVE(1); 46 LEAVE(); 47 } 48 49 FreeKnownVMs(); /* after last possible PrintUsage() */ 50 51 if (JLI_IsTraceLauncher()) { 52 end = CounterGet(); 53 JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n", 54 (long)(jint)Counter2Micros(end-start)); 55 } 56 57 /* At this stage, argc/argv have the application's arguments */ 58 if (JLI_IsTraceLauncher()){ 59 int i; 60 printf("%s is '%s'\n", launchModeNames[mode], what); 61 printf("App's argc is %d\n", argc); 62 for (i=0; i < argc; i++) { 63 printf(" argv[%2d] = '%s'\n", i, argv[i]); 64 } 65 } 66 67 ret = 1; 68 69 /* 70 * Get the application's main class. 71 * 72 * See bugid 5030265. The Main-Class name has already been parsed 73 * from the manifest, but not parsed properly for UTF-8 support. 74 * Hence the code here ignores the value previously extracted and 75 * uses the pre-existing code to reextract the value. This is 76 * possibly an end of release cycle expedient. However, it has 77 * also been discovered that passing some character sets through 78 * the environment has "strange" behavior on some variants of 79 * Windows. Hence, maybe the manifest parsing code local to the 80 * launcher should never be enhanced. 81 * 82 * Hence, future work should either: 83 * 1) Correct the local parsing code and verify that the 84 * Main-Class attribute gets properly passed through 85 * all environments, 86 * 2) Remove the vestages of maintaining main_class through 87 * the environment (and remove these comments). 88 * 89 * This method also correctly handles launching existing JavaFX 90 * applications that may or may not have a Main-Class manifest entry. 91 */ 92 mainClass = LoadMainClass(env, mode, what); 93 CHECK_EXCEPTION_NULL_LEAVE(mainClass); 94 /* 95 * In some cases when launching an application that needs a helper, e.g., a 96 * JavaFX application with no main method, the mainClass will not be the 97 * applications own main class but rather a helper class. To keep things 98 * consistent in the UI we need to track and report the application main class. 99 */ 100 appClass = GetApplicationClass(env); 101 NULL_CHECK_RETURN_VALUE(appClass, -1); 102 /* 103 * PostJVMInit uses the class name as the application name for GUI purposes, 104 * for example, on OSX this sets the application name in the menu bar for 105 * both SWT and JavaFX. So we'll pass the actual application class here 106 * instead of mainClass as that may be a launcher or helper class instead 107 * of the application class. 108 */ 109 PostJVMInit(env, appClass, vm); 110 /* 111 * The LoadMainClass not only loads the main class, it will also ensure 112 * that the main method's signature is correct, therefore further checking 113 * is not required. The main method is invoked here so that extraneous java 114 * stacks are not in the application stack trace. 115 */ 116 mainID = (*env)->GetStaticMethodID(env, mainClass, "main", 117 "([Ljava/lang/String;)V"); 118 CHECK_EXCEPTION_NULL_LEAVE(mainID); 119 120 /* Build platform specific argument array */ 121 mainArgs = CreateApplicationArgs(env, argv, argc); 122 CHECK_EXCEPTION_NULL_LEAVE(mainArgs); 123 124 /* Invoke main method. */ 125 (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); 126 127 /* 128 * The launcher's exit code (in the absence of calls to 129 * System.exit) will be non-zero if main threw an exception. 130 */ 131 ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1; 132 LEAVE(); 133 }
和本小節相關的函數為InitializeJVM函數,在這個函數中,調用CreateJavaVM方法,這個方法就是之前在加載 libjvm.so 的時候,從動態庫中獲取的,首先看InitializeJVM的源碼
1 /* 2 * Initializes the Java Virtual Machine. Also frees options array when 3 * finished. 4 */ 5 static jboolean 6 InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn) 7 { 8 JavaVMInitArgs args; 9 jint r; 10 11 memset(&args, 0, sizeof(args)); 12 args.version = JNI_VERSION_1_2; 13 args.nOptions = numOptions; 14 args.options = options; 15 args.ignoreUnrecognized = JNI_FALSE; 16 17 if (JLI_IsTraceLauncher()) { 18 int i = 0; 19 printf("JavaVM args:\n "); 20 printf("version 0x%08lx, ", (long)args.version); 21 printf("ignoreUnrecognized is %s, ", 22 args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE"); 23 printf("nOptions is %ld\n", (long)args.nOptions); 24 for (i = 0; i < numOptions; i++) 25 printf(" option[%2d] = '%s'\n", 26 i, args.options[i].optionString); 27 } 28 29 r = ifn->CreateJavaVM(pvm, (void **)penv, &args); 30 JLI_MemFree(options); 31 return r == JNI_OK; 32 }
29行處,調用 CreateJavaVM(定義在hotspot/src/share/vm/prims/jni.cpp) 方法,來進行 JVM 虛擬機的真正創建過程,源碼如下
1 _JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) { 2 #ifndef USDT2 3 HS_DTRACE_PROBE3(hotspot_jni, CreateJavaVM__entry, vm, penv, args); 4 #else /* USDT2 */ 5 HOTSPOT_JNI_CREATEJAVAVM_ENTRY( 6 (void **) vm, penv, args); 7 #endif /* USDT2 */ 8 9 jint result = JNI_ERR; 10 DT_RETURN_MARK(CreateJavaVM, jint, (const jint&)result); 11 12 // We're about to use Atomic::xchg for synchronization. Some Zero 13 // platforms use the GCC builtin __sync_lock_test_and_set for this, 14 // but __sync_lock_test_and_set is not guaranteed to do what we want 15 // on all architectures. So we check it works before relying on it. 16 #if defined(ZERO) && defined(ASSERT) 17 { 18 jint a = 0xcafebabe; 19 jint b = Atomic::xchg(0xdeadbeef, &a); 20 void *c = &a; 21 void *d = Atomic::xchg_ptr(&b, &c); 22 assert(a == (jint) 0xdeadbeef && b == (jint) 0xcafebabe, "Atomic::xchg() works"); 23 assert(c == &b && d == &a, "Atomic::xchg_ptr() works"); 24 } 25 #endif // ZERO && ASSERT 26 27 // At the moment it's only possible to have one Java VM, 28 // since some of the runtime state is in global variables. 29 30 // We cannot use our mutex locks here, since they only work on 31 // Threads. We do an atomic compare and exchange to ensure only 32 // one thread can call this method at a time 33 34 // We use Atomic::xchg rather than Atomic::add/dec since on some platforms 35 // the add/dec implementations are dependent on whether we are running 36 // on a multiprocessor, and at this stage of initialization the os::is_MP 37 // function used to determine this will always return false. Atomic::xchg 38 // does not have this problem. 39 if (Atomic::xchg(1, &vm_created) == 1) { 40 return JNI_EEXIST; // already created, or create attempt in progress 41 } 42 if (Atomic::xchg(0, &safe_to_recreate_vm) == 0) { 43 return JNI_ERR; // someone tried and failed and retry not allowed. 44 } 45 46 assert(vm_created == 1, "vm_created is true during the creation"); 47 48 /** 49 * Certain errors during initialization are recoverable and do not 50 * prevent this method from being called again at a later time 51 * (perhaps with different arguments). However, at a certain 52 * point during initialization if an error occurs we cannot allow 53 * this function to be called again (or it will crash). In those 54 * situations, the 'canTryAgain' flag is set to false, which atomically 55 * sets safe_to_recreate_vm to 1, such that any new call to 56 * JNI_CreateJavaVM will immediately fail using the above logic. 57 */ 58 bool can_try_again = true; 59 60 result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again); 61 if (result == JNI_OK) { 62 JavaThread *thread = JavaThread::current(); 63 /* thread is thread_in_vm here */ 64 *vm = (JavaVM *)(&main_vm); 65 *(JNIEnv**)penv = thread->jni_environment(); 66 67 // Tracks the time application was running before GC 68 RuntimeService::record_application_start(); 69 70 // Notify JVMTI 71 if (JvmtiExport::should_post_thread_life()) { 72 JvmtiExport::post_thread_start(thread); 73 } 74 75 EventThreadStart event; 76 if (event.should_commit()) { 77 event.set_javalangthread(java_lang_Thread::thread_id(thread->threadObj())); 78 event.commit(); 79 } 80 81 #ifndef PRODUCT 82 #ifndef TARGET_OS_FAMILY_windows 83 #define CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(f) f() 84 #endif 85 86 // Check if we should compile all classes on bootclasspath 87 if (CompileTheWorld) ClassLoader::compile_the_world(); 88 if (ReplayCompiles) ciReplay::replay(thread); 89 90 // Some platforms (like Win*) need a wrapper around these test 91 // functions in order to properly handle error conditions. 92 CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(test_error_handler); 93 CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(execute_internal_vm_tests); 94 #endif 95 96 // Since this is not a JVM_ENTRY we have to set the thread state manually before leaving. 97 ThreadStateTransition::transition_and_fence(thread, _thread_in_vm, _thread_in_native); 98 } else { 99 if (can_try_again) { 100 // reset safe_to_recreate_vm to 1 so that retrial would be possible 101 safe_to_recreate_vm = 1; 102 } 103 104 // Creation failed. We must reset vm_created 105 *vm = 0; 106 *(JNIEnv**)penv = 0; 107 // reset vm_created last to avoid race condition. Use OrderAccess to 108 // control both compiler and architectural-based reordering. 109 OrderAccess::release_store(&vm_created, 0); 110 } 111 112 return result; 113 }
這里只關注最核心的方法是60行的Threads::create_vm(hotspot/src/share/vm/runtime/Thread.cpp) 方法,在這個方法中,進行了大量的初始化操作,不過,這里我們只關注其中的一個點,就是 os::signal_init() 方法的調用,這就是啟動“Signal Dispatcher”線程的地方。先看 create_vm 的源碼
1 jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) { 2 3 extern void JDK_Version_init(); 4 5 // Check version 6 if (!is_supported_jni_version(args->version)) return JNI_EVERSION; 7 8 // Initialize the output stream module 9 ostream_init(); 10 11 // Process java launcher properties. 12 Arguments::process_sun_java_launcher_properties(args); 13 14 // Initialize the os module before using TLS 15 os::init(); 16 17 // Initialize system properties. 18 Arguments::init_system_properties(); 19 20 // So that JDK version can be used as a discrimintor when parsing arguments 21 JDK_Version_init(); 22 23 // Update/Initialize System properties after JDK version number is known 24 Arguments::init_version_specific_system_properties(); 25 26 // Parse arguments 27 jint parse_result = Arguments::parse(args); 28 if (parse_result != JNI_OK) return parse_result; 29 30 os::init_before_ergo(); 31 32 jint ergo_result = Arguments::apply_ergo(); 33 if (ergo_result != JNI_OK) return ergo_result; 34 35 if (PauseAtStartup) { 36 os::pause(); 37 } 38 39 #ifndef USDT2 40 HS_DTRACE_PROBE(hotspot, vm__init__begin); 41 #else /* USDT2 */ 42 HOTSPOT_VM_INIT_BEGIN(); 43 #endif /* USDT2 */ 44 45 // Record VM creation timing statistics 46 TraceVmCreationTime create_vm_timer; 47 create_vm_timer.start(); 48 49 // Timing (must come after argument parsing) 50 TraceTime timer("Create VM", TraceStartupTime); 51 52 // Initialize the os module after parsing the args 53 jint os_init_2_result = os::init_2(); 54 if (os_init_2_result != JNI_OK) return os_init_2_result; 55 56 jint adjust_after_os_result = Arguments::adjust_after_os(); 57 if (adjust_after_os_result != JNI_OK) return adjust_after_os_result; 58 59 // intialize TLS 60 ThreadLocalStorage::init(); 61 62 // Bootstrap native memory tracking, so it can start recording memory 63 // activities before worker thread is started. This is the first phase 64 // of bootstrapping, VM is currently running in single-thread mode. 65 MemTracker::bootstrap_single_thread(); 66 67 // Initialize output stream logging 68 ostream_init_log(); 69 70 // Convert -Xrun to -agentlib: if there is no JVM_OnLoad 71 // Must be before create_vm_init_agents() 72 if (Arguments::init_libraries_at_startup()) { 73 convert_vm_init_libraries_to_agents(); 74 } 75 76 // Launch -agentlib/-agentpath and converted -Xrun agents 77 if (Arguments::init_agents_at_startup()) { 78 create_vm_init_agents(); 79 } 80 81 // Initialize Threads state 82 _thread_list = NULL; 83 _number_of_threads = 0; 84 _number_of_non_daemon_threads = 0; 85 86 // Initialize global data structures and create system classes in heap 87 vm_init_globals(); 88 89 // Attach the main thread to this os thread 90 JavaThread* main_thread = new JavaThread(); 91 main_thread->set_thread_state(_thread_in_vm); 92 // must do this before set_active_handles and initialize_thread_local_storage 93 // Note: on solaris initialize_thread_local_storage() will (indirectly) 94 // change the stack size recorded here to one based on the java thread 95 // stacksize. This adjusted size is what is used to figure the placement 96 // of the guard pages. 97 main_thread->record_stack_base_and_size(); 98 main_thread->initialize_thread_local_storage(); 99 100 main_thread->set_active_handles(JNIHandleBlock::allocate_block()); 101 102 if (!main_thread->set_as_starting_thread()) { 103 vm_shutdown_during_initialization( 104 "Failed necessary internal allocation. Out of swap space"); 105 delete main_thread; 106 *canTryAgain = false; // don't let caller call JNI_CreateJavaVM again 107 return JNI_ENOMEM; 108 } 109 110 // Enable guard page *after* os::create_main_thread(), otherwise it would 111 // crash Linux VM, see notes in os_linux.cpp. 112 main_thread->create_stack_guard_pages(); 113 114 // Initialize Java-Level synchronization subsystem 115 ObjectMonitor::Initialize() ; 116 117 // Second phase of bootstrapping, VM is about entering multi-thread mode 118 MemTracker::bootstrap_multi_thread(); 119 120 // Initialize global modules 121 jint status = init_globals(); 122 if (status != JNI_OK) { 123 delete main_thread; 124 *canTryAgain = false; // don't let caller call JNI_CreateJavaVM again 125 return status; 126 } 127 128 // Should be done after the heap is fully created 129 main_thread->cache_global_variables(); 130 131 HandleMark hm; 132 133 { MutexLocker mu(Threads_lock); 134 Threads::add(main_thread); 135 } 136 137 // Any JVMTI raw monitors entered in onload will transition into 138 // real raw monitor. VM is setup enough here for raw monitor enter. 139 JvmtiExport::transition_pending_onload_raw_monitors(); 140 141 // Fully start NMT 142 MemTracker::start(); 143 144 // Create the VMThread 145 { TraceTime timer("Start VMThread", TraceStartupTime); 146 VMThread::create(); 147 Thread* vmthread = VMThread::vm_thread(); 148 149 if (!os::create_thread(vmthread, os::vm_thread)) 150 vm_exit_during_initialization("Cannot create VM thread. Out of system resources."); 151 152 // Wait for the VM thread to become ready, and VMThread::run to initialize 153 // Monitors can have spurious returns, must always check another state flag 154 { 155 MutexLocker ml(Notify_lock); 156 os::start_thread(vmthread); 157 while (vmthread->active_handles() == NULL) { 158 Notify_lock->wait(); 159 } 160 } 161 } 162 163 assert (Universe::is_fully_initialized(), "not initialized"); 164 if (VerifyDuringStartup) { 165 // Make sure we're starting with a clean slate. 166 VM_Verify verify_op; 167 VMThread::execute(&verify_op); 168 } 169 170 EXCEPTION_MARK; 171 172 // At this point, the Universe is initialized, but we have not executed 173 // any byte code. Now is a good time (the only time) to dump out the 174 // internal state of the JVM for sharing. 175 if (DumpSharedSpaces) { 176 MetaspaceShared::preload_and_dump(CHECK_0); 177 ShouldNotReachHere(); 178 } 179 180 // Always call even when there are not JVMTI environments yet, since environments 181 // may be attached late and JVMTI must track phases of VM execution 182 JvmtiExport::enter_start_phase(); 183 184 // Notify JVMTI agents that VM has started (JNI is up) - nop if no agents. 185 JvmtiExport::post_vm_start(); 186 187 { 188 TraceTime timer("Initialize java.lang classes", TraceStartupTime); 189 190 if (EagerXrunInit && Arguments::init_libraries_at_startup()) { 191 create_vm_init_libraries(); 192 } 193 194 initialize_class(vmSymbols::java_lang_String(), CHECK_0); 195 196 // Initialize java_lang.System (needed before creating the thread) 197 initialize_class(vmSymbols::java_lang_System(), CHECK_0); 198 initialize_class(vmSymbols::java_lang_ThreadGroup(), CHECK_0); 199 Handle thread_group = create_initial_thread_group(CHECK_0); 200 Universe::set_main_thread_group(thread_group()); 201 initialize_class(vmSymbols::java_lang_Thread(), CHECK_0); 202 oop thread_object = create_initial_thread(thread_group, main_thread, CHECK_0); 203 main_thread->set_threadObj(thread_object); 204 // Set thread status to running since main thread has 205 // been started and running. 206 java_lang_Thread::set_thread_status(thread_object, 207 java_lang_Thread::RUNNABLE); 208 209 // The VM creates & returns objects of this class. Make sure it's initialized. 210 initialize_class(vmSymbols::java_lang_Class(), CHECK_0); 211 212 // The VM preresolves methods to these classes. Make sure that they get initialized 213 initialize_class(vmSymbols::java_lang_reflect_Method(), CHECK_0); 214 initialize_class(vmSymbols::java_lang_ref_Finalizer(), CHECK_0); 215 call_initializeSystemClass(CHECK_0); 216 217 // get the Java runtime name after java.lang.System is initialized 218 JDK_Version::set_runtime_name(get_java_runtime_name(THREAD)); 219 JDK_Version::set_runtime_version(get_java_runtime_version(THREAD)); 220 221 // an instance of OutOfMemory exception has been allocated earlier 222 initialize_class(vmSymbols::java_lang_OutOfMemoryError(), CHECK_0); 223 initialize_class(vmSymbols::java_lang_NullPointerException(), CHECK_0); 224 initialize_class(vmSymbols::java_lang_ClassCastException(), CHECK_0); 225 initialize_class(vmSymbols::java_lang_ArrayStoreException(), CHECK_0); 226 initialize_class(vmSymbols::java_lang_ArithmeticException(), CHECK_0); 227 initialize_class(vmSymbols::java_lang_StackOverflowError(), CHECK_0); 228 initialize_class(vmSymbols::java_lang_IllegalMonitorStateException(), CHECK_0); 229 initialize_class(vmSymbols::java_lang_IllegalArgumentException(), CHECK_0); 230 } 231 232 // See : bugid 4211085. 233 // Background : the static initializer of java.lang.Compiler tries to read 234 // property"java.compiler" and read & write property "java.vm.info". 235 // When a security manager is installed through the command line 236 // option "-Djava.security.manager", the above properties are not 237 // readable and the static initializer for java.lang.Compiler fails 238 // resulting in a NoClassDefFoundError. This can happen in any 239 // user code which calls methods in java.lang.Compiler. 240 // Hack : the hack is to pre-load and initialize this class, so that only 241 // system domains are on the stack when the properties are read. 242 // Currently even the AWT code has calls to methods in java.lang.Compiler. 243 // On the classic VM, java.lang.Compiler is loaded very early to load the JIT. 244 // Future Fix : the best fix is to grant everyone permissions to read "java.compiler" and 245 // read and write"java.vm.info" in the default policy file. See bugid 4211383 246 // Once that is done, we should remove this hack. 247 initialize_class(vmSymbols::java_lang_Compiler(), CHECK_0); 248 249 // More hackery - the static initializer of java.lang.Compiler adds the string "nojit" to 250 // the java.vm.info property if no jit gets loaded through java.lang.Compiler (the hotspot 251 // compiler does not get loaded through java.lang.Compiler). "java -version" with the 252 // hotspot vm says "nojit" all the time which is confusing. So, we reset it here. 253 // This should also be taken out as soon as 4211383 gets fixed. 254 reset_vm_info_property(CHECK_0); 255 256 quicken_jni_functions(); 257 258 // Must be run after init_ft which initializes ft_enabled 259 if (TRACE_INITIALIZE() != JNI_OK) { 260 vm_exit_during_initialization("Failed to initialize tracing backend"); 261 } 262 263 // Set flag that basic initialization has completed. Used by exceptions and various 264 // debug stuff, that does not work until all basic classes have been initialized. 265 set_init_completed(); 266 267 #ifndef USDT2 268 HS_DTRACE_PROBE(hotspot, vm__init__end); 269 #else /* USDT2 */ 270 HOTSPOT_VM_INIT_END(); 271 #endif /* USDT2 */ 272 273 // record VM initialization completion time 274 #if INCLUDE_MANAGEMENT 275 Management::record_vm_init_completed(); 276 #endif // INCLUDE_MANAGEMENT 277 278 // Compute system loader. Note that this has to occur after set_init_completed, since 279 // valid exceptions may be thrown in the process. 280 // Note that we do not use CHECK_0 here since we are inside an EXCEPTION_MARK and 281 // set_init_completed has just been called, causing exceptions not to be shortcut 282 // anymore. We call vm_exit_during_initialization directly instead. 283 SystemDictionary::compute_java_system_loader(THREAD); 284 if (HAS_PENDING_EXCEPTION) { 285 vm_exit_during_initialization(Handle(THREAD, PENDING_EXCEPTION)); 286 } 287 288 #if INCLUDE_ALL_GCS 289 // Support for ConcurrentMarkSweep. This should be cleaned up 290 // and better encapsulated. The ugly nested if test would go away 291 // once things are properly refactored. XXX YSR 292 if (UseConcMarkSweepGC || UseG1GC) { 293 if (UseConcMarkSweepGC) { 294 ConcurrentMarkSweepThread::makeSurrogateLockerThread(THREAD); 295 } else { 296 ConcurrentMarkThread::makeSurrogateLockerThread(THREAD); 297 } 298 if (HAS_PENDING_EXCEPTION) { 299 vm_exit_during_initialization(Handle(THREAD, PENDING_EXCEPTION)); 300 } 301 } 302 #endif // INCLUDE_ALL_GCS 303 304 // Always call even when there are not JVMTI environments yet, since environments 305 // may be attached late and JVMTI must track phases of VM execution 306 JvmtiExport::enter_live_phase(); 307 308 // Signal Dispatcher needs to be started before VMInit event is posted 309 os::signal_init(); 310 311 // Start Attach Listener if +StartAttachListener or it can't be started lazily 312 if (!DisableAttachMechanism) { 313 AttachListener::vm_start(); 314 if (StartAttachListener || AttachListener::init_at_startup()) { 315 AttachListener::init(); 316 } 317 } 318 319 // Launch -Xrun agents 320 // Must be done in the JVMTI live phase so that for backward compatibility the JDWP 321 // back-end can launch with -Xdebug -Xrunjdwp. 322 if (!EagerXrunInit && Arguments::init_libraries_at_startup()) { 323 create_vm_init_libraries(); 324 } 325 326 // Notify JVMTI agents that VM initialization is complete - nop if no agents. 327 JvmtiExport::post_vm_initialized(); 328 329 if (TRACE_START() != JNI_OK) { 330 vm_exit_during_initialization("Failed to start tracing backend."); 331 } 332 333 if (CleanChunkPoolAsync) { 334 Chunk::start_chunk_pool_cleaner_task(); 335 } 336 337 // initialize compiler(s) 338 #if defined(COMPILER1) || defined(COMPILER2) || defined(SHARK) 339 CompileBroker::compilation_init(); 340 #endif 341 342 if (EnableInvokeDynamic) { 343 // Pre-initialize some JSR292 core classes to avoid deadlock during class loading. 344 // It is done after compilers are initialized, because otherwise compilations of 345 // signature polymorphic MH intrinsics can be missed 346 // (see SystemDictionary::find_method_handle_intrinsic). 347 initialize_class(vmSymbols::java_lang_invoke_MethodHandle(), CHECK_0); 348 initialize_class(vmSymbols::java_lang_invoke_MemberName(), CHECK_0); 349 initialize_class(vmSymbols::java_lang_invoke_MethodHandleNatives(), CHECK_0); 350 } 351 352 #if INCLUDE_MANAGEMENT 353 Management::initialize(THREAD); 354 #endif // INCLUDE_MANAGEMENT 355 356 if (HAS_PENDING_EXCEPTION) { 357 // management agent fails to start possibly due to 358 // configuration problem and is responsible for printing 359 // stack trace if appropriate. Simply exit VM. 360 vm_exit(1); 361 } 362 363 if (Arguments::has_profile()) FlatProfiler::engage(main_thread, true); 364 if (MemProfiling) MemProfiler::engage(); 365 StatSampler::engage(); 366 if (CheckJNICalls) JniPeriodicChecker::engage(); 367 368 BiasedLocking::init(); 369 370 if (JDK_Version::current().post_vm_init_hook_enabled()) { 371 call_postVMInitHook(THREAD); 372 // The Java side of PostVMInitHook.run must deal with all 373 // exceptions and provide means of diagnosis. 374 if (HAS_PENDING_EXCEPTION) { 375 CLEAR_PENDING_EXCEPTION; 376 } 377 } 378 379 { 380 MutexLockerEx ml(PeriodicTask_lock, Mutex::_no_safepoint_check_flag); 381 // Make sure the watcher thread can be started by WatcherThread::start() 382 // or by dynamic enrollment. 383 WatcherThread::make_startable(); 384 // Start up the WatcherThread if there are any periodic tasks 385 // NOTE: All PeriodicTasks should be registered by now. If they 386 // aren't, late joiners might appear to start slowly (we might 387 // take a while to process their first tick). 388 if (PeriodicTask::num_tasks() > 0) { 389 WatcherThread::start(); 390 } 391 } 392 393 // Give os specific code one last chance to start 394 os::init_3(); 395 396 create_vm_timer.end(); 397 #ifdef ASSERT 398 _vm_complete = true; 399 #endif 400 return JNI_OK; 401 }
309 行處,看到了os::signal_init() 的調用(hotspot/src/share/vm/runtime/os.cpp),這就是我們要找的。接着,我們看下其具體實現
1 void os::signal_init() { 2 if (!ReduceSignalUsage) { 3 // Setup JavaThread for processing signals 4 EXCEPTION_MARK; 5 Klass* k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK); 6 instanceKlassHandle klass (THREAD, k); 7 instanceHandle thread_oop = klass->allocate_instance_handle(CHECK); 8 9 const char thread_name[] = "Signal Dispatcher"; 10 Handle string = java_lang_String::create_from_str(thread_name, CHECK); 11 12 // Initialize thread_oop to put it into the system threadGroup 13 Handle thread_group (THREAD, Universe::system_thread_group()); 14 JavaValue result(T_VOID); 15 JavaCalls::call_special(&result, thread_oop, 16 klass, 17 vmSymbols::object_initializer_name(), 18 vmSymbols::threadgroup_string_void_signature(), 19 thread_group, 20 string, 21 CHECK); 22 23 KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass()); 24 JavaCalls::call_special(&result, 25 thread_group, 26 group, 27 vmSymbols::add_method_name(), 28 vmSymbols::thread_void_signature(), 29 thread_oop, // ARG 1 30 CHECK); 31 32 os::signal_init_pd(); 33 34 { MutexLocker mu(Threads_lock); 35 JavaThread* signal_thread = new JavaThread(&signal_thread_entry); 36 37 // At this point it may be possible that no osthread was created for the 38 // JavaThread due to lack of memory. We would have to throw an exception 39 // in that case. However, since this must work and we do not allow 40 // exceptions anyway, check and abort if this fails. 41 if (signal_thread == NULL || signal_thread->osthread() == NULL) { 42 vm_exit_during_initialization("java.lang.OutOfMemoryError", 43 "unable to create new native thread"); 44 } 45 46 java_lang_Thread::set_thread(thread_oop(), signal_thread); 47 java_lang_Thread::set_priority(thread_oop(), NearMaxPriority); 48 java_lang_Thread::set_daemon(thread_oop()); 49 50 signal_thread->set_threadObj(thread_oop()); 51 Threads::add(signal_thread); 52 Thread::start(signal_thread); 53 } 54 // Handle ^BREAK 55 os::signal(SIGBREAK, os::user_handler()); 56 } 57 }
這里的完全可以看出來,在此函數中35行處,創建了一個 java 線程,用於執行signal_thread_entry 函數,那我們來看看,這個 signal_thread_entry 函數到底做了什么?
1 // sigexitnum_pd is a platform-specific special signal used for terminating the Signal thread. 2 static void signal_thread_entry(JavaThread* thread, TRAPS) { 3 os::set_priority(thread, NearMaxPriority); 4 while (true) { 5 int sig; 6 { 7 // FIXME : Currently we have not decieded what should be the status 8 // for this java thread blocked here. Once we decide about 9 // that we should fix this. 10 sig = os::signal_wait(); 11 } 12 if (sig == os::sigexitnum_pd()) { 13 // Terminate the signal thread 14 return; 15 } 16 17 switch (sig) { 18 case SIGBREAK: { 19 // Check if the signal is a trigger to start the Attach Listener - in that 20 // case don't print stack traces. 21 if (!DisableAttachMechanism && AttachListener::is_init_trigger()) { 22 continue; 23 } 24 // Print stack traces 25 // Any SIGBREAK operations added here should make sure to flush 26 // the output stream (e.g. tty->flush()) after output. See 4803766. 27 // Each module also prints an extra carriage return after its output. 28 VM_PrintThreads op; 29 VMThread::execute(&op); 30 VM_PrintJNI jni_op; 31 VMThread::execute(&jni_op); 32 VM_FindDeadlocks op1(tty); 33 VMThread::execute(&op1); 34 Universe::print_heap_at_SIGBREAK(); 35 if (PrintClassHistogram) { 36 VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */); 37 VMThread::execute(&op1); 38 } 39 if (JvmtiExport::should_post_data_dump()) { 40 JvmtiExport::post_data_dump(); 41 } 42 break; 43 } 44 default: { 45 // Dispatch the signal to java 46 HandleMark hm(THREAD); 47 Klass* k = SystemDictionary::resolve_or_null(vmSymbols::sun_misc_Signal(), THREAD); 48 KlassHandle klass (THREAD, k); 49 if (klass.not_null()) { 50 JavaValue result(T_VOID); 51 JavaCallArguments args; 52 args.push_int(sig); 53 JavaCalls::call_static( 54 &result, 55 klass, 56 vmSymbols::dispatch_name(), 57 vmSymbols::int_void_signature(), 58 &args, 59 THREAD 60 ); 61 } 62 if (HAS_PENDING_EXCEPTION) { 63 // tty is initialized early so we don't expect it to be null, but 64 // if it is we can't risk doing an initialization that might 65 // trigger additional out-of-memory conditions 66 if (tty != NULL) { 67 char klass_name[256]; 68 char tmp_sig_name[16]; 69 const char* sig_name = "UNKNOWN"; 70 InstanceKlass::cast(PENDING_EXCEPTION->klass())-> 71 name()->as_klass_external_name(klass_name, 256); 72 if (os::exception_name(sig, tmp_sig_name, 16) != NULL) 73 sig_name = tmp_sig_name; 74 warning("Exception %s occurred dispatching signal %s to handler" 75 "- the VM may need to be forcibly terminated", 76 klass_name, sig_name ); 77 } 78 CLEAR_PENDING_EXCEPTION; 79 } 80 } 81 } 82 } 83 }
函數里面意思已經很清晰明了了,首先在10行處,有一個os::signal_wait()的調用,該調用的主要是阻塞當前線程,並等待接收系統信號,然后再根據接收到的信號 sig 做 switch 邏輯,對於不同的信號做不同的處理。至此,關於“目標 JVM 對OS信號監聽的實現”這一點,就已經分析結束了。簡單的一句話總結就是,JVM 在啟動的時候,會創建一個名為“Signal Dispatcher”的線程用於接收os 的信號,以便對不同信號分別做處理。
3.2、文件 Socket 通信的通道的創建
經過3.1的分析,我們已經知道在 JVM 啟動之后,內部會有線程監聽並處理 os 的信號,那么,這個時候,如果我們想和已經啟動的 JVM 建立通信,當然就可以毫不猶豫的使用信號來進行了。不過,基於信號的通信,也是存在限制的,一方面,os 支持的信號是有限的,二來信號的通信往往是單向的,不方便通信雙方進行高效的通信。基於這些,筆者認為,為了使得 Client JVM 和 Target JVM 更好的通信,就采用了 Socket 通信來實現二者的通信。那接下來我們看看,這個通道究竟是如何創建的?
當我們需要 attach 到某個目標 JVM 進程上去的時候,我們通常會寫如下代碼
1 VirtualMachine vm = VirtualMachine.attach(pid);
這樣我們就能得到目標 JVM 的相關信息了,是不是很簡單?不過,今天我們要做的可不是這么簡單的事情,我們需要深入其后,了解其根本。接下來我們就以com.sun.tools.attach.VirtualMachine的 attach 方法入手,逐層揭開其神秘面紗。
1 public static VirtualMachine attach(String id) 2 throws AttachNotSupportedException, IOException 3 { 4 if (id == null) { 5 throw new NullPointerException("id cannot be null"); 6 } 7 List<AttachProvider> providers = AttachProvider.providers(); 8 if (providers.size() == 0) { 9 throw new AttachNotSupportedException("no providers installed"); 10 } 11 AttachNotSupportedException lastExc = null; 12 for (AttachProvider provider: providers) { 13 try { 14 return provider.attachVirtualMachine(id); 15 } catch (AttachNotSupportedException x) { 16 lastExc = x; 17 } 18 } 19 throw lastExc; 20 }
這是attach的源碼,入參為目標 JVM 的進程 ID,其實現委派給了 AttachProvider 了,通過provider.attachVirtualMachine(id);來實現真正的 attach 操作。由於 AttachProvider 是個抽象類,所以這個方法的真正實現在子類中,在 Linux 環境下,我們看 sun.tools.attach.BsdAttachProvider.java 的實現。
1 public VirtualMachine attachVirtualMachine(String vmid) 2 throws AttachNotSupportedException, IOException 3 { 4 checkAttachPermission(); 5 6 // AttachNotSupportedException will be thrown if the target VM can be determined 7 // to be not attachable. 8 testAttachable(vmid); 9 10 return new BsdVirtualMachine(this, vmid); 11 }
這個方法非常簡單,就是 new 了一個 BsdVirtualMachine 對象,並且把目標進程 ID 帶過去了。看sun.tools.attach.BsdVirtualMachine.java 的構造函數
1 /** 2 * Attaches to the target VM 3 */ 4 BsdVirtualMachine(AttachProvider provider, String vmid) 5 throws AttachNotSupportedException, IOException 6 { 7 super(provider, vmid); 8 9 // This provider only understands pids 10 int pid; 11 try { 12 pid = Integer.parseInt(vmid); 13 } catch (NumberFormatException x) { 14 throw new AttachNotSupportedException("Invalid process identifier"); 15 } 16 17 // Find the socket file. If not found then we attempt to start the 18 // attach mechanism in the target VM by sending it a QUIT signal. 19 // Then we attempt to find the socket file again. 20 path = findSocketFile(pid); 21 if (path == null) { 22 File f = new File(tmpdir, ".attach_pid" + pid); 23 createAttachFile(f.getPath()); 24 try { 25 sendQuitTo(pid); 26 27 // give the target VM time to start the attach mechanism 28 int i = 0; 29 long delay = 200; 30 int retries = (int)(attachTimeout() / delay); 31 do { 32 try { 33 Thread.sleep(delay); 34 } catch (InterruptedException x) { } 35 path = findSocketFile(pid); 36 i++; 37 } while (i <= retries && path == null); 38 if (path == null) { 39 throw new AttachNotSupportedException( 40 "Unable to open socket file: target process not responding " + 41 "or HotSpot VM not loaded"); 42 } 43 } finally { 44 f.delete(); 45 } 46 } 47 48 // Check that the file owner/permission to avoid attaching to 49 // bogus process 50 checkPermissions(path); 51 52 // Check that we can connect to the process 53 // - this ensures we throw the permission denied error now rather than 54 // later when we attempt to enqueue a command. 55 int s = socket(); 56 try { 57 connect(s, path); 58 } finally { 59 close(s); 60 } 61 }
首先看20行處的findSocketFile(pid);這里是找對應的 socket (/tmp/.java_pid${pid})文件,這個文件就是我們在第二大點圖中畫出來的,用於進程間通信的 socket 文件,如果不存在,即第一次進入該方法的時候。這時會運行到74行的createAttachFile(f.getPath());來創建一個attach 文件,socket 文件的命名方式為:/tmp/../.attach_pid${pid},關於這兩個方法(findSocketFile和createAttachFile)的具體實現,這里就不展開了,感興趣的可以直接去查看jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c的相關源碼。然后就會運行到一個非常關鍵的方法25行的sendQuitTo(pid);這個方法的實現,我們等會進入BsdVirtualMachine.c看下源碼,其主要目的就是給該進程發送一個信號。之后會進入到31行處的 do...while循環,自旋反復輪詢指定的次數來獲取該 socket 文件的路徑,直到超時或者 path(即 socket 文件路徑) 不為空,最后在55行處,建立一個 socket,並且在57行處通過 path 進行 socket 的連接,從而完成了客戶端(Client JVM)到目標進程(Target JVM)的 socket 通道建立。不過,請打住,這里是不是少了點什么?我相信細心的你肯定發現了,至少還存2個問題,
1. Target JVM 的 socket 服務端是何時創建的?
2. 用於通信的 socket 文件是在哪里創建的?
帶着這兩個問題,我們進入25行關鍵方法sendQuitTo(pid);的源碼解讀,該方法是個本地方法,位於jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c中
1 /* 2 * Class: sun_tools_attach_BsdVirtualMachine 3 * Method: sendQuitTo 4 * Signature: (I)V 5 */ 6 JNIEXPORT void JNICALL Java_sun_tools_attach_BsdVirtualMachine_sendQuitTo 7 (JNIEnv *env, jclass cls, jint pid) 8 { 9 if (kill((pid_t)pid, SIGQUIT)) { 10 JNU_ThrowIOExceptionWithLastError(env, "kill"); 11 } 12 }
看到第9行的時候,是不是覺得這里必然和前面3.1中大篇幅分析的信號處理線程“Signal Dispatcher”有種必然聯系了?沒錯,這里就是通過 kill 這個系統調用像目標 JVM,發送了一個 SIGQUIT 的信號,該信號是個#define,即宏,表示的數字“3”,即類似在 linux 命令行執行了“kill -3 ${pid}”的操做(其實,這個命令正是獲取目標 JVM 線程 dump 文件的一種方式,讀者可以試試)。既然這里向目標 JVM 發送了這么個信號,那么我們現在就移步到3.1中講到過的 signal_thread_entry 方法中去。
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 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 && 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->flush()) after output. See 4803766. 26 // Each module also prints an extra carriage return after its output. 27 VM_PrintThreads op; 28 VMThread::execute(&op); 29 VM_PrintJNI jni_op; 30 VMThread::execute(&jni_op); 31 VM_FindDeadlocks op1(tty); 32 VMThread::execute(&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 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 Klass* k = SystemDictionary::resolve_or_null(vmSymbols::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 vmSymbols::dispatch_name(), 56 vmSymbols::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 }
這里的17行,我們看到了有個對 SIGBREAK(宏定義) 信號處理的 case,事實上,這個SIGBREAK和前面客戶端發過來的SIGQUIT 的值是一樣的,都是“3”,熟悉 C語言的讀者應該不難理解。所以,當客戶端發送這個信號給目標 JVM 時,就理所應當的進入了這個 case 的處理邏輯。這里的27行到40行,事實上就是對“kill -3 ${pid}”執行時對應的處理邏輯“進行目標 JVM 進程的線程 dump 操作”。現在我們重點關注一下20行的 if 語句,第一個 boolean 值,某認情況下是false(可通過/hotspot/src/share/vm/runtime/globals.c)查看,表示某認情況下是不禁止attach 機制的,於是就會進入第二個條件的判斷AttachListener::is_init_trigger(),這里的判斷還是比較有意思的(即判斷當前殺是不是需要進行 attach 的初始化操作),我們進入源碼,源碼的文件為:hotspot/src/os/bsd/vm/attachListener_bsd.cpp
1 // If the file .attach_pid<pid> exists in the working directory 2 // or /tmp then this is the trigger to start the attach mechanism 3 bool AttachListener::is_init_trigger() { 4 if (init_at_startup() || is_initialized()) { 5 return false; // initialized at startup or already initialized 6 } 7 char path[PATH_MAX + 1]; 8 int ret; 9 struct stat st; 10 11 snprintf(path, PATH_MAX + 1, "%s/.attach_pid%d", 12 os::get_temp_directory(), os::current_process_id()); 13 RESTARTABLE(::stat(path, &st), ret); 14 if (ret == 0) { 15 // simple check to avoid starting the attach mechanism when 16 // a bogus user creates the file 17 if (st.st_uid == geteuid()) { 18 init(); 19 return true; 20 } 21 } 22 return false; 23 }
方法進入的第一行,即判斷是不是在 JVM 啟動時就初始化或者之前已經初始化過,如果是,則直接返回,否則繼續當前方法。方法的第11行,是在處理/tmp/attach_pid${pid}路徑(這個文件就是 Client JVM 在attach 時創建的),並把 path 傳入13行定義的宏進行判斷,如果這個文件存在,且剛好是當前用戶的創建的 attach_pid 文件,則進入18行的 init() 方法,否則什么也不做,返回 false。接着我們進入 init 的源碼(hotspot/src/share/vm/services/attachListener.cpp)
1 // Starts the Attach Listener thread 2 void AttachListener::init() { 3 EXCEPTION_MARK; 4 Klass* k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK); 5 instanceKlassHandle klass (THREAD, k); 6 instanceHandle thread_oop = klass->allocate_instance_handle(CHECK); 7 8 const char thread_name[] = "Attach Listener"; 9 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(&result, thread_oop, 15 klass, 16 vmSymbols::object_initializer_name(), 17 vmSymbols::threadgroup_string_void_signature(), 18 thread_group, 19 string, 20 THREAD); 21 22 if (HAS_PENDING_EXCEPTION) { 23 tty->print_cr("Exception in VM (AttachListener::init) : "); 24 java_lang_Throwable::print(PENDING_EXCEPTION, tty); 25 tty->cr(); 26 27 CLEAR_PENDING_EXCEPTION; 28 29 return; 30 } 31 32 KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass()); 33 JavaCalls::call_special(&result, 34 thread_group, 35 group, 36 vmSymbols::add_method_name(), 37 vmSymbols::thread_void_signature(), 38 thread_oop, // ARG 1 39 THREAD); 40 41 if (HAS_PENDING_EXCEPTION) { 42 tty->print_cr("Exception in VM (AttachListener::init) : "); 43 java_lang_Throwable::print(PENDING_EXCEPTION, tty); 44 tty->cr(); 45 46 CLEAR_PENDING_EXCEPTION; 47 48 return; 49 } 50 51 { MutexLocker mu(Threads_lock); 52 JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry); 53 54 // Check that thread and osthread were created 55 if (listener_thread == NULL || listener_thread->osthread() == NULL) { 56 vm_exit_during_initialization("java.lang.OutOfMemoryError", 57 "unable to create new native thread"); 58 } 59 60 java_lang_Thread::set_thread(thread_oop(), listener_thread); 61 java_lang_Thread::set_daemon(thread_oop()); 62 63 listener_thread->set_threadObj(thread_oop()); 64 Threads::add(listener_thread); 65 Thread::start(listener_thread); 66 } 67 }
從源碼中,我們可以看出來,這里最主要的功能是,創建一個名為“Attach Listener”的 Java 線程,該線程啟動后會調用attach_listener_thread_entry這個方法(52行),來完成有關的任務處理。進入attach_listener_thread_entry方法
1 // The Attach Listener threads services a queue. It dequeues an operation 2 // from the queue, examines the operation name (command), and dispatches 3 // to the corresponding function to perform the operation. 4 5 static void attach_listener_thread_entry(JavaThread* thread, TRAPS) { 6 os::set_priority(thread, NearMaxPriority); 7 8 thread->record_stack_base_and_size(); 9 10 if (AttachListener::pd_init() != 0) { 11 return; 12 } 13 AttachListener::set_initialized(); 14 15 for (;;) { 16 AttachOperation* op = AttachListener::dequeue(); 17 if (op == NULL) { 18 return; // dequeue failed or shutdown 19 } 20 21 ResourceMark rm; 22 bufferedStream st; 23 jint res = JNI_OK; 24 25 // handle special detachall operation 26 if (strcmp(op->name(), AttachOperation::detachall_operation_name()) == 0) { 27 AttachListener::detachall(); 28 } else { 29 // find the function to dispatch too 30 AttachOperationFunctionInfo* info = NULL; 31 for (int i=0; funcs[i].name != NULL; i++) { 32 const char* name = funcs[i].name; 33 assert(strlen(name) <= AttachOperation::name_length_max, "operation <= name_length_max"); 34 if (strcmp(op->name(), name) == 0) { 35 info = &(funcs[i]); 36 break; 37 } 38 } 39 40 // check for platform dependent attach operation 41 if (info == NULL) { 42 info = AttachListener::pd_find_operation(op->name()); 43 } 44 45 if (info != NULL) { 46 // dispatch to the function that implements this operation 47 res = (info->func)(op, &st); 48 } else { 49 st.print("Operation %s not recognized!", op->name()); 50 res = JNI_ERR; 51 } 52 } 53 54 // operation complete - send result and output to client 55 op->complete(res, &st); 56 } 57 }
這里需要關注兩個方面的內容,
第一、第10行的AttachListener::pd_init();
第二、第15行開始的 for 循環里面的內容。
首先看AttachListener::pd_init()
1 int AttachListener::pd_init() { 2 JavaThread* thread = JavaThread::current(); 3 ThreadBlockInVM tbivm(thread); 4 5 thread->set_suspend_equivalent(); 6 // cleared by handle_special_suspend_equivalent_condition() or 7 // java_suspend_self() via check_and_wait_while_suspended() 8 9 int ret_code = BsdAttachListener::init(); 10 11 // were we externally suspended while we were waiting? 12 thread->check_and_wait_while_suspended(); 13 14 return ret_code; 15 }
以上的 pd_init() 方法是在hotspot/src/os/bsd/vm/attachListener_bsd.cpp中實現的,我們看第9行的代碼,調用了BsdAttachListener::init()一個這樣的方法,該方法的主要作用就是生產 socket 通信文件的。源碼如下
1 // Initialization - create a listener socket and bind it to a file 2 3 int BsdAttachListener::init() { 4 char path[UNIX_PATH_MAX]; // socket file 5 char initial_path[UNIX_PATH_MAX]; // socket file during setup 6 int listener; // listener socket (file descriptor) 7 8 // register function to cleanup 9 ::atexit(listener_cleanup); 10 11 int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d", 12 os::get_temp_directory(), os::current_process_id()); 13 if (n < (int)UNIX_PATH_MAX) { 14 n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path); 15 } 16 if (n >= (int)UNIX_PATH_MAX) { 17 return -1; 18 } 19 20 // create the listener socket 21 listener = ::socket(PF_UNIX, SOCK_STREAM, 0); 22 if (listener == -1) { 23 return -1; 24 } 25 26 // bind socket 27 struct sockaddr_un addr; 28 addr.sun_family = AF_UNIX; 29 strcpy(addr.sun_path, initial_path); 30 ::unlink(initial_path); 31 int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr)); 32 if (res == -1) { 33 ::close(listener); 34 return -1; 35 } 36 37 // put in listen mode, set permissions, and rename into place 38 res = ::listen(listener, 5); 39 if (res == 0) { 40 RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res); 41 if (res == 0) { 42 // make sure the file is owned by the effective user and effective group 43 // (this is the default on linux, but not on mac os) 44 RESTARTABLE(::chown(initial_path, geteuid(), getegid()), res); 45 if (res == 0) { 46 res = ::rename(initial_path, path); 47 } 48 } 49 } 50 if (res == -1) { 51 ::close(listener); 52 ::unlink(initial_path); 53 return -1; 54 } 55 set_path(path); 56 set_listener(listener); 57 58 return 0; 59 }
從方法的注釋,就能看出這個方法就是用來創建一個基於文件的 socket 的 listener 端,即服務端的。具體的創建過程,代碼寫的已經很清楚了,我做下簡單描述,11行處,構建 socket 通信文件的路徑(/tmp/.java_pid${pid}),21 行,創建一個 socket,其中關注 socket 函數的第一個參數,當為 PF_UNIX 時,表示創建文件 socket,詳情可以參考 linux 的 socket 函數說明,然后到29行,將 socket 文件 path 拷貝到 socket 通信地址中,即以此文件作為通信地址,然后在31行時,將 socket 和該 socket 文件地址做一個綁定,38行,表示對當前 socket 進行監聽(數字5表示監聽時可容納客戶端連接的隊列的大小),如果有Client JVM 的客戶端連接上來,並且發送了相關消息,該服務端就可以對其進行相應處理了。至此,進程間 socket 的通信的通道就建立了。
其次看下 for 循環做了什么?其實很簡單,16行,BsdAttachListener::dequeue() 從監聽器的隊列中拿到一個 Client JVM 的AttachOperation(當客戶端 attach 上 target JVM 之后,往目標 JVM 發送任意 socket 信息,都會被放置到這個隊列中,等待被處理),此處會被阻塞,直到收到請求,如下源碼的13行, socket 的 accept 函數處於等待狀態,等待來之客戶端 JVM 的相關請求,一旦獲取到請求,則將請求組裝好返回給調用者一個BadAttachOperation 對象。
1 // Dequeue an operation 2 // 3 // In the Bsd implementation there is only a single operation and clients 4 // cannot queue commands (except at the socket level). 5 // 6 BsdAttachOperation* BsdAttachListener::dequeue() { 7 for (;;) { 8 int s; 9 10 // wait for client to connect 11 struct sockaddr addr; 12 socklen_t len = sizeof(addr); 13 RESTARTABLE(::accept(listener(), &addr, &len), s); 14 if (s == -1) { 15 return NULL; // log a warning? 16 } 17 18 // get the credentials of the peer and check the effective uid/guid 19 // - check with jeff on this. 20 uid_t puid; 21 gid_t pgid; 22 if (::getpeereid(s, &puid, &pgid) != 0) { 23 ::close(s); 24 continue; 25 } 26 uid_t euid = geteuid(); 27 gid_t egid = getegid(); 28 29 if (puid != euid || pgid != egid) { 30 ::close(s); 31 continue; 32 } 33 34 // peer credential look okay so we read the request 35 BsdAttachOperation* op = read_request(s); 36 if (op == NULL) { 37 ::close(s); 38 continue; 39 } else { 40 return op; 41 } 42 } 43 }
所以,只要收到一個AttachOperation不為“detachall”的操縱請求就會進入到45行處進行處理,這里的目的就是為了拿到對應的操作AttachOperationFunctionInfo對象,如果不為空,則調用其func,來完成對客戶端的響應,如47行所示。AttachOperationFunctionInfo(/hotspot/src/share/vm/services/attachListener.cpp)的定義如下
1 // names must be of length <= AttachOperation::name_length_max 2 static AttachOperationFunctionInfo funcs[] = { 3 { "agentProperties", get_agent_properties }, 4 { "datadump", data_dump }, 5 { "dumpheap", dump_heap }, 6 { "load", JvmtiExport::load_agent_library }, 7 { "properties", get_system_properties }, 8 { "threaddump", thread_dump }, 9 { "inspectheap", heap_inspection }, 10 { "setflag", set_flag }, 11 { "printflag", print_flag }, 12 { "jcmd", jcmd }, 13 { NULL, NULL } 14 };
從這里,我們可以看到,threaddump、dumpheap 等我們常用的操縱。到此為止,水落石出,涉及到 attach 操縱的服務端的原理基本已經理清楚了。接下來我們以 jstack 為例,來看下客戶端 JVM 是不是確實是以我們上面分析出來的方式與服務端 JVM 進行通信,並獲取到它想要的內容的。
3.3 JVM 對 Attach 上來的進程的命令的響應,以 jstack -l 為例
我們首先進入 jstack 的源碼,源碼目錄為jdk/src/share/classes/sun/tools/jstack/JStack.java。進入 main 函數
1 public static void main(String[] args) throws Exception { 2 if (args.length == 0) { 3 usage(1); // no arguments 4 } 5 6 boolean useSA = false; 7 boolean mixed = false; 8 boolean locks = false; 9 10 // Parse the options (arguments starting with "-" ) 11 int optionCount = 0; 12 while (optionCount < args.length) { 13 String arg = args[optionCount]; 14 if (!arg.startsWith("-")) { 15 break; 16 } 17 if (arg.equals("-help") || arg.equals("-h")) { 18 usage(0); 19 } 20 else if (arg.equals("-F")) { 21 useSA = true; 22 } 23 else { 24 if (arg.equals("-m")) { 25 mixed = true; 26 } else { 27 if (arg.equals("-l")) { 28 locks = true; 29 } else { 30 usage(1); 31 } 32 } 33 } 34 optionCount++; 35 } 36 37 // mixed stack implies SA tool 38 if (mixed) { 39 useSA = true; 40 } 41 42 // Next we check the parameter count. If there are two parameters 43 // we assume core file and executable so we use SA. 44 int paramCount = args.length - optionCount; 45 if (paramCount == 0 || paramCount > 2) { 46 usage(1); 47 } 48 if (paramCount == 2) { 49 useSA = true; 50 } else { 51 // If we can't parse it as a pid then it must be debug server 52 if (!args[optionCount].matches("[0-9]+")) { 53 useSA = true; 54 } 55 } 56 57 // now execute using the SA JStack tool or the built-in thread dumper 58 if (useSA) { 59 // parameters (<pid> or <exe> <core> 60 String params[] = new String[paramCount]; 61 for (int i=optionCount; i<args.length; i++ ){ 62 params[i-optionCount] = args[i]; 63 } 64 runJStackTool(mixed, locks, params); 65 } else { 66 // pass -l to thread dump operation to get extra lock info 67 String pid = args[optionCount]; 68 String params[]; 69 if (locks) { 70 params = new String[] { "-l" }; 71 } else { 72 params = new String[0]; 73 } 74 runThreadDump(pid, params); 75 } 76 }
當采用 jstack -l 時,會走65行的 else 分支,最終執行77行的runThreadDump方法
1 // Attach to pid and perform a thread dump 2 private static void runThreadDump(String pid, String args[]) throws Exception { 3 VirtualMachine vm = null; 4 try { 5 vm = VirtualMachine.attach(pid); 6 } catch (Exception x) { 7 String msg = x.getMessage(); 8 if (msg != null) { 9 System.err.println(pid + ": " + msg); 10 } else { 11 x.printStackTrace(); 12 } 13 if ((x instanceof AttachNotSupportedException) && 14 (loadSAClass() != null)) { 15 System.err.println("The -F option can be used when the target " + 16 "process is not responding"); 17 } 18 System.exit(1); 19 } 20 21 // Cast to HotSpotVirtualMachine as this is implementation specific 22 // method. 23 InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args); 24 25 // read to EOF and just print output 26 byte b[] = new byte[256]; 27 int n; 28 do { 29 n = in.read(b); 30 if (n > 0) { 31 String s = new String(b, 0, n, "UTF-8"); 32 System.out.print(s); 33 } 34 } while (n > 0); 35 in.close(); 36 vm.detach(); 37 }
5行:執行VirtualMachine.attach(pid);則會達到3.1、3.2的效果,即服務端已經做好了所有 attach 所需的准備,如 socket 服務端、socket 通信文件、socket 請求處理線程“Attach Listener”。
23行:通過調用 HotSpotVirtualMachine 對象的 remoteDataDump 函數進行遠程 dump,獲得輸入流 InputStream in,最后通過讀取輸入流的內容,來通過標准輸出流輸出從服務端獲取的數據。至此,jstack -l 命令完成所有操作。
接下來,我們重點分析HotSpotVirtualMachine 對象的 remoteDataDump 函數。首先上HotSpotVirtualMachine(/jdk/src/share/classes/sun/tools/attach/HotSpotVirtualMachine.java) 對象的 remoteDataDump的源碼
1 // Remote ctrl-break. The output of the ctrl-break actions can 2 // be read from the input stream. 3 public InputStream remoteDataDump(Object ... args) throws IOException { 4 return executeCommand("threaddump", args); 5 }
請注意4行的 cmd 字符串為“threaddump”,這個和3.2中 AttachOperationFunctionInfo 的定義是吻合的,也就是說最終在服務端會調用 thread_dump 方法,來執行線程 dump,並將結果返回給客戶端。接着我們看下下 executeCommand方法,該方法只是簡單的調用 execute 方法,如下
1 /* 2 * Convenience method for simple commands 3 */ 4 private InputStream executeCommand(String cmd, Object ... args) throws IOException { 5 try { 6 return execute(cmd, args); 7 } catch (AgentLoadException x) { 8 throw new InternalError("Should not get here", x); 9 } 10 }
exectue 方法在該類中為抽象方法,其具體實現放在了sun.tools.attach.BsdVirtualMachine.java中,我們看下在其具體實現這里可是最最關鍵的地方了
1 /** 2 * Execute the given command in the target VM. 3 */ 4 InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException { 5 assert args.length <= 3; // includes null 6 7 // did we detach? 8 String p; 9 synchronized (this) { 10 if (this.path == null) { 11 throw new IOException("Detached from target VM"); 12 } 13 p = this.path; 14 } 15 16 // create UNIX socket 17 int s = socket(); 18 19 // connect to target VM 20 try { 21 connect(s, p); 22 } catch (IOException x) { 23 close(s); 24 throw x; 25 } 26 27 IOException ioe = null; 28 29 // connected - write request 30 // <ver> <cmd> <args...> 31 try { 32 writeString(s, PROTOCOL_VERSION); 33 writeString(s, cmd); 34 35 for (int i=0; i<3; i++) { 36 if (i < args.length && args[i] != null) { 37 writeString(s, (String)args[i]); 38 } else { 39 writeString(s, ""); 40 } 41 } 42 } catch (IOException x) { 43 ioe = x; 44 } 45 46 47 // Create an input stream to read reply 48 SocketInputStream sis = new SocketInputStream(s); 49 50 // Read the command completion status 51 int completionStatus; 52 try { 53 completionStatus = readInt(sis); 54 } catch (IOException x) { 55 sis.close(); 56 if (ioe != null) { 57 throw ioe; 58 } else { 59 throw x; 60 } 61 } 62 63 if (completionStatus != 0) { 64 sis.close(); 65 66 // In the event of a protocol mismatch then the target VM 67 // returns a known error so that we can throw a reasonable 68 // error. 69 if (completionStatus == ATTACH_ERROR_BADVERSION) { 70 throw new IOException("Protocol mismatch with target VM"); 71 } 72 73 // Special-case the "load" command so that the right exception is 74 // thrown. 75 if (cmd.equals("load")) { 76 throw new AgentLoadException("Failed to load agent library"); 77 } else { 78 throw new IOException("Command failed in target VM"); 79 } 80 } 81 82 // Return the input stream so that the command output can be read 83 return sis; 84 }
17行:創建一個 socket,這個是 socket 是一個 jni 本地方法,有興趣的可以去看對應的實現,源碼在jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c中,其關鍵操作就一個return socket(PF_UNIX, SOCK_STREAM, 0) 客戶端 socket 連接。
21行:這里也是一個本地方法,調用了 connect(s,p),這里的 p 就是 attach 時產生的/tmp/.java_pid${pid}的 socket 文件路徑,這樣,客戶端就和目標 JVM 連接上了,該方法同樣是一個 native 方法,可以通過查看BsdVirtualMachine.c的源碼來進行查看,如下,重點在16行使用 socket 文件路徑作為連接地址 和 18 行與目標 JVM 端啟動的 socket server 建立連接;
1 /* 2 * Class: sun_tools_attach_BsdVirtualMachine 3 * Method: connect 4 * Signature: (ILjava/lang/String;)I 5 */ 6 JNIEXPORT void JNICALL Java_sun_tools_attach_BsdVirtualMachine_connect 7 (JNIEnv *env, jclass cls, jint fd, jstring path) 8 { 9 jboolean isCopy; 10 const char* p = GetStringPlatformChars(env, path, &isCopy); 11 if (p != NULL) { 12 struct sockaddr_un addr; 13 int err = 0; 14 15 addr.sun_family = AF_UNIX; 16 strcpy(addr.sun_path, p); 17 18 if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { 19 err = errno; 20 } 21 22 if (isCopy) { 23 JNU_ReleaseStringPlatformChars(env, path, p); 24 } 25 26 /* 27 * If the connect failed then we throw the appropriate exception 28 * here (can't throw it before releasing the string as can't call 29 * JNI with pending exception) 30 */ 31 if (err != 0) { 32 if (err == ENOENT) { 33 JNU_ThrowByName(env, "java/io/FileNotFoundException", NULL); 34 } else { 35 char* msg = strdup(strerror(err)); 36 JNU_ThrowIOException(env, msg); 37 if (msg != NULL) { 38 free(msg); 39 } 40 } 41 } 42 }
31~44行:想 Target JVM 端發送命令 threaddump、以及可能存在的相關參數,如-l;這里的 writeString 同樣是一個本地方法,涉及到的底層操作就是一個 C 語言庫的 write 操作,感興趣的可以自己看源碼,不再贅述;
48~83行:這里就是對當前 socket 連接,構建一個 SocketInputStream 對象,並等待Target JVM 端數據完全返回,最后將這個 InputStream 對象作為方法返回參數返回。
四、總結
本文結合 Attach 的原理和使用案例(jstack -l),對 Attach 的各個方面都進行了深入的分析和總結,希望能對有需要的同學有所幫助。當然,以上均為本人個人所學,所以難免會有錯誤和疏忽的地方,如果您發現了,還麻煩指出。
本文參考連接:http://lovestblog.cn/blog/2014/06/18/jvm-attach/