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、全然使ったことありませんでしたが、便利そうですね。