「Java SE 6完全攻略」の第2回でオンデマンドアタッチを紹介しました。起動時に何も指定しなくても,必要なときにjconsoleでJava VMにアタッチできるという機能です。

それにしても,オンデマンドアタッチって,どうやって実現させているか不思議ではないですか?

筆者は夜も眠れぬぐらい不思議に思っていたのです。そんなある日,あらためてJava SE 6のドキュメントをつらつら眺めていると,Attach APIという聞き慣れないAPIを見つけたのです。

そう,このAttach APIがオンデマンドアタッチを実現するためのAPIだったのです。

Attach APIの正体

Attach APIはcom.sun.tools.attachパッケージとcom.sun.tools.attach.spiパッケージで定義されている四つのクラスから構成されています。しかし,主に使うのはcomsun.tools.attach.VirtualMachineクラスだけです。

また,パッケージ名がcom.sunではじまっていることからわかるように,標準のAPIではありません。SunのHotSpot VMにだけ適用できるAPIになっています。

VirtualMachineクラスが提供している機能はそれほど多くありません。主な機能を挙げてみましょう。

  1. VMの一覧を取得
  2. VMへのアタッチ/デタッチ
  3. 環境変数の取得
  4. Instrumentation APIを使用するエージェントのロード
  5. Java Virtual Machine Tool Interface(JVMTI)を使用するエージェントのロード

jconsoleを起動したときのことを思いだしてください。一番はじめに起動しているVMの一覧が表示されますね。このVMの一覧を取得するのにAttach APIを利用しています。

そして,VMにアタッチしたあとに,VMにエージェントを送り込みます。

エージェントにはいろいろな意味があり,使う人によって指すものが異なることもよくあります。ここでの「エージェント」は,Javaのアプリケーションとは独立して動作するコンポーネント,という意味です。

このエージェントには2種類あります。一方が,Instrumentation APIで使用するエージェント。これはJavaで記述されています。もう一方が,JVMTIを使用するエージェント。こちらはC/C++で記述されています。

Instrumentation APIを利用したエージェントは,mainメソッドがコールされる前に起動し,クラスファイルを操作したりすることができます。

このエージェントはVirtualMachineクラスのloadAgentメソッドを使用して,ロードします。

JVMTIのエージェントは,デバッグとプロファイリングに使います。このエージェントはVirtualMachineクラスのloadAgentLibraryメソッド,もしくはloadAgentPathメソッドを使用してロードします。

jconsoleのオンデマンドアタッチは二つのエージェントのうち,Instrumentation APIを使用したエージェントによるものです。

それでは,オンデマンドアタッチと同じことをここで実現してみましょう。

オンデマンドアタッチを実現する

せっかくなので,jconsoleと同じようにJMXとMXBeanを使って,ヒープの使用量を調べてみましょう。

ソースコードはこちらからダウンロードできます:AttachSample.javaAttachAgent.javaMANIFEST.MF

それでは,まずAttach APIを使用して,起動しているVMの一覧を出力してみます。これを行っているのが,AttachSampleのデフォルトコンストラクタです。

public AttachSample() {
     List<VirtualMachineDescriptor> vms = VirtualMachine.list();

     System.out.println("PID   VM");
     for(VirtualMachineDescriptor vm: vms) {
         System.out.printf("%4s: %s%n", 
                           vm.id(), vm.displayName());
     }
}

起動しているVMの一覧を取得するには,VirtualMachineクラスのlistメソッドを使用します。戻り値はVirtualMachineDescriptorオブジェクトのリストになります。

VirtualMachineDescriptorクラスは,その名前のとおり,VMの情報を保持するクラスです。上記のコードではVMのIDと名前を出力しています。IDの型はStringです。WindowsやUNIXでは,VMのIDとしてプロセスIDが使われます。

次はいよいよVMにアタッチしてみましょう。

アタッチするには,VirtualMachineクラスのstaticなattachメソッドを使用します。attachメソッドの引数はVMのID,もしくはVirtualMachineDescriptorオブジェクトです。下に示すコードではIDを使用しました。

private final static String AGENT_PATH = "C:\\temp\\agent.jar";
			  
public AttachSample(String pid) {
    try {
        // VMにアタッチ
        VirtualMachine vm = VirtualMachine.attach(pid);

        // エージェントをVMにロードする
        vm.loadAgent(AGENT_PATH, null);

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

        connect();
    } catch (AttachNotSupportedException ex) {
        ex.printStackTrace();
    } catch (AgentLoadException ex) {
        ex.printStackTrace();
    } catch (AgentInitializationException ex) {
        ex.printStackTrace();
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}

次のloadAgentメソッドでVMにエージェントをロードしています。loadAgentメソッドの第1引数はロードするエージェントを含むJARファイルの場所,第2引数がエージェントに与える引数になります。

そして,VMから切断するためにdetachメソッドをコールします。

ロードするエージェントに関しては後述します。

サンプルとして作成したエージェントは,JMX Remoteを使用してリモートからアクセスできるようにしています。そこで,connectメソッドでアクセスしてみます。

private final static String JMX_URL
          = "service:jmx:rmi:///jndi/rmi://localhost/jmx";
			  
private void connect() {
    try {
        // ロードしたエージェントがオープンしたMBeanServerに接続
        JMXServiceURL url = new JMXServiceURL(JMX_URL);
        JMXConnector connector = JMXConnectorFactory.connect(url);
        MBeanServerConnection connection
            = connector.getMBeanServerConnection();

        // MBeanServerからMXBeanを取得
        MemoryMXBean memoryMXBean 
            = ManagementFactory.newPlatformMXBeanProxy(
                connection,
                ManagementFactory.MEMORY_MXBEAN_NAME,
                MemoryMXBean.class);

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

        connector.close();
    } catch (MalformedURLException ex) {
        ex.printStackTrace();
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}

JMXRemoteを使用してVMにアクセスするには,次のような手順を踏みます。

  1. JMXServiceURLクラスを使用してアクセスするJMXのURLを表す
  2. JMXServiceURLオブジェクトを引数にして,JMXConnectorFactory#connectメソッドをコールする。connectメソッドの戻り値はJMXConnectorオブジェクトになる
  3. JMXConnectorクラスのgetMBeanServerConnectionメソッドを用いて,MBeanServerとの接続を表すMBeanServerConncetionオブジェクトを取得する

MBeanServerConnectionオブジェクトが取得できたら,第6回のjconsoleのプラグインで使用した方法と同様にMXBeanのプロキシを生成します。あとは必要な情報をMXBeanから取得するだけです。

最後にJMXConnectorオブジェクトのcloseメソッドをコールして,切断します。

エージェントの作成

さて,VMに送り込むエージェントを作成しましょう。

注意すべき点があります。パッケージ宣言をしていないクラスはロードできないので,かならずパッケージを設定するようにしましょう。

エージェントに必要なのは,premainメソッドとagentmainメソッドです。

起動時に実行するエージェントの場合はpremainメソッドがコールされます。起動時に実行するエージェントについては,筆者のWebサイトにある「J2SE5.0虎の穴」のInstrument APIの解説をご参照ください。

VMが起動中にロードされた場合,agentmainメソッドがコールされます。

ここではpremainメソッドはagentmainメソッドをコールしているだけです。

public class AttachAgent {
    public static final String SERVICE_URL
        = "service:jmx:rmi:///jndi/rmi://localhost/jmx";
 
    // mainメソッドがコールされる前に呼ばれるメソッド
    public static void premain(String args) throws Exception {
        agentmain(args);
    }
 
    // VM起動後に,エージェントがロードされたときに,
    // コールされるメソッド
    public static void agentmain(String args) throws Exception {
        try {
            // MXBeanを登録してあるMBeanServerを取得
            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
 
            // JMX Remoteを使用したリモート接続の設定
            JMXServiceURL url = new JMXServiceURL(SERVICE_URL);
            JMXConnectorServer connector
                = JMXConnectorServerFactory.newJMXConnectorServer(
                    url, null, server);
 
            // コネクタの開始
            connector.start();
        } catch (MalformedURLException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

JMX Remoteを使ったアクセスができるようにするにはJMXConnectorServerオブジェクトを使用します。

まずはアクセスするためのMBeanServerオブジェクトを生成します。ここではMXBeanが登録されているMBeanServerオブジェクトを使用します。

アクセスを行うためのURLは先ほど記述したものと同じです。これをJMXServiceURLクラスで表します。

JMXConnectorServerオブジェクトはJMXConnectorFactory#newJMXConncetorServerメソッドを使用して生成します。生成できたらstartメソッドでアクセスできるようにします。

これでエージェントの本体はできました。しかし,これだけではまだ足りません。

今回のサンプルは単純なので,一つのクラスしかありませんが,エージェントが複数のクラスから構成されている場合を考えてみましょう。そのような場合,VMにはどのクラスのagentmainメソッドをコールすればいいかわかりません。

したがって,agentmainメソッドを持つクラスを指定する必要があります。これにはJARファイルのマニフェスト・ファイルを利用します。

Manifest-Version: 1.0
Premain-Class: net.javainthebox.attach.AttachAgent
Agent-Class: net.javainthebox.attach.AttachAgent

Premain-Classにpremainメソッドを持つクラス,Agent-Classにagentmainメソッドを持つクラスを記述します。ここで作成したエージェントはpremainメソッドとagentmainメソッドの両方を持つので,両方とも同じAttachAgentクラスを記述しています。

コンパイルと実行

さて,ソースコードができたのでコンパイルしましょう。

エージェントはそのままコンパイルできますが,Attach APIを使用しているAttachSampleクラスはそのままではコンパイルできません。

というのもAttach APIは[JDK_HOME]/lib/tools.jarで定義されているからです。

C:\temp>javac -cp "C:\Program Files\Java\jdk1.6.0\lib\tools.jar";. AttachSample.java

次にエージェントのJARファイルを作成しましょう。MANIFEST.MFファイルはカレント・ディレクトリにあるとします。また,JARファイルの名前はagent.jarとしましょう。

AttachAgentクラスはnet.javainthebox.attachパッケージなので,次のようにjarコマンドを実行します。

C:\temp>jar cvmf MANIFEST.MF agent.jar net\javainthebox\attach\*.class

コンパイルできたので,実行してみましょう。

JMX Remoteは通信にRMIを使用するので,事前にrmiregistryを実行しておく必要があります。えっ,jconsoleを使うときはrmiregistryなんか実行しないよ,と思われる方も多いでしょう。

そうなんです。rmiregistryはアプリケーション中で実行させることもできるのです。しかし,ここでは簡単化のために省略しました。

興味のある方は,sun.management.Agentクラスを調べてみてください。このクラスがjconsoleによってロードされるエージェントです。

それでは早速実行してみましょう。まずは引数なしでVMの一覧を出力してみます。

C:\temp>java -cp "c:\Program Files\Java\jdk1.6.0\lib\tools.jar";. AttachSample
PID   VM
1132: Java2Demo.jar
3052: sun.rmi.registry.RegistryImpl
1272: AttachSample
 
C:\temp>

Java2Demoが動作しているようなので,これにアタッチしてみましょう。

C:\temp>java -cp "c:\Program Files\Java\jdk1.6.0\lib\tools.jar";. AttachSample 1132
init = 0(0K) used = 8934856(8725K) committed = 14352384(14016K) max = 66650112(65088K)
 
C:\temp>

ちゃんとアタッチして,ヒープの使用量を出力できました。

このように簡単にエージェントをリモートからVMに送り込むことができます。しかし,勝手にエージェントが送り込まれたら,ちょっと怖いですね。

ここで,jconsoleでオンデマンドアタッチをするときの制限を思いだしてください。そう,同一マシン上で,同一ユーザーだけがjconsoleでアタッチできます。

これはAttach APIの制限によっています。つまり,Attach APIでエージェントをロードする場合,同一マシン,かつ同一ユーザーに限られるというわけです。

今回は,管理のためにAttach APIを使いましたが,その他の用途にもエージェントを使うことはできそうです。JVMTIのエージェントも使用可能なので,パフォーマンスチューニングなどにも活用できますよ。

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

この時期になると何となく気になるのが,クリスマス。

実をいうと,櫻庭はクリスマスCDのコレクターなのです。数えたことがないので正確な枚数はわからないのですが,優に100枚はあるはず。

今年もDaryl Hall & John OatsやAimee Mannなどのクリスマスアルバムが発売されています。ということで,今年のクリスマスアルバムの櫻庭のお薦めは...

  • 癒されたいあなたには
    Keali'i Reichel "Maluhia"
  • しっとりとしたクリスマスを過したいあなたには
    Sara McLachlan "Wintersong"
  • ちょっと変ったのが好みというあなたには
    Bootsy Collins "Christmas Is 4 Ever"

いかがですか。なお,これを書いている時点では,James TaylorやBlackmore's Nightなど,聴いていないアルバムもあるので,お薦めが入れ替わる可能性はありますが,参考になればと思います。