Attach是什么
在講這個(gè)之前,我們先來(lái)點(diǎn)大家都知道的東西,當(dāng)我們感覺(jué)線(xiàn)程一直卡在某個(gè)地方,想知道卡在哪里,首先想到的是進(jìn)行線(xiàn)程dump,而常用的命令是jstack ,我們就可以看到如下線(xiàn)程棧了
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
大家是否注意過(guò)上面圈起來(lái)的兩個(gè)線(xiàn)程,”Attach Listener”和“Signal Dispatcher”,這兩個(gè)線(xiàn)程是我們這次要講的Attach機(jī)制的關(guān)鍵,先偷偷告訴各位,其實(shí)Attach Listener這個(gè)線(xiàn)程在jvm起來(lái)的時(shí)候可能并沒(méi)有的,后面會(huì)細(xì)說(shuō)。
??那Attach機(jī)制是什么?說(shuō)簡(jiǎn)單點(diǎn)就是jvm提供一種jvm進(jìn)程間通信的能力,能讓一個(gè)進(jìn)程傳命令給另外一個(gè)進(jìn)程,并讓它執(zhí)行內(nèi)部的一些操作,比如說(shuō)我們?yōu)榱俗屃硗庖粋€(gè)jvm進(jìn)程把線(xiàn)程dump出來(lái),那么我們跑了一個(gè)jstack的進(jìn)程,然后傳了個(gè)pid的參數(shù),告訴它要哪個(gè)進(jìn)程進(jìn)行線(xiàn)程dump,既然是兩個(gè)進(jìn)程,那肯定涉及到進(jìn)程間通信,以及傳輸協(xié)議的定義,比如要執(zhí)行什么操作,傳了什么參數(shù)等
Attach能做些什么
??總結(jié)起來(lái)說(shuō),比如內(nèi)存dump,線(xiàn)程dump,類(lèi)信息統(tǒng)計(jì)(比如加載的類(lèi)及大小以及實(shí)例個(gè)數(shù)等),動(dòng)態(tài)加載agent(使用過(guò)btrace的應(yīng)該不陌生),動(dòng)態(tài)設(shè)置vm flag(但是并不是所有的flag都可以設(shè)置的,因?yàn)橛行ゝlag是在jvm啟動(dòng)過(guò)程中使用的,是一次性的),打印vm flag,獲取系統(tǒng)屬性等,這些對(duì)應(yīng)的源碼(AttachListener.cpp)如下
static AttachOperationFunctionInfo funcs[] = {
{ "agentProperties", get_agent_properties },
{ "datadump", data_dump },
{ "dumpheap", dump_heap },
{ "load", JvmtiExport::load_agent_library },
{ "properties", get_system_properties },
{ "threaddump", thread_dump },
{ "inspectheap", heap_inspection },
{ "setflag", set_flag },
{ "printflag", print_flag },
{ "jcmd", jcmd },
{ NULL, NULL }
};
后面是命令對(duì)應(yīng)的處理函數(shù)。
Attach在jvm里如何實(shí)現(xiàn)的
Attach Listener線(xiàn)程的創(chuàng)建
??前面也提到了,jvm在啟動(dòng)過(guò)程中可能并沒(méi)有啟動(dòng)Attach Listener這個(gè)線(xiàn)程,可以通過(guò)jvm參數(shù)來(lái)啟動(dòng),代碼 (Threads::create_vm)如下:
if (!DisableAttachMechanism) {
if (StartAttachListener || AttachListener::init_at_startup()) {
AttachListener::init();
}
}
bool AttachListener::init_at_startup() {
if (ReduceSignalUsage) {
return true;
} else {
return false;
}
}
``
??其中DisableAttachMechanism,StartAttachListener ,ReduceSignalUsage均默認(rèn)是false(globals.hpp)
product(bool, DisableAttachMechanism, false, \
"Disable mechanism that allows tools to Attach to this VM”)
product(bool, StartAttachListener, false, \
"Always start Attach Listener at VM startup")
product(bool, ReduceSignalUsage, false, \
"Reduce the use of OS signals in Java and/or the VM”)
??因此AttachListener::init()并不會(huì)被執(zhí)行,而Attach Listener線(xiàn)程正是在此方法里創(chuàng)建的
// 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);
}
}
既然在啟動(dòng)的時(shí)候不會(huì)創(chuàng)建這個(gè)線(xiàn)程,那么我們?cè)谏厦婵吹降哪莻€(gè)線(xiàn)程是怎么創(chuàng)建的呢,這個(gè)就要關(guān)注另外一個(gè)線(xiàn)程“Signal Dispatcher”了,顧名思義是處理信號(hào)的,這個(gè)線(xiàn)程是在jvm啟動(dòng)的時(shí)候就會(huì)創(chuàng)建的,具體代碼就不說(shuō)了。
??下面以jstack的實(shí)現(xiàn)來(lái)說(shuō)明觸發(fā)Attach這一機(jī)制進(jìn)行的過(guò)程,jstack命令的實(shí)現(xiàn)其實(shí)是一個(gè)叫做JStack.java的類(lèi),查看jstack代碼后會(huì)走到下面的方法里
private static void runThreadDump(String pid, String args[]) throws Exception {
VirtualMachine vm = null;
try {
vm = VirtualMachine.Attach(pid);
} catch (Exception x) {
String msg = x.getMessage();
if (msg != null) {
System.err.println(pid + ": " + msg);
} else {
x.printStackTrace();
}
if ((x instanceof AttachNotSupportedException) &&
(loadSAClass() != null)) {
System.err.println("The -F option can be used when the target " +
"process is not responding");
}
System.exit(1);
}
// Cast to HotSpotVirtualMachine as this is implementation specific
// method.
InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args);
// read to EOF and just print 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();
}
請(qǐng)注意VirtualMachine.Attach(pid);這行代碼,觸發(fā)Attach pid的關(guān)鍵,如果是在linux下會(huì)走到下面的構(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);
}
}
這里要解釋下代碼了,首先看到調(diào)用了createAttachFile方法在目標(biāo)進(jìn)程的cwd目錄下創(chuàng)建了一個(gè)文件/proc//cwd/.Attach_pid,這個(gè)在后面的信號(hào)處理過(guò)程中會(huì)取出來(lái)做判斷(為了安全),另外我們知道在linux下線(xiàn)程是用進(jìn)程實(shí)現(xiàn)的,在jvm啟動(dòng)過(guò)程中會(huì)創(chuàng)建很多線(xiàn)程,比如我們上面的信號(hào)線(xiàn)程,也就是會(huì)看到很多的pid(應(yīng)該是LWP),那么如何找到這個(gè)信號(hào)處理線(xiàn)程呢,從上面實(shí)現(xiàn)來(lái)看是找到我們傳進(jìn)去的pid的父進(jìn)程,然后給它的所有子進(jìn)程都發(fā)送一個(gè)SIGQUIT信號(hào),而jvm里除了信號(hào)線(xiàn)程,其他線(xiàn)程都設(shè)置了對(duì)此信號(hào)的屏蔽,因此收不到該信號(hào),于是該信號(hào)就傳給了“Signal Dispatcher”,在傳完之后作輪詢(xún)等待看目標(biāo)進(jìn)程是否創(chuàng)建了某個(gè)文件,AttachTimeout默認(rèn)超時(shí)時(shí)間是5000ms,可通過(guò)設(shè)置系統(tǒng)變量sun.tools.Attach.AttachTimeout來(lái)指定,下面是Signal Dispatcher線(xiàn)程的entry實(shí)現(xiàn)
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;
}
….
}
}
}
}
當(dāng)信號(hào)是SIGBREAK(在jvm里做了#define,其實(shí)就是SIGQUIT)的時(shí)候,就會(huì)觸發(fā)
AttachListener::is_init_trigger()的執(zhí)行,
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()) {
init();
return true;
}
}
return false;
}
一開(kāi)始會(huì)判斷當(dāng)前進(jìn)程目錄下是否有個(gè).Attach_pid文件(前面提到了),如果沒(méi)有就會(huì)在/tmp下創(chuàng)建一個(gè)/tmp/.Attach_pid,當(dāng)那個(gè)文件的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);
}
}
此時(shí)水落石出了,看到創(chuàng)建了一個(gè)線(xiàn)程,并且取名為Attach Listener。再看看其子類(lèi)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)建了一個(gè)監(jiān)聽(tīng)套接字,并創(chuàng)建了一個(gè)文件/tmp/.java_pid,這個(gè)文件就是客戶(hù)端之前一直在輪詢(xún)等待的文件,隨著這個(gè)文件的生成,意味著Attach的過(guò)程圓滿(mǎn)結(jié)束了。
Attach listener接收請(qǐng)求
??看看它的entry實(shí)現(xiàn)Attach_listener_thread_entry
static void Attach_listener_thread_entry(JavaThread* thread, TRAPS) {
os::set_priority(thread, NearMaxPriority);
thread->record_stack_base_and_size();
if (AttachListener::pd_init() != 0) {
return;
}
AttachListener::set_initialized();
for (;;) {
AttachOperation* op = AttachListener::dequeue();
if (op == NULL) {
return; // dequeue failed or shutdown
}
ResourceMark rm;
bufferedStream st;
jint res = JNI_OK;
// handle special detachall 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);
}
}
從代碼來(lái)看就是從隊(duì)列里不斷取AttachOperation,然后找到請(qǐng)求命令對(duì)應(yīng)的方法進(jìn)行執(zhí)行,比如我們一開(kāi)始說(shuō)的jstack命令,找到 { “threaddump”, thread_dump }的映射關(guān)系,然后執(zhí)行thread_dump方法
??再來(lái)看看其要調(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);
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;
}
}
}
我們看到如果沒(méi)有請(qǐng)求的話(huà),會(huì)一直accept在那里,當(dāng)來(lái)了請(qǐng)求,然后就會(huì)創(chuàng)建一個(gè)套接字,并讀取數(shù)據(jù),構(gòu)建出LinuxAttachOperation返回并執(zhí)行。
??整個(gè)過(guò)程就這樣了,從Attach線(xiàn)程創(chuàng)建到接收請(qǐng)求,處理請(qǐng)求。
文章轉(zhuǎn)自:http://lovestblog.cn/blog/2014/06/18/jvm-attach/