前言

当Java程序运行时出现CPU负载高、内存占用大等异常情况时,通常需要使用JDK自带的工具jstack、jmap查看JVM的运行时数据,并进行分析。

什么是Java Attach

那么JVM自带的这些工具是如何获取到JVM的相关信息呢?

JVM提供了 Java Attach 功能,能够让客户端与目标JVM进行通讯从而获取JVM运行时的数据,甚至可以通过Java Attach 加载自定义的代理工具,实现AOP、运行时class热更新等功能。

如果我们通过jstack打印线程栈的时候会发现有这么2个线程:Signal DispatcherAttach 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 DispatcherAttach 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 DispatcherAttach 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个主要步骤:

  1. 获取命令

    获取命令AttachListener::dequeue()就是通过AttachListener线程接收客户端的命令执行请求。
LinuxAttachOperation* LinuxAttachListener::dequeue() {
for (;;) {
...
//接收客户端连接
RESTARTABLE(::accept(listener(), &addr, &len), s);
...
//读取命令并转化为LinuxAttachOperation
LinuxAttachOperation* op = read_request(s);
...
return op;
}
}
  1. 通过命令名从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
  1. 执行命令

    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);
}
}

处理流程如下:

  1. 查找/tmp/.java_pid<pid>文件。
  • 若文件已存在,则表示JVM已经初始化了Attach Listener线程,则可以直接连接到JVM。
  • 若文件不存在则表示JVM还没有启用Attach Listener线程。此时需要通过发送SIGQUIT信号量给JVM激活Attach Listener线程
  1. 创建/proc/<pid>/cwd/.attach_pid<pid>/tmp/.attach_pid<pid>,这个文件仅仅时用于attach机制的握手,服务端会检查该文件是否存在,用来确认是Attach机制是JVM启动触发的还是客户端触发的。
  2. 获取JVM的进程id
  • linux操作系统会进程的组ID,通过组ID获取到所有线程并发送SIGQUIT信号,只有Signal Dispatcher线程会处理SIGQUIT信号。从而激活Attach Listener线程。

linux是不区分进程和线程的,通过讲用户级线程映射到轻量级进程。组成一个用户级进程的多用户级线程被映射到共享同一个组ID的多个Linux内核级进程上。《操作系统精髓与设计原理》-4.6.2Linux线程

  • 其他操作系统当前线程的进程id就是进程id
  1. JVM收到信号后会判断若未启动Attach Listener线程,就会启动Attach Listener线程。

这是一种懒加载机制,只有在需要的时候才启动。

  1. 前面讲过。当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件事:

  1. 获取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());
...
}
  1. 执行remoteDataDump,实际就是通过socket与目标JVM进行通讯并执行相关的命令。
public InputStream remoteDataDump(Object ... args) throws IOException {
return executeCommand("threaddump", args);
}
  1. 调用detach与目标虚拟机断开。实际每次执行命令会重新创建连接,执行完就会关闭连接。这里仅仅把path置空而已,并没有做其他什么工作。

结语

本文对JVM之间使用过Java Attach的交互流程进行了梳理。一开始也提到,Java Attach并不只是在JVM之间获取运行时信息那么简单,load命令让JVM在运行时也能被代理,通过ASM、等字节码修改技术,在运行时对类进行修改。

源码解析Java Attach处理流程的更多相关文章

  1. Mybatis 系列10-结合源码解析mybatis 的执行流程

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  2. 【Java实战】源码解析Java SPI(Service Provider Interface )机制原理

    一.背景知识 在阅读开源框架源码时,发现许多框架都支持SPI(Service Provider Interface ),前面有篇文章JDBC对Driver的加载时应用了SPI,参考[Hibernate ...

  3. Android短彩信源码解析-短信发送流程(二)

    转载请注明出处:http://blog.csdn.net/droyon/article/details/11699935 2,短彩信发送framework逻辑 短信在SmsSingleRecipien ...

  4. Android View体系(七)从源码解析View的measure流程

    前言 在上一篇我们了解了Activity的构成后,开始了解一下View的工作流程,就是measure.layout和draw.measure用来测量View的宽高,layout用来确定View的位置, ...

  5. Flask源码解析:Flask应用执行流程及原理

    WSGI WSGI:全称是Web Server Gateway Interface,WSGI不是服务器,python模块,框架,API或者任何软件,只是一种规范,描述服务器端如何与web应用程序通信的 ...

  6. Java源码解析——Java IO包

    一.基础知识: 1. Java IO一般包含两个部分:1)java.io包中阻塞型IO:2)java.nio包中的非阻塞型IO,通常称为New IO.这里只考虑到java.io包中堵塞型IO: 2. ...

  7. Hbase flusher源码解析(flush全代码流程解析)

    版权声明:本文为博主原创文章,遵循版权协议,转载请附上原文出处链接和本声明. 在介绍HBASE flush源码之前,我们先在逻辑上大体梳理一下,便于后续看代码.flush的整体流程分三个阶段 1.第一 ...

  8. jvm源码解析java对象头

    认真学习过java的同学应该都知道,java对象由三个部分组成:对象头,实例数据,对齐填充,这三大部分扛起了java的大旗对象,实例数据其实就是我们对象中的数据,对齐填充是由于为了规则分配内存空间,j ...

  9. Android短彩信源码解析-短信发送流程(三)

    3.短信pdu的压缩与封装 相关文章: ------------------------------------------------------------- 1.短信发送上层逻辑 2.短信发送f ...

随机推荐

  1. Step By Step(Lua表达式和语句)

    Step By Step(Lua表达式和语句) 一.表达式:    1. 算术操作符:    Lua支持常规算术操作符有:二元的"+"."-"."*& ...

  2. 将TVM集成到PyTorch上

    将TVM集成到PyTorch上 随着TVM不断展示出对深度学习执行效率的改进,很明显PyTorch将从直接利用编译器堆栈中受益.PyTorch的主要宗旨是提供无缝且强大的集成,而这不会妨碍用户.为此, ...

  3. 【NX二次开发】属性操作相关函数的使用方法

    内容包括:1.属性创建2.判断属性是否存在3.读取属性值4.时间属性转换成字符串5.统计属性的数量6.删除指定属性7.删除全部属性效果: 源码: #include <stdlib.h> # ...

  4. 【NX二次开发】体消参,移除体参数UF_MODL_delete_body_parms()

    例子: 源码: extern DllExport void ufusr(char *param, int *returnCode, int rlen) { UF_initialize(); tag_t ...

  5. 「JVM」知识点详解一:JVM运行原理详解

    前言 JVM 一直都是面试的必考点,大家都知道,但是要把它搞清楚又好像不是特别容易.JVM 的知识点太散,不系统,今天带大家详细的了解一下jvm的运行原理. 正文 1 什么是JVM? JVM是Java ...

  6. 【题解】hdu2044一只小蜜蜂

    斐波拉契数列的应用 题目 有一只经过训练的蜜蜂只能爬向右侧相邻的蜂房,不能反向爬行.请编程计算蜜蜂从蜂房a爬到蜂房b的可能路线数.其中,蜂房的结构如下所示. Input输入数据的第一行是一个整数N,表 ...

  7. 项目启动报错:Redis health check failed

    最近是重新开发整个项目,在上线测试的时候发现这个问题. 项目环境:SpringBoot2.x+Consul+Redission+Maven 报错的信息如下: o.s.b.a.redis.RedisHe ...

  8. Java安全之挖掘回显链

    Java安全之挖掘回显链 0x00 前言 前文中叙述反序列化回显只是为了拿到Request和Response对象.在这里说的的回显链其实就是通过一连串反射代码获取到该Request对象. 在此之前想吹 ...

  9. CloudCanal

    CloudCanal 是一款数据迁移同步工具,帮助企业快速构建高质量数据流通通道,产品包含 SaaS 模式和私有输出专享模式.开发团队核心成员来自大厂,具备数据库内核.大规模分布式系统.云产品构建背景 ...

  10. 9.4、安装zabbix(2)

    8.从节点安装: (1)安装zabbix-agent: 1)下载zabbix-agent并安装: mkdir -p /tools/ cd /tools/ wget https://mirrors.tu ...