jstack命令或许是java开发人员在排查问题最常用的命令之一,它输出了当前时刻指定进程中java线程的堆栈信息。我们从jstack开始阅读,它的入口在sun.tools.jstack.JStack中。
在参数校验的逻辑之后,我们发现有两个入口 runJStackTool 和 runThreadDump ,这里涉及到两种实现。runJStackTool 是SA的jstack实现,由于它是在进程之外的审视工具,所以jstack在普通模式导不出数据时(hung)加上-F参数即可使用它来导出进程信息。它的实现本文不做解释(TODO)。在runThreadDump中我们看到
VirtualMachine.attach(pid)
这里是获取进程信息的关键,这里使用的就是Hotspot的attach,它提供了进程之间互相通信的机制,再这里可以描述成为jstack命令和目标jvm进程之间的通信连接。如何进行连接,先来看一份jstack结果中的两个线程:
"Attach Listener" daemon prio=10 tid=0x000000000755f800 nid=0xe74 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Signal Dispatcher" daemon prio=10 tid=0x0000000007563000 nid=0xa00 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
attach和这两个线程有何关系,请attach的linux实现LinuxVirtualMachine,在attach(pid)中,它做了下面几个事情:
1、查找pid文件。
static void SendQuitCallback(const pid_t pid, void* user_data) {
SendQuitContext* context = (SendQuitContext*)user_data;
pid_t parent = getParent(pid);
if (parent == context->ppid) {
kill(pid, SIGQUIT);
}
}
/*
* Class: sun_tools_attach_LinuxVirtualMachine
* Method: sendQuitToChildrenOf
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_sun_tools_attach_LinuxVirtualMachine_sendQuitToChildrenOf
(JNIEnv *env, jclass cls, jint pid)
{
SendQuitContext context;
context.ppid = (pid_t)pid;
/*
* Iterate over all children of 'pid' and send a QUIT signal to each.
*/
forEachProcess(SendQuitCallback, (void*)&context);
}
可以看到实际是向linux进程发起了kill(pid, SIGQUIT);信号。该信号在jvm中只有一个线程在监听:那就是上图中的“Signal Dispatcher”,可以从监听代码中得到一些信息:
即执行了AttachListener::is_init_trigger(),在AttachListener的初始化中我们看到了一行熟悉的名字:
const char thread_name[] = "Attach Listener";
没错,就是创建了一个新的线程即Attach Listener。该线程在init以后随即创建了一个文件sprintf(fn, ".attach_pid%d", os::current_process_id()); PID文件以及监听了该文件的socket端口,于此同时,attach(pid)做了第三件事:
3、循环等待直至发现由Attach Listener创建的pid文件。
4、检查文件权限,建立socket通道。
至此Attach和jstack“撩上了”。
接下来是执行了方法JStack#runThreadDump,可以看到向pid文件中发起了数据写入
this.writeString(s, "1");
this.writeString(s, "threaddump");
从attach方法表中对应的方法有如下定义:
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 }
};
对应的方法中使用VM_PrintThreads打印了线程信息,然后返回给jstack。值得注意的是,在导出线程信息时需要所有线程位于安全点(is_at_safepoint),此刻所有线程将阻塞,然后获取所有线程数据后释放线程锁,格式化输出。
// Implementation of "threaddump" command - essentially a remote ctrl-break
// See also: ThreadDumpDCmd class
//
static jint thread_dump(AttachOperation* op, outputStream* out) {
bool print_concurrent_locks = false;
if (op->arg(0) != NULL && strcmp(op->arg(0), "-l") == 0) {
print_concurrent_locks = true;
}
// thread stacks
VM_PrintThreads op1(out, print_concurrent_locks);
VMThread::execute(&op1);
// JNI global handles
VM_PrintJNI op2(out);
VMThread::execute(&op2);
// Deadlock detection
VM_FindDeadlocks op3(out);
VMThread::execute(&op3);
return JNI_OK;
}
至此,线程信息就输出到你的眼前了。
总结下来,由外部线程(jstack)发起握手命令(SIGQUIT),目标jvm监听该命令后启动attach机制,外部线程检查attch机制响应,从而建立链接会话,然后根据请求返回数据。