JVM Attach API

2022-05-02  本文已影响0人  程序员札记

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

继续跟进executeCommandsun.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。

上一篇下一篇

猜你喜欢

热点阅读