JDI でオブジェクトの年齢(age)を取得

HotSpot VM の世代別 GC において、オブジェクト(インスタンス)には年齢 (age) が設定されており、Minor GC が適用される度にカウントアップされ、長命オブジェクトかどうかの判定に使われるとされています。

そこで今回は、Groovy で JDI (Java Debug Interface)Serviceability Agent API を使って、実行中の Java アプリケーションへアタッチし、オブジェクトの年齢を取得してみたいと思います。

使用した環境は以下の通りです。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160425/

はじめに

今回は、JDI の SA PID コネクタsun.jvm.hotspot.jdi.SAPIDAttachingConnector) を使ってデバッグ接続する事にします。

SAPIDAttachingConnector の特徴は以下のようになります。

SAPIDAttachingConnector の特徴
利点 欠点
デバッグ対象アプリケーションの実行時に -agentlib:jdwp のようなオプション指定が不要 読み取り専用で VM へアタッチするため、実行可能な API が限定される。デバッグ接続中は対象のプロセスが中断する ※
 ※ 全スレッドの処理が中断した状態となり、
    ステップ実行のような状態を変化させる API は使えません。
    (VMCannotBeModifiedException が throw される API は使えない)

デバッグ接続中は全ての処理が完全に中断するので、運用中のサーバーアプリケーションなどへの適用には不向きだと思います。

(a) JDI でクラスの一覧を取得

まずは SAPIDAttachingConnector の動作確認も兼ねて、実行中のアプリケーションへデバッグ接続して、クラスの一覧を出力してみます。

SAPIDAttachingConnector を使ったデバッグ接続手順は以下のようになります。

  • (1) Bootstrap から com.sun.jdi.VirtualMachineManager を取得
  • (2) VirtualMachineManager から利用可能な JDI コネクタの一覧を attachingConnectors メソッドで取得し、SAPIDAttachingConnector を抽出
  • (3) デバッグ対象アプリケーションのプロセスIDをパラメータへ設定しデバッグ接続

SAPIDAttachingConnector を使うには、JDI を使用するアプリケーション(下記スクリプト)の実行時のクラスパスへ lib/sa-jdi.jar を含めておかなければならない点に注意が必要です ※。

そうしないと attachingConnectors の結果に SAPIDAttachingConnector が含まれず、下記スクリプトでは NullPointerException となります。 (find の結果が null になるので)

 ※ JDI を使うには lib/tools.jar が必要ですが、
    Groovy の場合は groovy-starter.conf のデフォルト設定で
    tools.jar をロードするようになっています
class_list.groovy
import com.sun.jdi.Bootstrap

def pid = args[0]

// (1)
def manager = Bootstrap.virtualMachineManager()

// (2) SAPIDAttachingConnector を抽出
def connector = manager.attachingConnectors().find {
    it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector'
}

// パラメータの設定
def params = connector.defaultArguments()
params.get('pid').setValue(pid)

// (3) デバッグ接続
def vm = connector.attach(params)

try {
    // クラス情報 com.sun.jdi.ReferenceType の取得
    vm.allClasses().each { cls ->
        println cls.name()
    }
} finally {
    vm.dispose()
}

動作確認

今回は apache-tomcat-9.0.0.M4 を起動しておき、上記スクリプトデバッグ接続してクラスの情報を取得してみます。

実行例1 (Windows の場合)

まずはデバッグ対象の Tomcat を実行しておきます。(設定等はデフォルトのまま)

Tomcat 起動
> startup

jps 等でプロセス ID を調べて上記スクリプトを実行します。 JDK の lib/sa-jdi.jar をクラスパスへ指定する必要があります。

class_list.groovy の実行
> jps

804 Bootstrap
5244 Jps

> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar class_list.groovy 804

sun.management.HotSpotDiagnostic
java.security.CodeSigner
java.security.CodeSigner[]
java.lang.Character
java.lang.Character[]
・・・
short[]
short[][]
long[]
float[]
double[]

実行例2 (Linux の場合)

Linux でも同じです。

Tomcat 起動
$ ./startup.sh
class_list.groovy の実行
$ jps

2388 Jps
2363 Bootstrap

$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar class_list.groovy 2363

sun.nio.ch.SelectorProviderImpl
java.util.jar.JarInputStream
org.apache.catalina.mapper.Mapper$ContextVersion
org.apache.catalina.mapper.Mapper$ContextVersion[]
java.nio.channels.SeekableByteChannel
・・・
short[]
short[][]
long[]
float[]
double[]

SAPIDAttachingConnector により読み取り専用でデバッグ接続するため、デバッグ接続中に Tomcat へアクセスしても応答が返ってこなくなる点にご注意下さい。

(b) JDI でオブジェクトの年齢を取得

それでは、本題の年齢を取得してみます。

クラス情報 ReferenceType を取得するまでは上記 (a) と同じで、その後は以下のように JDI の API から Serviceability Agent API を取り出せばオブジェクトの年齢を取得できます。

  • (1) ReferenceType の instances メソッドインスタンス情報 ObjectReference を取得 (引数を 0 にすると全インスタンスを取得)
  • (2) (1) の実体が sun.jvm.hotspot.jdi.ObjectReferenceImpl なので、protected メソッドref() を呼び出して sun.jvm.hotspot.oops.Oop (Serviceability Agent API) を取得
  • (3) OopgetMark() メソッドsun.jvm.hotspot.oops.Mark を取得し、age() メソッドで年齢を取得

instances メソッドは非常に重い処理だったので、今回は対象クラスをコマンドライン引数 (第2引数) で指定した名称で始まるクラス名に限定するようにしました。 (下記 findAll の箇所)

また、Oop は他の方法 (sun.jvm.hotspot.oops.ObjectHeap を使用) でも取得できるようです。

age_list.groovy
import com.sun.jdi.Bootstrap

def pid = args[0]
def prefix = args[1]

def manager = Bootstrap.virtualMachineManager()

def connector = manager.attachingConnectors().find {
    it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector'
}

def params = connector.defaultArguments()
params.get('pid').setValue(pid)

def vm = connector.attach(params)

try {
    if (vm.canGetInstanceInfo()) {
        vm.allClasses().findAll { it.name().startsWith(prefix) }.each { cls ->
            println cls.name()

            // (1) インスタンス情報 ObjectReference の取得
            cls.instances(0).each { inst ->
                // (2) Oop の取得
                def oop = inst.ref()
                // (3) Mark を取得して年齢を取得
                def age = oop.mark.age()

                println "  handle=${oop.handle}, age=${age}"
            }
        }
    }
} finally {
    vm.dispose()
}

ここで、HotSpot VM のソース share/vm/oops/oop.inline.hpp の oopDesc::age() を見ると、has_displaced_mark() が true の時は displaced_mark の age を取得しているのですが、同じように実装(下記)すると hasDisplacedMarkHelper が true の場合に sun.jvm.hotspot.debugger.UnalignedAddressException が発生したため (WindowsLinux の両方で発生)、とりあえず displacedMarkHelper は今回無視するようにしました。

hasDisplacedMarkHelper が true の場合に UnalignedAddressException が発生したコード例
def mark = oop.mark
def age = mark.hasDisplacedMarkHelper()? mark.displacedMarkHelper().age(): mark.age()

動作確認

先程と同じように実行中の apache-tomcat-9.0.0.M4 に対して適用してみます。

全クラスを対象にすると時間がかかり過ぎるので、クラス名が org.apache.catalina.core.ApplicationContext で始まるものに限定してみました。

実行例1 (Windows の場合)

> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar age_list.groovy 804 org.apache.catalina.core.ApplicationContext

org.apache.catalina.core.ApplicationContextFacade
  handle=0x00000000c39602f0, age=0
  handle=0x00000000c3962460, age=1
  handle=0x00000000c3971718, age=0
  handle=0x00000000ecf913b0, age=3
  handle=0x00000000ecf99950, age=1
org.apache.catalina.core.ApplicationContext
  handle=0x00000000c39607e0, age=0
  handle=0x00000000c3966960, age=1
  handle=0x00000000c3971c08, age=0
  handle=0x00000000ecf918a0, age=3
  handle=0x00000000ecf99680, age=1

実行例2 (Linux の場合)

$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar age_list.groovy 2363 org.apache.catalina.core.ApplicationContext

org.apache.catalina.core.ApplicationContextFacade
  handle=0x00000000fb041d08, age=1
  handle=0x00000000fb1288d8, age=0
  handle=0x00000000fb2b1df0, age=1
  handle=0x00000000fb307c98, age=1
  handle=0x00000000fb318aa8, age=1
org.apache.catalina.core.ApplicationContext
  handle=0x00000000fb0338c0, age=1
  handle=0x00000000fb12ed50, age=0
  handle=0x00000000fb2a56a0, age=1
  handle=0x00000000fb2f8da8, age=1
  handle=0x00000000fb312a80, age=1