jmap,jstack,jconsole等一系列jdk所实现的小工具对学习JVM的内部原理和现实中的性能分析都很有用处.
这是我分析其实现原理中的笔记.
示例代码如下:
package com.hongl;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import sun.tools.attach.HotSpotVirtualMachine;
import java.io.InputStream;
public class TestThreadDump {
public static void dumpStream(InputStream is) throws Exception{
byte [] b=new byte[128];
int n;
do{
n=is.read(b);
if (n>0){
String s=new String(b,"UTF-8");
System.out.print(s);
}
}while (n>0);
}public static void main(String [] args){
HotSpotVirtualMachine vm=null;
if (args.length == 0 ){
System.out.println("Usage:TestThreadDump pid");
System.out.println("Choose following process:");
//jps
for (VirtualMachineDescriptor jp :VirtualMachine.list()){
System.out.println(jp.id()+":"+jp.displayName());
}
return;
}
try{
vm = (HotSpotVirtualMachine)VirtualMachine.attach(args[0]);
//jstack
InputStream ins=vm.remoteDataDump("-1");
dumpStream(ins);
//jmap -dump
ins = vm.dumpHeap("stack.dmp");
dumpStream(ins);
}
catch (Exception e){
System.err.println("attach failed");
}
finally{
try{
if (vm != null){
vm.detach();
}
}catch (Exception e){}
}
}
}
关键的Java类是com.sun.tools.attach.VirtualMachine,注意其位于JDK_HOME\lib\tools.jar中,编译时要额外加入.
VirtualMachine的静态方法list()会返回一个包含所有java进程的List,这就是jps的实现原理.
静态方法attach(pid)则返回一个和该Java进程连接的HotSpotVirtualMachine示例.
调用这个实例的remoteDataDump方法返回这个进程内的所有线程的calling stacks,即jstack.
调用dumpHeap方法返回jvm堆的文件镜像,即jmap.
还有setFlag,printFlag等,即jinfo -flags.
那么,再往下,这些方法又是怎么实现的呢?
居然是使用了远程代码注入的方法.下面是windows上的实现.
JNIEXPORT jlong JNICALL Java_sun_tools_attach_WindowsVirtualMachine_createPipe
(JNIEnv *env, jclass cls, jstring pipename)
{....
hPipe = CreateNamedPipe(
name, // pipe name
PIPE_ACCESS_INBOUND, // read access
PIPE_TYPE_BYTE | // byte mode
PIPE_READMODE_BYTE |
PIPE_WAIT, // blocking mode
1, // max. instances
128, // output buffer size
8192, // input buffer size
NMPWAIT_USE_DEFAULT_WAIT, // client time-out
NULL); // default security attribute....
}
JNIEXPORT void JNICALL Java_sun_tools_attach_WindowsVirtualMachine_enqueue
(JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jstring cmd,
jstring pipename, jobjectArray args)
{
...
strcpy(data.jvmLib, "jvm");
strcpy(data.func1, "JVM_EnqueueOperation");
strcpy(data.func2, "_JVM_EnqueueOperation@20");
...
pData = (DataBlock*) VirtualAllocEx( hProcess, 0, sizeof(DataBlock), MEM_COMMIT, PAGE_READWRITE );
...
WriteProcessMemory( hProcess, (LPVOID)pData, (LPCVOID)&data, (SIZE_T)sizeof(DataBlock), NULL );
...
pCode = (PDWORD) VirtualAllocEx( hProcess, 0, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
...
WriteProcessMemory( hProcess, (LPVOID)pCode, (LPCVOID)stubCode, (SIZE_T)stubLen, NULL );
hThread = CreateRemoteThread( hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE) pCode,
pData,
0,
NULL );
...
}/*
* Code copied to target process
*/
#pragma check_stack (off)
static DWORD WINAPI thread_func(DataBlock *pData)
{
HINSTANCE h;
EnqueueOperationFunc addr;h = pData->_GetModuleHandle(pData->jvmLib);
if (h == NULL) {
return ERR_OPEN_JVM_FAIL;
}addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func1));
if (addr == NULL) {
addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func2));
}
if (addr == NULL) {
return ERR_GET_ENQUEUE_FUNC_FAIL;
}/* "null" command - does nothing in the target VM */
if (pData->cmd[0] == ‘\0‘) {
return 0;
} else {
return (*addr)(pData->cmd, pData->arg[0], pData->arg[1], pData->arg[2], pData->pipename);
}
}/* This function marks the end of thread_func. */
static void thread_end (void) {
}
#pragma check_stack
大概流程如下:
首先创建一个命名管道,打开相应的java进程,设定自身对其完全权限,用VirtualAllocEx在对方进程地址空间中分配2个段,然后分别把参数和自身的函数thread_func二进制代码复制到这2个段,然后CreateRemoteThread实现代码注入.
那么注入的代码thread_func又是执行的什么呢?它通过GetProcAddress来获取对方java进程中jvm.dll的JVM_EnqueueOperation函数地址.
然后执行,JVM_EnqueueOperation和源进程通过刚才创建的命名管道通信,把执行的结果发送给源进程.
Linux稍微有些区别,操作进程和Java进程通过Unix Socket来通信,通过信号SIQUIT来同步.