JDI でオブジェクトの世代(Young・Old)を判別する

前回、オブジェクトの age を取得しましたが、同様の方法で今回はオブジェクトが Young 世代(New 領域)と Old 世代(Old 領域) のどちらに割り当てられているかを判別してみたいと思います。 (ただし、結果の正否は確認できていません)

使用した環境は前回と同じです。

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

Young・Old 世代の判別

さて、Young・Old の判別方法ですが。

Serviceability Agent API を見てみると sun.jvm.hotspot.gc_implementation.parallelScavenge パッケージに PSYoungGenPSOldGen というクラスがあり、isIn(Address) メソッドで判定できそうです。

更に PSYoungGen と PSOldGen は sun.jvm.hotspot.gc_implementation.parallelScavenge.ParallelScavengeHeap から取得できます。

Address (sun.jvm.hotspot.debugger パッケージ所属) は sun.jvm.hotspot.oops.OopgetHandle()getMark().getAddress() で取得できるので (下記サンプルでは getHandle を使用)、ParallelScavengeHeap を取得すれば何とかなりそうです。

実際に試してみたところ、ParallelScavengeHeap を取得できたのは Windows 環境で、Linux 環境では GenCollectedHeap を使った別の方法 (getGen メソッドを使う) が必要でした。 (GC の設定等によって更に変わるかもしれません)

世代の判定クラス
実行環境 ヒープクラス ※ Young 世代の判定クラス Old 世代の判定クラス
Windows ParallelScavengeHeap PSYoungGen PSOldGen
Linux GenCollectedHeap DefNewGeneration TenuredGeneration
 ※ Universe の heap() メソッド戻り値の実際の型
    CollectedHeap のサブクラス

上記を踏まえて、前回の処理をベースに以下を追加してみました。

  • (1) sun.jvm.hotspot.jdi.VirtualMachineImpl から sun.jvm.hotspot.runtime.VM オブジェクトを取り出す ※1
  • (2) VM オブジェクトから sun.jvm.hotspot.memory.Universe オブジェクトを取得
  • (3) Universe オブジェクトから CollectedHeap (のサブクラス) を取得 ※2
  • (4) (3) の結果から世代を判定するオブジェクトをそれぞれ取得

(4) で妥当な条件分岐の仕方が分からなかったので、とりあえず youngGen メソッドが無ければ GenCollectedHeap として処理するようにしました。

 ※1 private フィールドの saVM か、package メソッドの saVM() で取得

 ※2 今回のやり方では、Windows は ParallelScavengeHeap、
     Linux は GenCollectedHeap でした

JDI の SAPIDAttachingConnector で attach した結果が VirtualMachineImpl となります。

また、SAPIDAttachingConnector でデバッグ接続した場合 (読み取り専用のデバッグ接続)、デバッグ対象オブジェクトのメソッド (hashCode や toString 等) を呼び出せないようなので、オブジェクトを識別するための情報を得るため identityHash を使ってみました。 (ただし、戻り値が 0 になるものが多数ありました)

check_gen.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)

// (4) 世代を判定するためのオブジェクトを取得
generation = { heap ->
    def hasYoungGen = heap.metaClass.getMetaMethod('youngGen') != null

    [
        // Young 世代の判定オブジェクト(PSYoungGen or DefNewGeneration)
        young: hasYoungGen? heap.youngGen(): heap.getGen(0),
        // Old 世代の判定オブジェクト(PSOldGen or TenuredGeneration)
        old: hasYoungGen? heap.oldGen(): heap.getGen(1)
    ]
}

try {
    if (vm.canGetInstanceInfo()) {

        // (1) (2)
        def uv = vm.saVM.universe

        // (3)
        def gen = generation(uv.heap())

        def youngGen = gen.young
        def oldGen = gen.old

        println "*** youngGen=${youngGen}, oldGen=${oldGen}"
        println ''

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

            cls.instances(0).each { inst ->
                def oop = inst.ref()
                def age = oop.mark.age()

                // 世代の判別
                def inYoung = youngGen.isIn(oop.handle)
                def inOld = oldGen.isIn(oop.handle)

                def identityHash = ''

                try {
                    identityHash = Long.toHexString(oop.identityHash())
                } catch (e) {
                }

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

動作確認

前回と同じように、実行中の apache-tomcat-9.0.0.M4 へ適用してみました。

実行例1 (Windows の場合)
> jps

2836 Bootstrap
5944 Jps
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen.groovy 2836 org.apache.catalina.core.StandardContext

*** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002049ad0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002049b60

org.apache.catalina.core.StandardContext
  hash=66dfd722, handle=0x00000000c394a990, age=0, inYoung=false, inOld=true
  hash=39504d4e, handle=0x00000000edea7cf8, age=3, inYoung=true, inOld=false
  hash=194311fa, handle=0x00000000edea8e90, age=1, inYoung=true, inOld=false
  hash=2b28e016, handle=0x00000000edf0c130, age=2, inYoung=true, inOld=false
  hash=578787b8, handle=0x00000000edf457c0, age=1, inYoung=true, inOld=false
org.apache.catalina.core.StandardContext$ContextFilterMaps
  hash=0, handle=0x00000000c394e7d0, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000c396ec90, age=2, inYoung=false, inOld=true
  hash=0, handle=0x00000000c3988eb0, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000edf04320, age=1, inYoung=true, inOld=false
  hash=0, handle=0x00000000edf70988, age=1, inYoung=true, inOld=false
・・・
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen.groovy 2836 org.apache.catalina.LifecycleEvent

*** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002049ad0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002049b60

org.apache.catalina.LifecycleEvent
  hash=0, handle=0x00000000c37459c0, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000c374ed40, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000c39ff950, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000ebb8ef90, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000ebb90490, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000ebb904c0, age=0, inYoung=true, inOld=false
  ・・・
実行例2 (Linux の場合)
$ jps

2801 Jps
2790 Bootstrap
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen.groovy 2790 org.apache.catalina.core.StandardContext

*** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007fca50019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007fca5001bfc0

org.apache.catalina.core.StandardContext
  hash=27055bff, handle=0x00000000fb025d38, age=1, inYoung=false, inOld=true
  hash=5638a30f, handle=0x00000000fb1270a8, age=1, inYoung=false, inOld=true
  hash=15fad243, handle=0x00000000fb296730, age=1, inYoung=false, inOld=true
  hash=36c4d4a0, handle=0x00000000fb2f3cf0, age=1, inYoung=false, inOld=true
  hash=33309557, handle=0x00000000fb2f3ef8, age=1, inYoung=false, inOld=true
org.apache.catalina.core.StandardContextValve
  hash=0, handle=0x00000000fb045ad8, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb135300, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb2ad568, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb3022b0, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb3050e8, age=1, inYoung=false, inOld=true
・・・
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen.groovy 2790 org.apache.catalina.LifecycleEvent

*** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007fca50019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007fca5001bfc0

org.apache.catalina.LifecycleEvent
  hash=0, handle=0x00000000f82079a8, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000f8207a00, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000f8210470, age=0, inYoung=true, inOld=false
  ・・・
  hash=0, handle=0x00000000f8506568, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000fb003370, age=0, inYoung=false, inOld=true

この結果の正否はともかく、一応は判別できているように見えます。

ちなみに、前回と同様に処理が遅い(重い)点に関しては、Oop を Serviceability Agent APIsun.jvm.hotspot.oops.ObjectHeap で取得するように変更すれば改善できます。

注意点

今回のように JDI の内部で管理している Serviceability Agent API を取り出して使う場合の注意点は以下の通りです。

  • JDI 内部の Serviceability Agent API のクラス(インターフェースも含む)は sun.jvm.hotspot.jdi.SAJDIClassLoader クラスローダーによってロードされる

同じ名称のクラスでもロードするクラスローダーが異なれば別物となりますので、Java で今回のような処理を実装しようとすると、クラスのキャストができずリフレクション等を多用する事になると思います。

また、Groovy でも HeapVisitor 等を使う場合に多少の工夫が必要になります。