CLOVER🍀

That was when it all began.

ローカルのJavaVMのリストを出力したり、JConsoleのようにJMX接続したりしてみる

JMXのリモート接続を有効にしていないローカルのJavaVMプロセスに対して、JConsoleのようにJMX接続するにはどうしたらいいのかな?と思い、調べてみました。

何か方法があるのだろうと。

調べてみたら、tools.jarに含まれているクラスを使用すれば、なんとかなりそうな感じですね。

参考)
「Java SE 6完全攻略」第10回 オンデマンドアタッチを実現するAttach API | 日経 xTECH(クロステック)

プログラムによる監視および管理の設定

前述のように、Java SE プラットフォームのバージョン 6 では、Attach API を使用する JMX クライアントを作成できます。これによって Java SE 6 プラットフォームで起動する任意のアプリケーションから、起動時に監視用にアプリケーションを構成しなくても、アウトオブボックスの監視および管理ができるようになります。Attach API は、ツールによってターゲットアプリケーションのエージェントに接続し、それらのエージェントを起動します。エージェントが起動すれば、JMX クライアント (やほかのツール) は、そのエージェントに代わって Java VM が保持するプロパティーリストから、そのエージェントの JMX コネクタアドレスを取得できます。このリストに含まれるプロパティーは、Attach API. を使用するツールからアクセスできます。したがって、アプリケーションから起動したエージェントが構成情報を示すプロパティーを生成した場合、その構成情報はアプリケーションに接続するツールから利用できます。

https://docs.oracle.com/javase/jp/7/technotes/guides/management/agent.html

Attach APIを使えばよい、と。

プログラムのコンパイル、実行の前に

Attach APIを使うには、tools.jarにクラスパスを通しておくことが必要みたいです。

Mavenであれば、こんな感じに。

        <dependency>
            <groupId>sun.jdk</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

javacでコンパイルするなら

$ javac -cp ${JAVA_HOME}/lib/tools.jar

というようにクラスパスを通し、javaコマンドでの実行時には

$ java -cp ${JAVA_HOME}/lib/tools.jar

みたいな感じになりますと。

サンプルプログラム

それでは、サンプルプログラムを書いてみます。

クラス宣言とimport文

これから使用するクラスの選言と、import文を以下に記載しておきます。クラスの意味はあまりなさないので、ホント宣言だけですが。

src/main/java/org/littlewings/jmx/local/App.java

package org.littlewings.jmx.local;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.List;
import java.util.Optional;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class App {
    public static void main(String... args) {
        // 以降に、ここに埋めるサンプルを書く!
    }
}
ローカルで動作しているJavaVMの一覧を取得する

VirtualMachine#listを使用すればよいみたいです。

        List<VirtualMachineDescriptor> vmDescriptors = VirtualMachine.list();
        vmDescriptors.forEach(vmd -> {
            System.out.println("Local JVM Process");
            System.out.println("  PID = " + vmd.id());
            System.out.println("  DisplayName = " + vmd.displayName());
            System.out.println("  Name = " + vmd.provider().name());
            System.out.println("  Type = " + vmd.provider().type());
        });

VirtualMachineDescriptor#idで、PIDが取れるようです。VirtualMachineDescriptor#displayNameは、クラス名+引数みたいです。

このプログラムを動かした時の出力例は、以下です。

Local JVM Process
  PID = 112945
  DisplayName = com.intellij.idea.Main
  Name = sun
  Type = socket
Local JVM Process
  PID = 50531
  DisplayName = org.jetbrains.idea.maven.server.RemoteMavenServer
  Name = sun
  Type = socket
Local JVM Process
  PID = 52794
  DisplayName = org.littlewings.jmx.local.App
  Name = sun
  Type = socket
Local JVM Process
  PID = 24780
  DisplayName = org.jetbrains.plugins.scala.nailgun.NailgunRunner 48644 948140ac-bc9d-4ff3-86c6-80e42a65f304
  Name = sun
  Type = socket

一緒に、IntelliJ IDEAも動かしていました…。

自分自身は、こちらです。

Local JVM Process
  PID = 52794
  DisplayName = org.littlewings.jmx.local.App
  Name = sun
  Type = socket
ローカルのJavaVMプロセスに、JMX接続する

続いて、ローカルのJavaVMにJMX接続してみます。

この場合、JavaVMにアタッチする必要があるようです。アタッチするにはVirtualMachine#attachを使用しますが、StringでPIDを指定するか、VirtualMachineDescriptorを渡せばよいみたいです。

今回は、VirtualMachine#listから自分のPIDを探し、そこからJMX接続してみます。

        List<VirtualMachineDescriptor> vmDescriptors = VirtualMachine.list();

        Optional<String> selfPid =
                vmDescriptors
                        .stream()
                        .filter(vmd -> vmd.displayName().contains(App.class.getName()))
                        .map(VirtualMachineDescriptor::id)
                        .findFirst();

        selfPid.ifPresent(pid -> {
            try {
                String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";

                // VMにアタッチ
                VirtualMachine vm = VirtualMachine.attach(pid);

                System.out.println("Attache PID = " + pid);
                String connectorAddress =
                        vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);

                if (connectorAddress == null) {
                    // Agentが未ロードの場合は、ロード
                    String agent = vm.getSystemProperties().getProperty("java.home") +
                            File.separator + "lib" + File.separator + "management-agent.jar";
                    vm.loadAgent(agent);

                    System.out.println("agent loaded.");

                    connectorAddress =
                            vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
                }

                // VMからデタッチ
                vm.detach();

                // JMX接続確立
                JMXServiceURL url = new JMXServiceURL(connectorAddress);

                System.out.println("Service URL = " + connectorAddress);

                try (JMXConnector connector = JMXConnectorFactory.connect(url)) {
                    MBeanServerConnection connection = connector.getMBeanServerConnection();

                    MemoryMXBean memoryMXBean
                            = ManagementFactory.newPlatformMXBeanProxy(
                            connection,
                            ManagementFactory.MEMORY_MXBEAN_NAME,
                            MemoryMXBean.class);

                    // ヒープの使用量を出力
                    MemoryUsage usage = memoryMXBean.getHeapMemoryUsage();
                    System.out.println(usage);
                }

            } catch (IOException | AttachNotSupportedException | AgentInitializationException | AgentLoadException e) {
                e.printStackTrace();
            }
        });

大半は、ここに載っている例のままです。
JMX テクノロジを使用する監視と管理 - Java SE 監視および管理ガイド

PIDを指定して、アタッチを試みます。

                String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";

                // VMにアタッチ
                VirtualMachine vm = VirtualMachine.attach(pid);

                System.out.println("Attache PID = " + pid);
                String connectorAddress =
                        vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);

ここで、ConnectorAddressが取得できなかった場合は、Agentのロードを行います。

                if (connectorAddress == null) {
                    // Agentが未ロードの場合は、ロード
                    String agent = vm.getSystemProperties().getProperty("java.home") +
                            File.separator + "lib" + File.separator + "management-agent.jar";
                    vm.loadAgent(agent);

                    System.out.println("agent loaded.");

                    connectorAddress =
                            vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS);
                }

ConnectorAddressが取得できたら、もうVMからは切り離してよいみたいなので、デタッチします。

                // VMからデタッチ
                vm.detach();

あとは、この時のConnnectorAddressからJMX接続を確立することができますよ、と。

                // JMX接続確立
                JMXServiceURL url = new JMXServiceURL(connectorAddress);

                System.out.println("Service URL = " + connectorAddress);

                try (JMXConnector connector = JMXConnectorFactory.connect(url)) {
                    MBeanServerConnection connection = connector.getMBeanServerConnection();

                    MemoryMXBean memoryMXBean
                            = ManagementFactory.newPlatformMXBeanProxy(
                            connection,
                            ManagementFactory.MEMORY_MXBEAN_NAME,
                            MemoryMXBean.class);

                    // ヒープの使用量を出力
                    MemoryUsage usage = memoryMXBean.getHeapMemoryUsage();
                    System.out.println(usage);
                }

今回は、ヒープの使用量を出力しています。

実行結果は、こちら。

Attache PID = 52794
agent loaded.
Service URL = service:jmx:rmi://127.0.0.1/stub/rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LnJlbW90ZS5ybWkuUk1JU2VydmVySW1wbF9TdHViAAAAAAAAAAICAAB4cgAaamF2YS5ybWkuc2VydmVyLlJlbW90ZVN0dWLp/tzJi+FlGgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc0AAtVbmljYXN0UmVmMgAACTEyNy4wLjAuMQAAul1K8Vpt4XLMfItj6fwAAAFSLJhGiIABAHg=
init = 161480704(157696K) used = 5825776(5689K) committed = 155189248(151552K) max = 2293235712(2239488K)

ちゃんと動いてますね。

このあたりのAPI、全然使ったことありませんでしたが、便利そうですね。