JVMTI Attach機(jī)制與核心源碼分析(轉(zhuǎn))

0 前言

前面文章根蟹,我們已講述了《基于JVMTI的Agent實(shí)現(xiàn)》《基于Java Instrument的Agent實(shí)現(xiàn)》兩種Agent的實(shí)現(xiàn)方式,其中每種方式都會分為:啟動時Agent莲祸、運(yùn)行時Agent踩窖。

對于 啟動時Agent的觸發(fā)機(jī)制,在上一節(jié)《JVMTI Agent 工作原理及核心源碼分析》中泉瞻,已經(jīng)在源碼級進(jìn)行了分析系吭,具體如下:

加載Agent鏈接庫五嫂,觸發(fā)調(diào)用Agent_OnLoad方法

但是對于 運(yùn)行時Agent的觸發(fā)機(jī)制,卻沒有進(jìn)行詳細(xì)說明村斟,本節(jié)的主要目標(biāo)就是在源碼級分析下JVMTI Attach 工作機(jī)制。

1 Attach是什么

Attach機(jī)制是JVM提供一種JVM進(jìn)程間通信的能力抛猫,能讓一個進(jìn)程傳命令給另外一個進(jìn)程蟆盹,并讓它執(zhí)行內(nèi)部的一些操作

比如:為了讓另外一個JVM進(jìn)程把線程dump出來闺金,那么首先跑了一個jstack的進(jìn)程逾滥,然后傳了個pid的參數(shù),告訴它要哪個進(jìn)程進(jìn)行線程dump败匹,既然是兩個進(jìn)程寨昙,那肯定涉及到進(jìn)程間通信,以及傳輸協(xié)議的定義掀亩,比如:要執(zhí)行什么操作舔哪,傳了什么參數(shù)等。

有時當(dāng)我們感覺線程一直卡在某個地方槽棍,想知道卡在哪里捉蚤,首先想到的是進(jìn)行 線程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

在上面的Thread Dump日志中缆巧,出現(xiàn)了兩個線程:“Attach Listener” 和 “Signal Dispatcher”,這兩個線程便是Attach機(jī)制的關(guān)鍵豌拙。

那么JVM是如何啟動這兩個線程呢陕悬?JVM有很多線程主要在thread.cpp里的create_vm方法體里實(shí)現(xiàn)

JvmtiExport::enter_live_phase();  

// 1. Signal Dispatcher 需要在發(fā)布VMInit事件之前啟動  
os::signal_init();  

// 2. Start Attach Listener 如果配置 +StartAttachListener; 否則會延遲啟動  
if (!DisableAttachMechanism) {  
  if (StartAttachListener || AttachListener::init_at_startup()) {  
    AttachListener::init();  
  }  
}

其中JVM相關(guān)參數(shù):DisableAttachMechanism,StartAttachListener 按傅,ReduceSignalUsage 均默認(rèn)是 false

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”);  

如上面create_vm源碼所示捉超,在啟動的時候有可能不會創(chuàng)建AttachListener線程,那么 在上面Thread Stack日志中看到的AttachListener線程是怎么創(chuàng)建的呢唯绍,這個就要關(guān)注另外一個線程“Signal Dispatcher”了狂秦,顧名思義是處理信號的,這個線程是在JVM啟動的時候肯定會創(chuàng)建的推捐。

1.1 Signal Dispatcher 線程

在os.cpp中的 signal_init() 函數(shù)中裂问,啟動了signal dispatcher 線程,對signal dispather線程主要是用于處理信號,等待信號并且分發(fā)處理堪簿,可以詳細(xì)看 signal_thread_entry 的方法:

// 該方法用于Signal Dispatcher線程處理接受到的信號
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 thread  
       return;  
    }  

    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;  
      }  
      default: {  
        // Dispatch the signal to java  
        HandleMark hm(THREAD);  
        klassOop k = SystemDictionary::resolve_or_null(vmSymbolHandles::sun_misc_Signal(), THREAD);  
        KlassHandle klass (THREAD, k);  
        if (klass.not_null()) {  
          JavaValue result(T_VOID);  
          JavaCallArguments args;  
          args.push_int(sig);  
          JavaCalls::call_static(  
            &result,  
            klass,  
            vmSymbolHandles::dispatch_name(),  
            vmSymbolHandles::int_void_signature(),  
            &args,  
            THREAD  
          );  
        }  
        if (HAS_PENDING_EXCEPTION) {  
          // tty is initialized early so we don't expect it to be null, but  
          // if it is we can't risk doing an initialization that might  
          // trigger additional out-of-memory conditions  
          if (tty != NULL) {  
            char klass_name[256];  
            char tmp_sig_name[16];  
            const char* sig_name = "UNKNOWN";  
            instanceKlass::cast(PENDING_EXCEPTION->klass())->  
              name()->as_klass_external_name(klass_name, 256);  
            if (os::exception_name(sig, tmp_sig_name, 16) != NULL)  
              sig_name = tmp_sig_name;  
            warning("Exception %s occurred dispatching signal %s to handler"  
                    "- the VM may need to be forcibly terminated",  
                    klass_name, sig_name );  
          }  
          CLEAR_PENDING_EXCEPTION;  
        }  
      }  
    }  
  }  
}  

可以看到通過 os::signal_wait(); 等待信號痊乾,而在Linux里是通過 sem_wait() 來實(shí)現(xiàn),當(dāng)接受到信號是SIGBREAK(在JVM里做了#define椭更,其實(shí)就是SIGQUIT)的時候哪审,就會觸發(fā) AttachListener::is_init_trigger()的執(zhí)行初始化attach listener線程

  1. 第一次收到信號虑瀑,會開始初始化湿滓,當(dāng)初始化成功,將會直接返回舌狗,而且 不返回任何線程stack的信息(通過socket file的操作返回)叽奥,并且第二次將不在需要初始化。如果初始化不成功痛侍,將直接在控制臺的outputstream中打印線程棧信息朝氓;
  2. 第二次收到信號,如果已經(jīng)初始化過主届,將直接在控制臺中打印線程的棧信息赵哲。如果沒有初始化,繼續(xù)初始化君丁,走和第一次相同的流程枫夺;

比如:我們經(jīng)常會 使用 kill -3 pid的操作打印出線程棧信息,可以看到具體的實(shí)現(xiàn)是在Signal Dispatcher 線程中完成的绘闷,因?yàn)閗ill -3 pid 并不會創(chuàng)建.attach_pid#pid文件筷屡,所以一直初始化不成功,從而線程的棧信息被打印到控制臺中簸喂。

1.2 Attach Listener 線程

Attach Listener 線程是負(fù)責(zé)接收到外部的命令毙死,而對該命令進(jìn)行執(zhí)行的并且把結(jié)果返回給發(fā)送者。在JVM啟動的時候喻鳄,如果沒有指定 +StartAttachListener扼倘,該Attach Listener線程是不會啟動的。

在接受到 quit 信號之后除呵,會調(diào)用 AttachListener::is_init_trigger() 方法再菊, AttachListener::is_init_trigger() 內(nèi)會調(diào)用AttachListener::init() 啟動了Attach Listener 線程,在不同的操作系統(tǒng)下初始化實(shí)現(xiàn)是不同的颜曾,在linux中是在attachListener_Linux.cpp文件中實(shí)現(xiàn)的纠拔。

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 file
    if (st.st_uid == geteuid()) {
      // 創(chuàng)建AttachListener線程
      init();
      return true;
    }
  }
  return false;
}

一開始會 判斷當(dāng)前進(jìn)程目錄下是否有個.Attach_pid文件,如果沒有就會在/tmp下創(chuàng)建一個/tmp/.Attach_pid泛豪,當(dāng)那個文件的uid和自己的uid是一致的情況下(為了安全)再調(diào)用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 threadGroup
  Handle 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 1
                        CHECK);

  { MutexLocker mu(Threads_lock);
    JavaThread* listener_thread = new JavaThread(&Attach_listener_thread_entry);

    // Check that thread and osthread were created
    if (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);
  }
}

此時水落石出了侦鹏,看到創(chuàng)建了一個線程,并且取名為Attach Listener臀叙。再看看Linux系統(tǒng)下其子類LinuxAttachListener的init方法

int LinuxAttachListener::init() {
  char path[UNIX_PATH_MAX];          // socket file
  char initial_path[UNIX_PATH_MAX];  // socket file during setup
  int 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 socket
  listener = ::socket(PF_UNIX, SOCK_STREAM, 0);
  if (listener == -1) {
    return -1;
  }

  // bind socket
  struct 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 place
  res = ::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;
}

看到其創(chuàng)建了一個監(jiān)聽套接字略水,并創(chuàng)建了一個文件/tmp/.java_pid,這個文件就是客戶端之前一直在輪詢等待的文件劝萤,隨著這個文件的生成渊涝,意味著Attach的創(chuàng)建過程圓滿結(jié)束了。

Attach Listener線程接收到請求時床嫌,具體的請求處理在 attach_listener_thread_entry 方法體中實(shí)現(xiàn)

static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {  
  os::set_priority(thread, NearMaxPriority);  

  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 operation  
    if (strcmp(op->name(), AttachOperation::detachall_operation_name()) == 0) {  
      AttachListener::detachall();  
    } else {  
      // find the function to dispatch too  
      AttachOperationFunctionInfo* 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 operation  
      if (info == NULL) {  
        info = AttachListener::pd_find_operation(op->name());  
      }  

      if (info != NULL) {  
        // dispatch to the function that implements this operation  
        res = (info->func)(op, &st);  
      } else {  
        st.print("Operation %s not recognized!", op->name());  
        res = JNI_ERR;  
      }  
    }  

    // operation complete - send result and output to client  
    op->complete(res, &st);  
  }  
}  

從代碼來看就是 從隊(duì)列里不斷取AttachOperation跨释,然后找到請求命令對應(yīng)的方法進(jìn)行執(zhí)行,比如一開始說的jstack命令厌处,找到 { “threaddump”, thread_dump }的映射關(guān)系鳖谈,然后執(zhí)行thread_dump方法。

AttachOperation有很多種類嘱蛋,比如:內(nèi)存dump蚯姆,線程dump五续,類信息統(tǒng)計(jì)(比如加載的類及大小以及實(shí)例個數(shù)等)洒敏,動態(tài)加載agent,動態(tài)設(shè)置vm flag(但是并不是所有的flag都可以設(shè)置的疙驾,因?yàn)橛行ゝlag是在jvm啟動過程中使用的凶伙,是一次性的),打印vm flag它碎,獲取系統(tǒng)屬性等函荣,這些對應(yīng)的源碼(AttachListener.cpp)如下:

static AttachOperationFunctionInfo funcs[] = {
  // 第二個參數(shù)是命令對應(yīng)的處理函數(shù)
  { "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 }
};

再來看看其要調(diào)用的 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;
}

最終會調(diào)用的是 LinuxAttachListener::dequeue()

LinuxAttachOperation* LinuxAttachListener::dequeue() {
  for (;;) {
    int s;

    // wait for client to connect
    struct sockaddr addr;
    socklen_t len = sizeof(addr);
    // 如果沒有請求的話,會一直accept在那里
    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 request
    LinuxAttachOperation* op = read_request(s);
    if (op == NULL) {
      int res;
      RESTARTABLE(::close(s), res);
      continue;
    } else {
      return op;
    }
  }
}

如上代碼中可以看到扳肛,如果沒有請求的話傻挂,會一直accept在那里,當(dāng)來了請求挖息,然后就會創(chuàng)建一個套接字金拒,并讀取數(shù)據(jù),構(gòu)建出LinuxAttachOperation返回套腹,找到請求對應(yīng)的操作绪抛,調(diào)用操作得到結(jié)果并把結(jié)果寫到這個socket的文件,如果你把socket的文件刪除电禀,jstack/jmap會出現(xiàn)錯誤信息 unable to open socket file:........

1.3 jstack/jmap命令流程圖

以jstack的實(shí)現(xiàn)來說明觸發(fā)Attach這一機(jī)制進(jìn)行的過程幢码,jstack命令的實(shí)現(xiàn)其實(shí)是一個叫做JStack.java的類,jstack命令首先會attach到目標(biāo)JVM進(jìn)程尖飞,產(chǎn)生VirtualMachine類症副;Linux系統(tǒng)下店雅,其實(shí)現(xiàn)類為LinuxVirtualMachine,調(diào)用其remoteDataDump方法瓦糕,打印堆棧信息底洗;查看JStack.java代碼后會走到下面的方法里:

private static void runThreadDump(String pid, String args[]) throws Exception {
        VirtualMachine vm = null;
        try {
            // jstack命令首先會attach到目標(biāo)JVM進(jìn)程
            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 output
        byte 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是如何連接到目標(biāo)JVM進(jìn)程的呢?請注意 VirtualMachine.Attach(pid); 這行代碼咕娄,觸發(fā)Attach pid的關(guān)鍵亥揖,如果是在Linux下具體的實(shí)現(xiàn)邏輯在 sun.tools.attach.LinuxVirtualMachine 的構(gòu)造函數(shù):

LinuxVirtualMachine(AttachProvider provider, String vmid) throws AttachNotSupportedException, IOException
    {
        super(provider, vmid);

        // This provider only understands pids
        int 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 mechanism
                int 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 process
        checkPermissions(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);
        }
}

  1. 查找/tmp目錄下是否存在".java_pid"+pid文件;
  2. 如果文件不存在圣勒,則首先創(chuàng)建"/proc/" + pid + "/cwd/" + ".attach_pid" + pid文件费变;
  3. 通過kill命令發(fā)送SIGQUIT信號給目標(biāo)JVM進(jìn)程,由于JVM里除了信號線程圣贸,其他線程都設(shè)置了對此信號的屏蔽挚歧,因此收不到該信號,于是該信號就傳給了“Signal Dispatcher”吁峻;
  4. 目標(biāo)JVM進(jìn)程接收到信號之后滑负,會在/tmp目錄下創(chuàng)建".java_pid"+pid文件;
  5. 當(dāng)發(fā)現(xiàn)/tmp目錄下存在".java_pid"+pid文件用含,LinuxVirtualMachine會通過connect系統(tǒng)調(diào)用連接到該文件描述符矮慕,后續(xù)通過該fd進(jìn)行雙方的通訊;

JVM接受SIGQUIT信號的相關(guān)邏輯處理啄骇,則是在前面 signal_thread_entry 方法中進(jìn)行實(shí)現(xiàn)痴鳄。

jstack/jmap命令流程圖

前面JStack.java源碼中,輸出堆棧信息是通過調(diào)用remoteDataDump方法實(shí)現(xiàn)的缸夹,該方法就是通過往前面提到的fd中寫入threaddump指令痪寻,讀取返回結(jié)果,從而得到目標(biāo)JVM的堆棧信息虽惭。

2 Java 代碼實(shí)現(xiàn)動態(tài) attach Agent

Java動態(tài)attach Agent與上面所講到的JStack.java實(shí)現(xiàn)基本類似橡类,在 attach 的java代碼中,使用sun自用的tool.jar中的VirtualMachine的attach的方式:

VirtualMachine vm = VirtualMachine.attach(processid);  
vm.loadAgent(agentpath, args) 

HotSpotVirtualMachine.java中芽唇,loadAgent 方法源碼如下:

public void loadAgent(String agent, String options) throws AgentLoadException, AgentInitializationException, IOException  
{  
    String args = agent;  
    if (options != null) {  
        args = args + "=" + options;  
    }  
    try {  
        loadAgentLibrary("instrument", args);  
    } .....  
}

private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options) throws AgentLoadException, AgentInitializationException, IOException  
{  
    InputStream in = execute("load", agentLibrary, isAbsolute ? "true" : "false", options);  
    try {  
        int result = readInt(in);  
        if (result != 0) {  
            throw new AgentInitializationException("Agent_OnAttach failed", result);  
        }  
    } finally {  
        in.close();  
    }  
}  

LinuxVirtualMachine.java中的execute方法:

InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {  
    assert args.length <= 3;                // includes null  
    // did we detach?  
    String p;  
    synchronized (this) {  
        if (this.path == null) {  
            throw new IOException("Detached from target VM");  
        }  
        p = this.path;  
    }  
    // create UNIX socket  
    int s = socket();  
    // connect to target VM  
    try {  
        connect(s, p);  
    } catch (IOException x) {  
        close(s);  
        throw x;  
    }  

    IOException ioe = null;  

    // connected - write request  
    // <ver> <cmd> <args...>  
    try {  
        writeString(s, PROTOCOL_VERSION);  
        writeString(s, cmd);  
        for (int i=0; i<3; i++) {  
            if (i < args.length && args[i] != null) {  
                writeString(s, (String)args[i]);  
            } else {  
                writeString(s, "");  
            }  
        }  
    } catch (IOException x) {  
        ioe = x;  
    }  
    // Create an input stream to read reply  
    SocketInputStream sis = new SocketInputStream(s);  

    // Read the command completion status  
    int completionStatus;  
    try {  
        completionStatus = readInt(sis);  
    } catch (IOException x) {  
        sis.close();  
        if (ioe != null) {  
            throw ioe;  
        } else {  
            throw x;  
        }  
    }  
    ....  
}  

也就是向socket的中寫入了顾画,格式為:

<ver> <cmd> <args...> 

具體內(nèi)容為:

1 load instrument agentPath=path.jar

既然Load Agent 往socket里發(fā)了load指令,匹配到JVM的操作:

static AttachOperationFunctionInfo funcs[] = {  
  { "agentProperties",  get_agent_properties },  
  { "datadump",         data_dump },  
#ifndef SERVICES_KERNEL  
  { "dumpheap",         dump_heap },  
#endif  // SERVICES_KERNEL  
  { "load",             JvmtiExport::load_agent_library },  
  { "properties",       get_system_properties },  
  { "threaddump",       thread_dump },  
  { "inspectheap",      heap_inspection },  
  { "setflag",          set_flag },  
  { "printflag",        print_flag },  
  { NULL,               NULL }  
}; 

"load", JvmtiExport::load_agent_library披摄,具體源碼如下:

jint JvmtiExport::load_agent_library(AttachOperation* op, outputStream* st) {  
  char ebuf[1024];  
  char buffer[JVM_MAXPATHLEN];  
  void* library;  
  jint result = JNI_ERR;  
  const char* agent = op->arg(0);  
  const char* absParam = op->arg(1);  
  const char* options = op->arg(2);  
  bool is_absolute_path = (absParam != NULL) && (strcmp(absParam,"true")==0);  
  if (is_absolute_path) {  
    library = os::dll_load(agent, ebuf, sizeof ebuf);  
  } else {  
    // Try to load the agent from the standard dll directory  
    os::dll_build_name(buffer, sizeof(buffer), Arguments::get_dll_dir(), agent);  
    library = os::dll_load(buffer, ebuf, sizeof ebuf);  
    if (library == NULL) {  
      // not found - try local path  
      char ns[1] = {0};  
      os::dll_build_name(buffer, sizeof(buffer), ns, agent);  
      library = os::dll_load(buffer, ebuf, sizeof ebuf);  
    }  
  }  
  if (library != NULL) {  
    // Lookup the Agent_OnAttach function  
    OnAttachEntry_t on_attach_entry = NULL;  
    const char *on_attach_symbols[] = AGENT_ONATTACH_SYMBOLS;  
    for (uint symbol_index = 0; symbol_index < ARRAY_SIZE(on_attach_symbols); symbol_index++) {  

      on_attach_entry =  

        CAST_TO_FN_PTR(OnAttachEntry_t, os::dll_lookup(library, on_attach_symbols[symbol_index]));  

      if (on_attach_entry != NULL) break;  

    }  
    if (on_attach_entry == NULL) {  
      // Agent_OnAttach missing - unload library  
      os::dll_unload(library);  
    } else {  
      // Invoke the Agent_OnAttach function  
      JavaThread* THREAD = JavaThread::current();  
      {  
        extern struct JavaVM_ main_vm;  
        JvmtiThreadEventMark jem(THREAD);  
        JvmtiJavaThreadEventTransition jet(THREAD);  
        result = (*on_attach_entry)(&main_vm, (char*)options, NULL);  

      }  
      if (HAS_PENDING_EXCEPTION) {  
        CLEAR_PENDING_EXCEPTION;  
      }  
      if (result == JNI_OK) {  
        Arguments::add_loaded_agent(agent, (char*)options, is_absolute_path, library);  
      }  
      // Agent_OnAttach executed so completion status is JNI_OK  
      st->print_cr("%d", result);  
      result = JNI_OK;  
    }  
  }  
  return result;  
}  

#define AGENT_ONATTACH_SYMBOLS  {"Agent_OnAttach"} 

3 執(zhí)行 Instrument 的 Agent on attach

加載instrument的動態(tài)庫亲雪,并且調(diào)用方法instrument動態(tài)庫中的Agent_OnAttach方法:

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *args, void * reserved) {  
   .....  
    initerror = createNewJPLISAgent(vm, &agent);  
    if ( initerror == JPLIS_INIT_ERROR_NONE ) {  
        ......  
        if (parseArgumentTail(args, &jarfile, &options) != 0) {  
            return JNI_ENOMEM;  
        }  
        attributes = readAttributes( jarfile );  
        if (attributes == NULL) {  
            fprintf(stderr, "Error opening zip file or JAR manifest missing: %s\n", jarfile);  
            free(jarfile);  
            if (options != NULL) free(options);  
            return AGENT_ERROR_BADJAR;  
        }  
        agentClass = getAttribute(attributes, "Agent-Class");  
        if (agentClass == NULL) {  
            fprintf(stderr, "Failed to find Agent-Class manifest attribute from %s\n",  
                jarfile);  
            free(jarfile);  
            if (options != NULL) free(options);  
            freeAttributes(attributes);  
            return AGENT_ERROR_BADJAR;  
        }  
        if (appendClassPath(agent, jarfile)) {  
            fprintf(stderr, "Unable to add %s to system class path "  
                "- not supported by system class loader or configuration error!\n",  
                jarfile);  
            free(jarfile);  
            if (options != NULL) free(options);  
            freeAttributes(attributes);  
            return AGENT_ERROR_NOTONCP;  
        }  
        oldLen = strlen(agentClass);  
        newLen = modifiedUtf8LengthOfUtf8(agentClass, oldLen);  
        if (newLen == oldLen) {  
            agentClass = strdup(agentClass);  
        } else {  
            char* str = (char*)malloc( newLen+1 );  
            if (str != NULL) {  
                convertUtf8ToModifiedUtf8(agentClass, oldLen, str, newLen);  
            }  
            agentClass = str;  
        }  

        if (agentClass == NULL) {  
            free(jarfile);  
            if (options != NULL) free(options);  
            freeAttributes(attributes);  
            return JNI_ENOMEM;  
        }  
        bootClassPath = getAttribute(attributes, "Boot-Class-Path");  
        if (bootClassPath != NULL) {  
            appendBootClassPath(agent, jarfile, bootClassPath);  
        }  
        convertCapabilityAtrributes(attributes, agent);  
        success = createInstrumentationImpl(jni_env, agent);  
        jplis_assert(success);  
        /* 
         *  Turn on the ClassFileLoadHook. 
         */  
        if (success) {  
            success = setLivePhaseEventHandlers(agent);  
            jplis_assert(success);  
        }  
        if (success) {  
            success = startJavaAgent(agent,  
                                     jni_env,  
                                     agentClass,  
                                     options,  
                                     agent->mAgentmainCaller);  
        }  
        if (!success) {  
            fprintf(stderr, "Agent failed to start!\n");  
            result = AGENT_ERROR_STARTFAIL;  
        }  

        if (options != NULL) free(options);  
        free(agentClass);  
        freeAttributes(attributes);  
    }  
    return result;  
}  

上面代碼里一開始的createNewJPLISAgenton_load是一樣的注冊了一些鉤子函數(shù),具體詳情可參考:《JVMTI Agent 工作原理及核心源碼分析》疚膊。

在上面的Agent_OnAttach代碼中我們也看到了义辕,讀取加載的jar中MANIFEST Agent-Class的配置:

agentClass = getAttribute(attributes, "Agent-Class");

創(chuàng)建生成sun.instrument.InstrumentationImpl對象:

success = createInstrumentationImpl(jni_env, agent);

通過InstrumentationImpl對象中的loadClassAndCallAgentmain方法去初始化在Agent-Class中的類,并調(diào)用class里的agentmain的方法:

success = startJavaAgent(agent, jni_env, agentClass, options, agent->mAgentmainCaller);

也就是說定義的on_attach的class里需要有agentmain的方法實(shí)現(xiàn):

public class MyTransformer {  
    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, NotFoundException, CannotCompileException, IOException{  
        ....  
    }  
}

鏈接:http://www.reibang.com/p/7e005007d87f

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寓盗,一起剝皮案震驚了整個濱河市灌砖,隨后出現(xiàn)的幾起案子璧函,更是在濱河造成了極大的恐慌,老刑警劉巖基显,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蘸吓,死亡現(xiàn)場離奇詭異,居然都是意外死亡撩幽,警方通過查閱死者的電腦和手機(jī)库继,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窜醉,“玉大人宪萄,你說我怎么就攤上這事≌ザ瑁” “怎么了拜英?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長琅催。 經(jīng)常有香客問我居凶,道長,這世上最難降的妖魔是什么藤抡? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任侠碧,我火速辦了婚禮,結(jié)果婚禮上杰捂,老公的妹妹穿的比我還像新娘舆床。我一直安慰自己棋蚌,他們只是感情好嫁佳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谷暮,像睡著了一般蒿往。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上湿弦,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天瓤漏,我揣著相機(jī)與錄音,去河邊找鬼颊埃。 笑死蔬充,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的班利。 我是一名探鬼主播饥漫,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼罗标!你這毒婦竟也來了庸队?” 一聲冷哼從身側(cè)響起积蜻,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎彻消,沒想到半個月后竿拆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宾尚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年丙笋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片煌贴。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡不见,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出崔步,到底是詐尸還是另有隱情稳吮,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布井濒,位于F島的核電站灶似,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏瑞你。R本人自食惡果不足惜酪惭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望者甲。 院中可真熱鬧春感,春花似錦、人聲如沸虏缸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刽辙。三九已至窥岩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宰缤,已是汗流浹背颂翼。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留慨灭,地道東北人朦乏。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像氧骤,于是被迫代替她去往敵國和親呻疹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容