源码解析Java Attach处理流程
前言
当Java程序运行时出现CPU负载高、内存占用大等异常情况时,通常需要使用JDK自带的工具jstack、jmap查看JVM的运行时数据,并进行分析。
什么是Java Attach
那么JVM自带的这些工具是如何获取到JVM的相关信息呢?
JVM提供了 Java Attach
功能,能够让客户端与目标JVM进行通讯从而获取JVM运行时的数据,甚至可以通过Java Attach
加载自定义的代理工具,实现AOP、运行时class热更新等功能。
如果我们通过jstack打印线程栈的时候会发现有这么2个线程:Signal Dispatcher
和Attach Listener
。
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=917.19s tid=0x00000164ff377000 nid=0x4ba0 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Attach Listener" #5 daemon prio=5 os_prio=2 cpu=0.00ms elapsed=917.19s tid=0x000001648f4d1800 nid=0x1fc0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Signal Dispatcher
用于处理操作系统信号(软中断信号),Attach Listener
线程用于JVM进程间的通信。
操作系统支持的信号可以通过
kill -l
查看。比如我们平时杀进程用kill -9
可以看到9对应的信号就是SIGKILL
。
其他的信号并不会杀掉JVM进程,而是通知到进程, 具体进程如何处理根据Signal Dispatcher
线程处理逻辑决定。
root@DESKTOP-45K54QO:~# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
线程初始化
在虚拟机初始完成后,Signal Dispatcher
和Attach Listener
线程会根据配置进行必要的初始化。
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
...
//记录虚拟机初始化完成时间
Management::record_vm_init_completed();
...
// 初始化Signal Dispatcher
os::signal_init();
// 当设置了StartAttachListener或者无法懒加载时启动Attach Listener
if (!DisableAttachMechanism) {
AttachListener::vm_start();
if (StartAttachListener || AttachListener::init_at_startup()) {
AttachListener::init();
}
}
...
// 通知所有的 JVMTI agents 虚拟机初始化完成
JvmtiExport::post_vm_initialized();
...
}
相关JVM参数
JVM相关参数如下,默认都是false
JVM参数 | 默认值 |
---|---|
DisableAttachMechanism | false |
StartAttachListener | false |
ReduceSignalUsage | false |
除了这三个参数以外,我们可以看到AttachListener::init_at_startup()
也是用于控制Attach Listener
是否初始化。
JDK设计的时候根据不同的操作系统设计了不同的初始化方式。
- linux支持操作系统信号通知
- 默认情况下,
ReduceSignalUsage
配置的是false
,初始化完Signal Dispatcher
线程就不需要立即初始化Attach Listener
线程。而是在收到操作系统通知的时候,去触发Attach Listener
线程初始化。 - 如果
ReduceSignalUsage
配置的是true,那JVM启动时就不会启动Signal Dispatcher
线程。也就无法接收并处理操作系统的信号通知。这时就需要在JVM启动的时候需要立即初始化Attach Listener
线程。
- 默认情况下,
bool AttachListener::init_at_startup() {
if (ReduceSignalUsage) {
return true;
} else {
return false;
}
}
- windows虽然也有操作系统的信号通知,不过信号通知类型并没有linux那么多,JDK也并没有实现windows下的操作系统信号处理逻辑,因此windows下在JVM启动时就需要直接初始化
Attach Listener
线程。
// always startup on Windows NT/2000/XP
bool AttachListener::init_at_startup() {
return os::win32::is_nt();
}
Signal Dispatcher 线程初始化
根据配置ReduceSignalUsage
配置决定是否启动Signal Dispatcher
线程。
void os::signal_init() {
if (!ReduceSignalUsage) {
...
JavaThread* signal_thread = new JavaThread(&signal_thread_entry);
}
}
Signal Dispatcher
线程启动后会通过os::signal_wait()
等待操作系统信号量。当收到操作系统信号量,且信号量为SIGBREAK
时会触发初始化Attach Listener
。
Attach Listener
线程只会初始化一次,如果已初始化过,不会重复初始化。
JavaThread* signal_thread = new JavaThread(&signal_thread_entry);
static void signal_thread_entry(JavaThread* thread, TRAPS) {
os::set_priority(thread, NearMaxPriority);
while (true) {
int sig;
{
sig = os::signal_wait();
}
...
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;
}
...
}
需要补充说明的是SIGBREAK
实际就是SIGQUIT
信号。
#define SIGBREAK SIGQUIT
Attach Listener 线程初始化
...
if (!DisableAttachMechanism) {
AttachListener::vm_start();
if (StartAttachListener || AttachListener::init_at_startup()) {
AttachListener::init();
}
}
根据DisableAttachMechanism
配置决定是否启动Attach Listener
线程;
void AttachListener::vm_start() {
char fn[UNIX_PATH_MAX];
struct stat64 st;
int ret;
int n = snprintf(fn, UNIX_PATH_MAX, "%s/.java_pid%d",
os::get_temp_directory(), os::current_process_id());
assert(n < (int)UNIX_PATH_MAX, "java_pid file name buffer overflow");
RESTARTABLE(::stat64(fn, &st), ret);
if (ret == 0) {
ret = ::unlink(fn);
if (ret == -1) {
debug_only(warning("failed to remove stale attach pid file at %s", fn));
}
}
}
首先会创建/tmp/.java_pid<pid>
文件,该文件用于与socket进行绑定,实现进程间通讯。
这种通讯方式被称为UNIX domain socket,只能用于本机的进程间通讯。
根据StartAttachListener
配置决定是否初始化Attach Listener
,在初始化时会启动Attach Listener
线程
前面说过,具体还是要看操作系统是否支持系统级别的信号通知,如果不支持还是会立即初始化。
AttachListener::init();
void AttachListener::init() {
...
JavaThread* listener_thread = new JavaThread(&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::pd_init()
初始化逻辑根据实际的操作系统决定。在linux上,最终的初始化工作是由LinuxAttachListener::init()
完成。
AttachListener::pd_init()
int AttachListener::pd_init() {
...
int ret_code = LinuxAttachListener::init();
...
}
int LinuxAttachListener::init() {
...
::atexit(listener_cleanup);
int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",
os::get_temp_directory(), os::current_process_id());
...
listener = ::socket(PF_UNIX, SOCK_STREAM, 0);
...
int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));
...
}
LinuxAttachListener::init()
主要做了2件事:
- 注册清理回调函数,在JVM退出的时候进行资源释放(主要是
/tmp/.java_pid<pid>
文件的清理)。 - 将socket绑定到
/tmp/.java_pid<pid>
用户进程间通讯。
Attach Listener线程启动的两种方式
现在我们基本上搞清楚了Signal Dispatcher
和Attach Listener
线程启动的情况了。我们再来总结一下。
默认情况下JVM启动的时候并不会立即启动Attach Listener
线程。在客户端发送SIGQUIT
信号时会启动Attach Listener
线程。
或者我们可以通过参数配置在JVM启动时直接启动Attach Listener
线程。
Attach Listener执行命令
前面我们已经了解了Attach Listener
启动时会在AttachListener::pd_init()
方法中创建socket并监听。接下来我们简单看下Attach Listener
是如何执行命令的。
static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
...
if (AttachListener::pd_init() != 0) {
return;
}
...
for (;;) {
//获取命令
AttachOperation* op = AttachListener::dequeue();
...
AttachOperationFunctionInfo* info = NULL;
for (int i=0; funcs[i].name != NULL; i++) {
const char* name = funcs[i].name;
...
if (strcmp(op->name(), name) == 0) {
//查找命令
info = &(funcs[i]);
break;
}
}
...
//执行命令
res = (info->func)(op, &st);
// operation complete - send result and output to client
op->complete(res, &st);
}
}
执行命令有3个主要步骤:
- 获取命令
获取命令AttachListener::dequeue()
就是通过AttachListener线程接收客户端的命令执行请求。
LinuxAttachOperation* LinuxAttachListener::dequeue() {
for (;;) {
...
//接收客户端连接
RESTARTABLE(::accept(listener(), &addr, &len), s);
...
//读取命令并转化为LinuxAttachOperation
LinuxAttachOperation* op = read_request(s);
...
return op;
}
}
- 通过命令名从funcs查找需要执行的命令函数,linux支持的命令如下:
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 }
};
这些命令实际就与JDK自带的异常排查工具相对应。相关命令和函数对应关系如下。
命令 | 函数名 |
---|---|
jstack -l | threaddump |
jmap -dump:file=XXX | dumpheap |
jmap -histo:live | inspectheap |
jcmd | jcmd |
jinfo -flag | setflag |
jinfo flag | printflag |
- 执行命令
Attach Listener
线程主要用于JVM之间的通讯,部分命令的实际操作最终还是有虚拟机线程完成。比如threaddump
函数,实际由vmthread完成命令的执行。
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;
}
LinuxVirtualMachine
搞清楚了Java Attach服务端的处理逻辑,接下来我们看下客户端是如何连接并执行命令的。
还是以linux环境下客户端的代码在jdk\src\solaris\classes\sun\tools\attach\LinuxVirtualMachine.java
。
其他操作系统客户端代码在
jdk\src\solaris\classes\sun\tools\attach\
下也能找到。
LinuxVirtualMachine(AttachProvider provider, String vmid)
throws AttachNotSupportedException, IOException
{
...
path = findSocketFile(pid);
if (path == null) {
File f = createAttachFile(pid);
...
if (isLinuxThreads) {
...
mpid = getLinuxThreadsManager(pid);
...
sendQuitToChildrenOf(mpid);
} else {
sendQuitTo(pid);
}
...
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();
}
}
...
int s = socket();
try {
connect(s, path);
} finally {
close(s);
}
}
处理流程如下:
- 查找
/tmp/.java_pid<pid>
文件。
- 若文件已存在,则表示JVM已经初始化了
Attach Listener
线程,则可以直接连接到JVM。 - 若文件不存在则表示JVM还没有启用
Attach Listener
线程。此时需要通过发送SIGQUIT
信号量给JVM激活Attach Listener
线程
- 创建
/proc/<pid>/cwd/.attach_pid<pid>
或/tmp/.attach_pid<pid>
,这个文件仅仅时用于attach机制的握手,服务端会检查该文件是否存在,用来确认是Attach机制是JVM启动触发的还是客户端触发的。 - 获取JVM的进程id
- linux操作系统会进程的组ID,通过组ID获取到所有线程并发送
SIGQUIT
信号,只有Signal Dispatcher
线程会处理SIGQUIT
信号。从而激活Attach Listener
线程。
linux是不区分进程和线程的,通过讲用户级线程映射到轻量级进程。组成一个用户级进程的多用户级线程被映射到共享同一个组ID的多个Linux内核级进程上。《操作系统精髓与设计原理》-4.6.2Linux线程
- 其他操作系统当前线程的进程id就是进程id
- JVM收到信号后会判断若未启动
Attach Listener
线程,就会启动Attach Listener
线程。
这是一种懒加载机制,只有在需要的时候才启动。
- 前面讲过。当JVM启动
Attach Listener
线程后,会创建tmp/java_pid<pid>
文件,客户端就通过该文件与服务端进行网络通讯。
默认情况下
attachTimeout()
为5秒,若JVM 5秒钟没有创建java_pid
文件就认为超时了。
那么LinuxVirtualMachine
是如何被执行的呢?我们以jstack
为例。
jstack
代码在jdk\src\share\classes\sun\tools\jstack\JStack.java
当我们通过命令行调用jstack打印线程栈时。若不是SA模式,则会调用到runThreadDump
SA(ServiceAbility)提供了虚拟机调试快照的功能,它内部提供了一些jstack,jmap的一些工具也可以获取到相关的JVM参数。但是如果调试的是运行程序,则会使调试的目标进程完全暂停。
public static void main(String[] args) throws Exception {
...
if (useSA) {
...
runJStackTool(mixed, locks, params);
} else {
...
runThreadDump(pid, params);
}
}
private static void runThreadDump(String pid, String args[]) throws Exception {
...
vm = VirtualMachine.attach(pid);
...
InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args);
...
//和attach相反
vm.detach();
}
这里做了3件事:
- 获取
VirtualMachine
,并attach到目标JVM
public static VirtualMachine attach(String id)
throws AttachNotSupportedException, IOException
{
...
List<AttachProvider> providers = AttachProvider.providers();
...
AttachNotSupportedException lastExc = null;
for (AttachProvider provider: providers) {
return provider.attachVirtualMachine(id);
}
}
在linux下,provider
使用的是LinuxAttachProvider
,创建的是LinuxVirtualMachine
对象。
public VirtualMachine attachVirtualMachine(VirtualMachineDescriptor vmd)
throws AttachNotSupportedException, IOException
{
...
return new LinuxVirtualMachine(this, vmd.id());
...
}
- 执行
remoteDataDump
,实际就是通过socket与目标JVM进行通讯并执行相关的命令。
public InputStream remoteDataDump(Object ... args) throws IOException {
return executeCommand("threaddump", args);
}
- 调用
detach
与目标虚拟机断开。实际每次执行命令会重新创建连接,执行完就会关闭连接。这里仅仅把path
置空而已,并没有做其他什么工作。
结语
本文对JVM之间使用过Java Attach
的交互流程进行了梳理。一开始也提到,Java Attach
并不只是在JVM之间获取运行时信息那么简单,load
命令让JVM在运行时也能被代理,通过ASM、等字节码修改技术,在运行时对类进行修改。