前言
在我们的前文分析中,我们了解到程序可以在运行中可以实现动态加载补丁程序,通过java agent的attach机制实现,例如阿里的 arthas,jdk的jmap、jstack、jstat等命令,这些操作如何实现,我们针对这一块进行总结一下;
java attach使用方式:
在JVM运行时加载一个Agent的jar包是 java agent 的一种更加灵活的实现方式,通常我们使用如下 API 将 Agent的 jar包 attach 到目标 JVM 上。
package com.sk.service;
import com.sun.tools.attach.VirtualMachine;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class Init {
@PostConstruct
public void run(){
System.out.println("--------attach----------------------1");
try {
Integer pid = 19952;
VirtualMachine virtualMachine = VirtualMachine.attach(String.valueOf(pid));
virtualMachine.loadAgent("E:\\work\\patchAgent\\target\\patchAgent-0.0.1-jar-with-dependencies.jar");
// virtualMachine.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
}
加载过程图:
Attach API
不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。
Attach API
很简单,只有 2 个主要的类
,都在 com.sun.tools.attach
包里面:
1)VirtualMachine
代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ;
VirtualMachine类,该类允许我们 通过给attach方法传入一个jvm的pid
(进程id),远程连接到jvm上 。然后我们可以 通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation
实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer
接口中提供的方法进行处理。
2)VirtualMachineDescriptor
则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
JVM attach简介:
JVM提供了 Java Attach 功能,能够让客户端与目标JVM进行通讯从而获取JVM运行时的数据,甚至可以通过Java Attach 加载自定义的代理工具,实现AOP、运行时class热更新等功能,说简单点就是jvm提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作,比如说我们为了让另外一个jvm进程把线程dump出来,那么我们跑了一个jstack的进程,然后传了个pid的参数,告诉它要哪个进程进行线程dump,既然是两个进程,那肯定涉及到进程间通信,以及传输协议的定义,比如要执行什么操作,传了什么参数等;
JVM attach运行机制:
由上图可知,attach运行机制有两个比较重要线程:Attach Listener
和Signal Dispatcher
Attach Listener:
Attach Listener线程: 在一些常用的工具里面 经常会和它有关, 比如 jstack, jinfo, jmap, jconsole 等等, 都可能会和 Attach Listener 进行交互的 ;
Attach Listener作用:
由上图可知,Attach Listener不断等待 消息过来, 然后进行处理 ,根据给定的 operation, 查询处理的 这个 operation 的函数, 调用这个函数, 将数据存放到 st 里面,然后将结果(res, st) 返回给调用方;
启动方式:
Attach Listener
的启动方式有多种, 一种就是 vm 启动的时候创建, 另外一种就是 我上图这种 由 Signal Dispatcher
触发 Attach Listener
在需要的时候初始化 ;
1)jvm在启动过程中可能并没有启动Attach Listener这个线程,可以通过jvm参数来启动,代码 (Threads::create_vm)如下:
if (!DisableAttachMechanism) {
if (StartAttachListener || AttachListener::init_at_startup()) {
AttachListener::init();
}
}
bool AttachListener::init_at_startup() {
if (ReduceSignalUsage) {
return true;
} else {
return false;
}
}
其中DisableAttachMechanism
,StartAttachListener
,ReduceSignalUsage
均默认是false(globals.hpp)
product(bool, DisableAttachMechanism, false, \
"Disable mechanism that allows tools to Attach to this VM”)
product(bool, StartAttachListener, false, \
"Always start Attach Listener at VM startup")
product(bool, ReduceSignalUsage, false, \
"Reduce the use of OS signals in Java and/or the VM”)
2)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
linux
支持操作系统信号通知:
1)默认情况下,ReduceSignalUsage
配置的是false
,初始化完Signal Dispatcher
线程就不需要立即初始化Attach Listener线程。而是在收到操作系统通知的时候,去触发Attach Listener线程初始化;
2)如果ReduceSignalUsage
配置的是true
,那JVM启动时就不会启动Signal Dispatcher
线程。也就无法接收并处理操作系统的信号通知。这时就需要在JVM启动的时候需要立即初始化Attach
;
注:windows
虽然也有操作系统的信号通知,不过信号通知类型并没有linux那么多,JDK也并没有实现windows下的操作系统信号处理逻辑,因此windows下在JVM启动时就需要直接初始化Attach Listener线程;
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
信号。
补充:
AttachListener(请求接收线程)
:AttachListener线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求JVM给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在JVM启动的时候没有初始化,那么,则会在用户第一次执行JVM命令时,得到启动。
Signal Dispatcher(信号转发线程)
:前面我们提到第一个Attach Listener线程的职责是接收外部JVM命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部JVM命令时,进行初始化工作。
总结:可以看出以上的AttachListener线程和Signal Dispatcher线程是属于两者相互解耦的线程,有点类似单一职责且加入了命令模式的概念在里面,一个抓门负责接受(AttachListener)之后进行封装后转发给执行线程(Signal DIspatcher)统一进行执行和派遣工作。如同邮局收件和运件属于两个部门。