JVM Attach API
Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM “附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。更多相关内容请参考 Oracal Java Attach API 支持
或 IBM Java Attach API 支持。
Sun JVM Attach API
Sun JVM Attach API是Sun JVM中的一套非标准的可以连接到JVM上的API,从JDK6开始引入,除了Solaris平台的Sun JVM支持远程的Attach,在其他平台都只允许Attach到本地的JVM上。
Attach API 很简单,只有 2 个主要的类,都在com.sun.tools.attach
包里面: VirtualMachine
代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了JVM枚举,Attach动作和Detach动作等等 ; VirtualMachineDescriptor
则是一个描述虚拟机的容器类,配合VirtualMachine
类完成各种功能。
应用实例
之前在一个迁移项目上遇到一个Heap Dump的问题,迁移前Heap Dump的方式是使用jmap工具来进行Heap Dump,但是在迁移后的环境中,没有了JDK的环境,所以无法使用jmap来直接进行Heap Dump,这时选择的方式是改为使用Attach API来进行Heap Dump,下面介绍一下使用Attach API来Heap Dump的方式。
列出本地所有的JVM
本例子是列出所有本地的JVM实例,当然也可以使用VirtualMachineDescriptor
对象获取displayName来筛选出目标JVM。
List<VirtualMachineDescriptor> vmDescriptorList = VirtualMachine.list();
List<String> ids = new ArrayList<>();
for (VirtualMachineDescriptor vmDescriptor : vmDescriptorList) {
ids.add(vmDescriptor.id());
}
由JVM ID对目标程序链接
HotSpotVirtualMachine vm = (HotSpotVirtualMachine) HotSpotVirtualMachine.attach(id);
使用HotSpotVirtualMachine对象执行Heap Dump
HotSpotVirtualMachine.dumpHeap("/the/path/of/dump/file/heapdump.hprof")
完整的代码
把上面的步骤拼接起来,完成全部Heap Dump的操作了。
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import sun.tools.attach.HotSpotVirtualMachine;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException, AttachNotSupportedException {
List<String> ids = getAllVMIdList();
for (String id : ids) {
HotSpotVirtualMachine vm = (HotSpotVirtualMachine) HotSpotVirtualMachine.attach(id);
try (InputStream in = vm.dumpHeap("./heapdump.hprof")) {
byte[] buf = new byte[200];
for (int bytes; (bytes = in.read(buf)) > 0; ) {
System.out.write(buf, 0, bytes);
}
} finally {
vm.detach();
}
}
}
private static List<String> getAllVMIdList() {
List<VirtualMachineDescriptor> vmDescriptorList = VirtualMachine.list();
List<String> ids = new ArrayList<>();
for (VirtualMachineDescriptor vmDescriptor : vmDescriptorList) {
ids.add(vmDescriptor.id());
}
return ids;
}
}
由下图所示,本地由三个JVM实例,一个是调试使用的Intellij IDEA JVM,第二个就是我们的项目的JVM,DisplayName为Main,和我们的Main.class一致,另一个我们先暂时忽略。如此就可以筛选出目标JVM或者选择Dump所有的JVM了。
[图片上传失败...(image-3069a6-1651590805009)]
Attach API Client端原理
根据Attach API Client端的源码可以看出,链接到JVM的方式主要还是使用了socket链接,根据JVM ID找到对应的java进程socket file进行socket连接,而后通过socket发送具体的命令进行相关的JVM操作。(获取JVM ID的方法稍微复杂些,这里不做讲解,具体可以跟进com.sun.tools.attach.VirtualMachine.list()
方法)
我们由HotSpotVirtualMachine.attach(id)
attach入口方法跟进,在sun.tools.attach.AttachProviderImpl
中,可以找到如下代码:
public VirtualMachine attachVirtualMachine(String vmid)
throws AttachNotSupportedException, IOException {
checkAttachPermission();
// AttachNotSupportedException will be thrown if the target VM can be determined
// to be not attachable.
testAttachable(vmid);
return new VirtualMachineImpl(this, vmid);
}
继续跟进VirtualMachineImpl(this, vmid)
可以看到由pid获取socket file后初始化socket连接的具体逻辑
VirtualMachineImpl(AttachProvider provider, String vmid)
throws AttachNotSupportedException, IOException {
super(provider, vmid);
// This provider only understands pids
int pid;
try {
pid = Integer.parseInt(vmid);
if (pid < 1) {
throw new NumberFormatException();
}
} catch (NumberFormatException x) {
throw new AttachNotSupportedException("Invalid process identifier: " + vmid);
}
// Find the socket file. If not found then we attempt to start the
// attach mechanism in the target VM by sending it a QUIT signal.
// Then we attempt to find the socket file again.
File socket_file = new File(tmpdir, ".java_pid" + pid);
socket_path = socket_file.getPath();
if (!socket_file.exists()) {
File f = createAttachFile(pid);
try {
sendQuitTo(pid);
// give the target VM time to start the attach mechanism
final int delay_step = 100;
final long timeout = attachTimeout();
long time_spend = 0;
long delay = 0;
do {
// Increase timeout on each attempt to reduce polling
delay += delay_step;
try {
Thread.sleep(delay);
} catch (InterruptedException x) { }
time_spend += delay;
if (time_spend > timeout/2 && !socket_file.exists()) {
// Send QUIT again to give target VM the last chance to react
sendQuitTo(pid);
}
} while (time_spend <= timeout && !socket_file.exists());
if (!socket_file.exists()) {
throw new AttachNotSupportedException(
String.format("Unable to open socket file %s: " +
"target process %d doesn't respond within %dms " +
"or HotSpot VM not loaded", socket_path,
pid, time_spend));
}
} finally {
f.delete();
}
}
// Check that the file owner/permission to avoid attaching to
// bogus process
checkPermissions(socket_path);
// Check that we can connect to the process
// - this ensures we throw the permission denied error now rather than
// later when we attempt to enqueue a command.
int s = socket();
try {
connect(s, socket_path);
} finally {
close(s);
}
}
完成socket连接后,接下来就是要执行相关的命令,从HotSpotVirtualMachine.dumpHeap(Object ... args)
方法跟进,如下:
public InputStream dumpHeap(Object ... args) throws IOException {
return executeCommand("dumpheap", args);
}
继续跟进executeCommand
在sun.tools.attach.VirtualMachineImpl
中,可以找到VirtualMachineImpl.execute
方法,通过writeString(s, cmd)
方法,将cmd命令通过socket发送到目标JVM,从而进行相关的JVM操作。
InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
assert args.length <= 3; // includes null
// did we detach?
synchronized (this) {
if (socket_path == null) {
throw new IOException("Detached from target VM");
}
}
// create UNIX socket
int s = socket();
// connect to target VM
try {
connect(s, socket_path);
} catch (IOException x) {
close(s);
throw x;
}
IOException ioe = null;
// connected - write request
// <ver> <cmd> <args...>
try {
writeString(s, PROTOCOL_VERSION);
writeString(s, cmd);
for (int i = 0; i < 3; i++) {
if (i < args.length && args[i] != null) {
writeString(s, (String)args[i]);
} else {
writeString(s, "");
}
}
} catch (IOException x) {
ioe = x;
}
...省略...
最后推荐一个开源的Attach API工具jattach,由C语言实现,相比参考Java代码,C实现的代码相对更简单,例如https://github.com/apangin/jattach/blob/master/src/posix/jattach_hotspot.c ,也是使用socket的方式连接JVM,发送相关命令,操作JVM。