JDI でオブジェクトの年齢(age)を取得
HotSpot VM の世代別 GC において、オブジェクト(インスタンス)には年齢 (age) が設定されており、Minor GC が適用される度にカウントアップされ、長命オブジェクトかどうかの判定に使われるとされています。
そこで今回は、Groovy で JDI (Java Debug Interface) と Serviceability Agent API を使って、実行中の Java アプリケーションへアタッチし、オブジェクトの年齢を取得してみたいと思います。
使用した環境は以下の通りです。
- Groovy 2.4.6
- Java SE 8u92 64bit版 (JDK)
ソースは 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) Oop の
getMark()
メソッドで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
が発生したため (Windows・Linux の両方で発生)、とりあえず 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