源码解析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、等字节码修改技术,在运行时对类进行修改。
源码解析Java Attach处理流程的更多相关文章
- Mybatis 系列10-结合源码解析mybatis 的执行流程
[Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...
- 【Java实战】源码解析Java SPI(Service Provider Interface )机制原理
一.背景知识 在阅读开源框架源码时,发现许多框架都支持SPI(Service Provider Interface ),前面有篇文章JDBC对Driver的加载时应用了SPI,参考[Hibernate ...
- Android短彩信源码解析-短信发送流程(二)
转载请注明出处:http://blog.csdn.net/droyon/article/details/11699935 2,短彩信发送framework逻辑 短信在SmsSingleRecipien ...
- Android View体系(七)从源码解析View的measure流程
前言 在上一篇我们了解了Activity的构成后,开始了解一下View的工作流程,就是measure.layout和draw.measure用来测量View的宽高,layout用来确定View的位置, ...
- Flask源码解析:Flask应用执行流程及原理
WSGI WSGI:全称是Web Server Gateway Interface,WSGI不是服务器,python模块,框架,API或者任何软件,只是一种规范,描述服务器端如何与web应用程序通信的 ...
- Java源码解析——Java IO包
一.基础知识: 1. Java IO一般包含两个部分:1)java.io包中阻塞型IO:2)java.nio包中的非阻塞型IO,通常称为New IO.这里只考虑到java.io包中堵塞型IO: 2. ...
- Hbase flusher源码解析(flush全代码流程解析)
版权声明:本文为博主原创文章,遵循版权协议,转载请附上原文出处链接和本声明. 在介绍HBASE flush源码之前,我们先在逻辑上大体梳理一下,便于后续看代码.flush的整体流程分三个阶段 1.第一 ...
- jvm源码解析java对象头
认真学习过java的同学应该都知道,java对象由三个部分组成:对象头,实例数据,对齐填充,这三大部分扛起了java的大旗对象,实例数据其实就是我们对象中的数据,对齐填充是由于为了规则分配内存空间,j ...
- Android短彩信源码解析-短信发送流程(三)
3.短信pdu的压缩与封装 相关文章: ------------------------------------------------------------- 1.短信发送上层逻辑 2.短信发送f ...
随机推荐
- Python+Selenium学习笔记6 - 定位
1.8种针对单个元素的定位方法 find_element_by_id() find_element_by_name() find_element_by_class_name() find_elemen ...
- Tengine Framework基础
Tengine Framework基础 最受开发者喜爱的边缘AI计算框架 Tengine是OPEN AI LAB推出的自主知识产权的边缘AI计算框架,致力于解决AIoT产业链碎片化问题,加速AI产业化 ...
- ML Pipelines管道
ML Pipelines管道 In this section, we introduce the concept of ML Pipelines. ML Pipelines provide a uni ...
- 用Microsoft DirectX光线跟踪改善渲染质量
用Microsoft DirectX光线跟踪改善渲染质量 Implementing Stochastic Levels of Detail with Microsoft DirectX Raytrac ...
- SQL进阶总结(二)
2.第二个特性----以集合为单位进行操作 在我们以往面向过程语言不同,SQL是一门面向集合的一门语言.由于习惯了面向过程的思考方式,导致我们在使用SQL时往往也陷入之前的思维定式. 我们现在分别创建 ...
- fiddler概念及原理
一.什么是fiddler? fiddler是位于客户端与服务器端的HTTP代理,它能够记录客户端与服务器之间所有的HTTP请求,可以针对特定的HTTP请求,分析请求数据,设置断点,调试WEB应用,修改 ...
- WordPress安装篇(4):YUM方式安装LNMP并部署WordPress
YUM方式安装软件的优点就是简单.方便.快捷,本文介绍在Linux上如何使用YUM方式快速安装LNMP并部署WordPress.使用Linux CentOS 7.9 + Nginx 1.18 + My ...
- UF_UI 界面相关
Open C uc1600uc1601uc1603 uc1605uc1607uc1608uc1609uc1613 获取用户输入的字符串uc1615uc1616uc1617uc1618uc163 ...
- 【NX二次开发】NX对象类型及基本操作
说明:NX中的所有对象都是通过唯一的tag_t值进行标识的,这些对象大致可以分为部件对象.UF对象.表达式.链表对象和属性对象等. 部件对象的操作: 基本操作函数: 1. UF_PART_new() ...
- 带你从头到尾捋一遍MySQL索引结构
索性这次把数据库中最核心的也是最难搞懂的内容,也就是索引,分享给大家. 这篇博客我会谈谈对于索引结构我自己的看法,以及分享如何从零开始一层一层向上最终理解索引结构. 从一个简单的表开始 create ...