Groovy で Cassandra を組み込み実行
Groovy で Apache Cassandra を組み込み実行してみました。
今回のソースは http://github.com/fits/try_samples/tree/master/blog/20170227/
組み込み実行
まずは、設定ファイルを用意しておきます。
今回は実行に必要な最小限の設定を行っています。
embed.conf
cluster_name: 'Test Cluster' listen_address: localhost commitlog_sync: periodic commitlog_sync_period_in_ms: 10000 partitioner: org.apache.cassandra.dht.Murmur3Partitioner endpoint_snitch: SimpleSnitch seed_provider: - class_name: org.apache.cassandra.locator.SimpleSeedProvider parameters: - seeds: "127.0.0.1" # CQL クライアントで接続するために必要 start_native_transport: true
ここで、Apache Cassandra 3.10 では thrift クライアントで接続するためのポート 9160 はデフォルトで有効のようですが、CQL クライアント用のポート 9042 を有効化するには start_native_transport
の設定が必要でした。
ポート番号 | 用途 |
---|---|
9042 | CQL クライアント用 |
9160 | thrift クライアント用 |
Cassandra を組み込み実行する Groovy スクリプトは以下の通りです。
cassandra.config
で設定ファイル、cassandra.storagedir
でデータディレクトリのパスを設定、CassandraDaemon
をインスタンス化して activate
します。(deactivate
を実行すると停止します)
cassandra_embed.groovy
@Grab('org.apache.cassandra:cassandra-all:3.10') import org.apache.cassandra.service.CassandraDaemon def conf = 'embed.yaml' def dir = new File(args[0]) if (!dir.exists()) { dir.mkdirs() } System.setProperty('cassandra.config', conf) System.setProperty('cassandra-foreground', 'true') System.setProperty('cassandra.storagedir', dir.absolutePath) def cassandra = new CassandraDaemon() // 開始 cassandra.activate() System.in.read() // 終了 cassandra.deactivate()
実行結果は以下の通りで、特に問題なく起動できました。
実行
> groovy cassandra_embed.groovy data ・・・ 21:30:46.493 [main] INFO o.apache.cassandra.transport.Server - Starting listening for CQL clients on localhost/127.0.0.1:9042 (unencrypted)... ・・・ 21:30:46.790 [Thread-1] INFO o.a.cassandra.thrift.ThriftServer - Listening for thrift clients...
動作確認
CQL クライアントを使って Cassandra への接続確認を行います。
cqlsh 利用
まずは、Cassandra 3.10 に同梱されている cqlsh
コマンドを使って、キースペースとテーブルを作成しデータ登録を行います。
ここで、cqlsh (本体は cqlsh.py) の実行には Python の実行環境が必要です。
cqlsh による操作結果
> cqlsh ・・・ Connected to Test Cluster at 127.0.0.1:9042. [cqlsh 5.0.1 | Cassandra 3.10 | CQL spec 3.4.4 | Native protocol v4] ・・・ cqlsh> CREATE KEYSPACE sample WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor' : 1}; cqlsh> use sample; cqlsh:sample> CREATE TABLE data (id text PRIMARY KEY, name text, value int); cqlsh:sample> INSERT INTO data (id, name, value) values ('d1', 'sample1', 1); cqlsh:sample> INSERT INTO data (id, name, value) values ('d2', 'sample2', 20); cqlsh:sample> INSERT INTO data (id, name, value) values ('d3', 'sample3', 300); cqlsh:sample> SELECT * FROM data; id | name | value ----+---------+------- d2 | sample2 | 20 d1 | sample1 | 1 d3 | sample3 | 300 (3 rows)
Datastax Java Driver for Apache Cassandra 利用
次に、登録したデータを Datastax Java Driver for Apache Cassandra を使って検索してみます。
netty と jffi モジュールで Error grabbing Grapes -- [download failed: ・・・]
となったので、@GrabExclude を使って回避しています。
client_sample.groovy
@Grapes([ @Grab('com.datastax.cassandra:cassandra-driver-core:3.1.4'), @GrabExclude('io.netty#netty-handler;4.0.37'), @GrabExclude('com.github.jnr#jffi;1.2.10') ]) @Grab('io.netty:netty-all:4.0.44.Final') @Grab('org.slf4j:slf4j-nop:1.7.23') import com.datastax.driver.core.Cluster Cluster.builder().addContactPoint('localhost').build().withCloseable { cluster -> cluster.connect('sample').withCloseable { session -> def res = session.execute('select * from data') res.each { println it } } }
実行結果は以下の通りです。
実行結果
> groovy client_sample.groovy Row[d2, sample2, 20] Row[d1, sample1, 1] Row[d3, sample3, 300]
Groovy で Elasticsearch を組み込み実行
Groovy で Elasticsearch を組み込み実行してみました。
今回のソースは http://github.com/fits/try_samples/tree/master/blog/20170203/
(a) クライアント接続しない場合
まずは、クライアント接続が不可な Elasticsearch を起動して、ドキュメント登録や検索を行ってみます。
ポート番号 | クライアント接続 |
---|---|
9200 (HTTP) | × |
9300 (Transport) | × |
Elasticsearch の組み込み実行は、適切な設定を行った Settings
(もしくは Environment
)を使って Node
を作成し start
を実行するだけです。
path.home
の設定は必須で、指定したパスの data ディレクトリを使用します。(無ければ自動的に作成されます)
transport.type
を local
へ、http.enabled
を false
へ設定すればクライアントの接続を受け付けない状態になります。※
※ クライアント接続を受け付けるためのプラグインを適用していない場合、 このように設定しておかないと実行時にエラーとなります
この場合、Node の client
メソッドで取得した Client
を使ってインデックス等を操作します。
els_local.groovy
@Grab('org.elasticsearch:elasticsearch:5.2.0') // log4j のモジュールが必要(無い場合は NoClassDefFoundError が発生) @Grab('org.apache.logging.log4j:log4j-api:2.8') @Grab('org.apache.logging.log4j:log4j-core:2.8') import org.elasticsearch.common.settings.Settings import org.elasticsearch.node.Node def index = args[0] // インデックス def type = args[1] // タイプ // 設定 def setting = Settings.builder() .put('path.home', '.') // data ディレクトリの配置先を指定 .put('transport.type', 'local') .put('http.enabled', 'false') .build() new Node(setting).withCloseable { node -> // Elasticsearch の実行 node.start() node.client().withCloseable { client -> // インデックスへのドキュメント登録 def r1 = client.prepareIndex(index, type) .setSource('time', System.currentTimeMillis()) .execute() .get() println r1 // 検索結果へ即時反映されなかったので適度に待機 sleep(1000) println '-----' // 検索 def r2 = client.prepareSearch(index) .setTypes(type) .execute() .get() println r2 } }
動作確認
実行結果は以下の通りです。
log4j2 の設定ファイルが見つからない旨のエラーログが出力されていますが、 Elasticsearch の組み込み実行は成功しているようです。
実行結果
> groovy els_local.groovy a1 item ERROR StatusLogger No log4j2 configuration file found. ・・・ IndexResponse[index=a1,type=item,id=AVn6bOp-0Vu_EXj66Fj9,version=1,result=created,shards={"_shards":{"total":2,"successful":1,"failed":0}}] ----- {"took":140,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":1.0,"hits":[{"_index":"a1","_type":"item","_id":"AVn6bOp-0Vu_EXj66Fj9","_score":1.0,"_source":{"time":1485965157491}}]}}
(b) クライアント接続する場合
次に、クライアント接続が可能な Elasticsearch を組み込み実行します。
ポート番号 | クライアント接続 |
---|---|
9200 (HTTP) | ○ |
9300 (Transport) | ○ |
これらのポート番号でクライアント接続を受けるにはプラグインが必要なので、
今回は Netty4Plugin
(transport-netty4-client) を使用しました。
Node の public コンストラクタはプラグインクラスを直接指定できないため、今回は protected コンストラクタ ※ を直接呼び出して Netty4Plugin を適用しています。
※ 第 2引数で Plugin クラスを指定できるようになっている protected Node(Environment environment, Collection<Class<? extends Plugin>> plugins)
なお、close
メソッドの実行有無に関係なく Node は終了してしまうので、System.in.read()
を使って終了を止めています。
els_netty.groovy
@Grab('org.elasticsearch:elasticsearch:5.2.0') @Grab('org.elasticsearch.plugin:transport-netty4-client:5.2.0') @Grab('org.apache.logging.log4j:log4j-api:2.8') @Grab('org.apache.logging.log4j:log4j-core:2.8') import org.elasticsearch.common.settings.Settings import org.elasticsearch.env.Environment import org.elasticsearch.node.Node import org.elasticsearch.transport.Netty4Plugin def setting = Settings.builder() .put('path.home', '.') .build() def env = new Environment(setting) // Netty4Plugin を指定して Node をインスタンス化 new Node(env, [Netty4Plugin]).withCloseable { node -> node.start() println 'started server ...' // 終了するのを止めるための措置 System.in.read() }
動作確認
Elasticsearch を実行します。
Elasticsearch 組み込み実行
> groovy els_netty.groovy ERROR StatusLogger No log4j2 configuration file found. ・・・ started server ...
(1) HTTP 接続(9200 ポート)
curl で 9200 ポートへ接続した結果は以下の通りです。
実行結果(HTTP)
$ curl -s http://localhost:9200/b1/item -d "{\"time\": `date +%s%3N`}" {"_index":"b1","_type":"item","_id":"AVn6rxw5O4vwdZ1ibdCo","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"created":true} $ curl -s http://localhost:9200/b1/item/_search {"took":78,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":1.0,"hits":[{"_index":"b1","_type":"item","_id":"AVn6rxw5O4vwdZ1ibdCo","_score":1.0,"_source":{"time": 1485969495792}}]}}
(2) Transport 接続(9300 ポート)
9300 ポートへ接続して (a) と同等のクライアント処理を実施するスクリプトは以下のようになります。
9300 ポートへ接続するために TransportClient
を使用しています。
els_client.groovy
@Grab('org.elasticsearch.client:transport:5.2.0') @Grab('org.apache.logging.log4j:log4j-api:2.8') @Grab('org.apache.logging.log4j:log4j-core:2.8') import org.elasticsearch.common.settings.Settings import org.elasticsearch.transport.client.PreBuiltTransportClient import org.elasticsearch.common.transport.InetSocketTransportAddress def index = args[0] def type = args[1] def addr = new InetSocketTransportAddress( InetAddress.getLoopbackAddress(), 9300) // TransportClient の生成 def transportClient = new PreBuiltTransportClient(Settings.EMPTY) .addTransportAddress(addr) transportClient.withCloseable { client -> // インデックスへのドキュメント登録 def r1 = client.prepareIndex(index, type) .setSource('time', System.currentTimeMillis()) .execute() .get() println r1 sleep(1000) println '-----' // 検索 def r2 = client.prepareSearch(index) .setTypes(type) .execute() .get() println r2 }
実行結果は以下の通りです。
実行結果(Transport)
> groovy els_client.groovy b2 item ・・・ IndexResponse[index=b2,type=item,id=AVn6tvd3WlzxY0bEAQ4r,version=1,result=created,shards={"_shards":{"total":2,"successful":1,"failed":0}}] ----- {"took":78,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":1.0,"hits":[{"_index":"b2","_type":"item","_id":"AVn6tvd3WlzxY0bEAQ4r","_score":1.0,"_source":{"time":1485970011680}}]}}
備考. HTTP 接続の無効化
9200 ポートの接続だけを無効化するには http.enabled
を false
にします。
ポート番号 | クライアント接続 |
---|---|
9200 (HTTP) | × |
9300 (Transport) | ○ |
els_netty_nohttp.groovy
・・・ def setting = Settings.builder() .put('path.home', '.') .put('http.enabled', 'false') // HTTP 接続(9200 ポート)の無効化 .build() ・・・ new Node(env, [Netty4Plugin]).withCloseable { node -> ・・・ }
Lucene API で Solr と Elasticsearch のインデックスを確認
Groovy で Lucene の API を使用して Solr や Elasticsearch のインデックスの内容を確認してみました。(Lucene 6.2.1 の API を使用)
ソースは http://github.com/fits/try_samples/tree/master/blog/20161024/
(a) ドキュメントの内容を出力
まずは、ドキュメントに属するフィールドの内容を出力する処理です。
DirectoryReader
から Document
を取得し、フィールド IndexableField
の内容を出力しています。
dump_docs.groovy
@Grab('org.apache.lucene:lucene-core:6.2.1') import org.apache.lucene.index.DirectoryReader import org.apache.lucene.store.FSDirectory import java.nio.file.Paths def dir = FSDirectory.open(Paths.get(args[0])) DirectoryReader.open(dir).withCloseable { reader -> println "numDocs = ${reader.numDocs()}" (0..<reader.numDocs()).each { // ドキュメントの取得 def doc = reader.document(it) println "---------- doc: ${it} ----------" // ドキュメント内のフィールドを出力 doc.fields.each { f -> def value = f.binaryValue()? f.binaryValue().utf8ToString(): f.stringValue() println "<field> name=${f.name}, value=${value}, class=${f.class}" } } }
(b) Term の内容を出力
インデックス内のフィールド情報と Term を出力する処理です。 Term は基本的な検索の単位となっており、Term の内容を見れば単語の分割状況を確認できます。
これらの情報を取得するには LeafReader
を使います。
Term の内容 BytesRef
は TermsEnum
から取得できます。
LeafReader から terms
メソッドで該当フィールドの Terms
を取得し、iterator
メソッドで TermsEnum を取得します。
dump_terms.groovy
@Grab('org.apache.lucene:lucene-core:6.2.1') import org.apache.lucene.index.DirectoryReader import org.apache.lucene.store.FSDirectory import java.nio.file.Paths def dir = FSDirectory.open(Paths.get(args[0])) DirectoryReader.open(dir).withCloseable { reader -> reader.leaves().each { ctx -> // LeafReader の取得 def leafReader = ctx.reader() println "---------- leaf: ${leafReader} ----------" // フィールド情報の出力 leafReader.getFieldInfos().each { fi -> println "<fieldInfo> name: ${fi.name}, valueType: ${fi.docValuesType}, indexOptions: ${fi.indexOptions}" } leafReader.fields().each { name -> // 指定のフィールド名に対する TermsEnum を取得 def termsEnum = leafReader.terms(name).iterator() println '' println "===== <term> name=${name} =====" try { while(termsEnum.next() != null) { // Term の内容を出力 println "term=${termsEnum.term().utf8ToString()}, freq=${termsEnum.docFreq()}" } } catch(e) { } } } }
動作確認
Lucene のバージョンが以下のようになっていたので、今回は Solr 6.2.1 と Elasticsearch 5.0.0 RC1 のインデックス内容を確認してみます。
プロダクト | 使用している Lucene のバージョン |
---|---|
Solr 6.2.1 | Lucene 6.2.1 |
Elasticsearch 2.4.1 | Lucene 5.5.2 |
Elasticsearch 5.0.0 RC1 | Lucene 6.2.0 |
(1) Solr 6.2.1
Solr 6.2.1 のインデックスから確認してみます。
準備
インデックスを作成してドキュメントを登録しておきます。
1. Solr 起動とインデックスの作成
> solr start ・・・ > solr create -c sample { "responseHeader":{ "status":0, "QTime":9197}, "core":"sample"}
2. スキーマの登録
schema.json
{ "add-field": { "name": "title", "type": "string" }, "add-field": { "name": "num", "type": "int" }, "add-field": { "name": "rdate", "type": "date" } }
スキーマ登録
$ curl -s http://localhost:8983/solr/sample/schema --data-binary @schema.json { "responseHeader":{ "status":0, "QTime":554}}
3. ドキュメントの登録
data1.json
{ "title": "item1", "num": 11, "rdate": "2016-10-20T13:45:00Z" }
ドキュメント登録
$ curl -s http://localhost:8983/solr/sample/update/json/docs --data-binary @data1.json {"responseHeader":{"status":0,"QTime":199}}
ちなみに、コミットしなくてもインデックスファイルには反映されるようです。
インデックスの内容確認
それでは、インデックスの内容を確認します。
該当するインデックスのディレクトリ(例. C:\solr-6.2.1\server\solr\sample\data\index
)を引数に指定して実行します。
(a) ドキュメントの内容
> groovy dump_docs.groovy C:\solr-6.2.1\server\solr\sample\data\index numDocs = 1 ---------- doc: 0 ---------- <field> name=title, value=item1, class=class org.apache.lucene.document.StoredField <field> name=num, value=11, class=class org.apache.lucene.document.StoredField <field> name=rdate, value=1476971100000, class=class org.apache.lucene.document.StoredField <field> name=id, value=2b1080dd-0cd3-43c6-a3ff-ab618ad00113, class=class org.apache.lucene.document.StoredField
(b) Term の内容
> groovy dump_terms.groovy C:\solr-6.2.1\server\solr\sample\data\index ---------- leaf: _0(6.2.1):C1 ---------- <fieldInfo> name: title, valueType: SORTED, indexOptions: DOCS <fieldInfo> name: _text_, valueType: NONE, indexOptions: DOCS_AND_FREQS_AND_POSITIONS <fieldInfo> name: num, valueType: NUMERIC, indexOptions: DOCS <fieldInfo> name: rdate, valueType: NUMERIC, indexOptions: DOCS <fieldInfo> name: id, valueType: SORTED, indexOptions: DOCS <fieldInfo> name: _version_, valueType: NUMERIC, indexOptions: DOCS ===== <term> name=_text_ ===== term=00, freq=1 term=0cd3, freq=1 term=11, freq=1 term=13, freq=1 term=1548710288432300032, freq=1 term=20, freq=1 term=2016, freq=1 term=2b1080dd, freq=1 term=43c6, freq=1 term=45, freq=1 term=a3ff, freq=1 term=ab618ad00113, freq=1 term=item1, freq=1 term=oct, freq=1 term=thu, freq=1 term=utc, freq=1 ===== <term> name=_version_ ===== term= ?yTP , freq=1 ===== <term> name=id ===== term=2b1080dd-0cd3-43c6-a3ff-ab618ad00113, freq=1 ===== <term> name=num ===== term= , freq=1 ===== <term> name=rdate ===== term= *~Yn`, freq=1 ===== <term> name=title ===== term=item1, freq=1
version・num・rdate の値が文字化けしているように見えますが、これは org.apache.lucene.util.LegacyNumericUtils.intToPrefixCoded()
メソッド等で処理されてバイナリデータとなっているためです。
実際の値を復元するには LegacyNumericUtils.prefixCodedToInt()
等を適用する必要があるようです。
なお、LegacyNumericUtils クラスは Lucene 6.2.1 API で deprecated となっていますが、Solr は未だ使っているようです。
(2) Elasticsearch 5.0.0 RC1
次は Elasticsearch です。
準備
インデックスを作成してドキュメントを登録しておきます。
1. Elasticsearch 起動
> elasticsearch ・・・
2. インデックスの作成とスキーマ登録
schema.json
{ "mappings": { "data": { "properties": { "title": { "type": "string", "index": "not_analyzed" }, "num": { "type": "integer" }, "rdate": { "type": "date" } } } } }
インデックス作成とスキーマ登録
$ curl -s -XPUT http://localhost:9200/sample --data-binary @schema.json {"acknowledged":true,"shards_acknowledged":true}
3. ドキュメントの登録
data1.json
{ "title": "item1", "num": 11, "rdate": "2016-10-20T13:45:00Z" }
ドキュメント登録
$ curl -s http://localhost:9200/sample/data --data-binary @data1.json {"_index":"sample","_type":"data","_id":"AVfwTjEQFnFWQdd5V9p5","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"created":true}
なお、すぐにインデックスファイルへ反映されない場合は flush を実施します。
flush 例
$ curl -s http://localhost:9200/sample/_flush
インデックスの内容確認
インデックスの内容を確認します。
Elasticsearch の場合はデフォルトで複数の shard に分かれているため、ドキュメントを登録した shard を確認しておきます。
shard の確認
$ curl -s http://localhost:9200/_cat/shards/sample?v index shard prirep state docs store ip node sample 1 p STARTED 0 130b 127.0.0.1 iUp_FE_ ・・・ sample 4 p STARTED 1 3.8kb 127.0.0.1 iUp_FE_ sample 4 r UNASSIGNED sample 0 p STARTED 0 130b 127.0.0.1 iUp_FE_ sample 0 r UNASSIGNED
shard 4 にドキュメントが登録されています。
Elasticsearch のインデックスディレクトリは data/nodes/<ノード番号>/indices/<インデックスのuuid>/<shard番号>/index
となっているようで、今回は data\nodes\0\indices\QBXMjcCFSWy26Gow1Y9ItQ\4\index
でした。(インデックスの uuid は QBXMjcCFSWy26Gow1Y9ItQ)
(a) ドキュメントの内容
> groovy dump_docs.groovy C:\elasticsearch-5.0.0-rc1\data\nodes\0\indices\QBXMjcCFSWy26Gow1Y9ItQ\4\index numDocs = 1 ---------- doc: 0 ---------- <field> name=_source, value={ "title": "item1", "num": 11, "rdate": "2016-10-20T13:45:00Z" } , class=class org.apache.lucene.document.StoredField <field> name=_uid, value=data#AVfwTjEQFnFWQdd5V9p5, class=class org.apache.lucene.document.StoredField
(b) Term の内容
> groovy dump_terms.groovy C:\elasticsearch-5.0.0-rc1\data\nodes\0\indices\QBXMjcCFSWy26Gow1Y9ItQ\4\index ---------- leaf: _0(6.2.0):c1 ---------- <fieldInfo> name: _source, valueType: NONE, indexOptions: NONE <fieldInfo> name: _type, valueType: SORTED_SET, indexOptions: DOCS <fieldInfo> name: _uid, valueType: NONE, indexOptions: DOCS <fieldInfo> name: _version, valueType: NUMERIC, indexOptions: NONE <fieldInfo> name: title, valueType: SORTED_SET, indexOptions: DOCS <fieldInfo> name: num, valueType: SORTED_NUMERIC, indexOptions: NONE <fieldInfo> name: rdate, valueType: SORTED_NUMERIC, indexOptions: NONE <fieldInfo> name: _all, valueType: NONE, indexOptions: DOCS_AND_FREQS_AND_POSITIONS <fieldInfo> name: _field_names, valueType: NONE, indexOptions: DOCS ===== <term> name=_all ===== term=00z, freq=1 term=10, freq=1 term=11, freq=1 term=2016, freq=1 term=20t13, freq=1 term=45, freq=1 term=item1, freq=1 ===== <term> name=_field_names ===== term=_all, freq=1 term=_source, freq=1 term=_type, freq=1 term=_uid, freq=1 term=_version, freq=1 term=num, freq=1 term=rdate, freq=1 term=title, freq=1 ===== <term> name=_type ===== term=data, freq=1 ===== <term> name=_uid ===== term=data#AVfwTjEQFnFWQdd5V9p5, freq=1 ===== <term> name=title ===== term=item1, freq=1
Solr とは、かなり違った結果になっています。
Groovy の @Grab で Spark Framework を実行
Spark Framework - A tiny Java web framework を Groovy の @Grab
を使って実行してみました。
- Spark Framework 2.5
- Groovy 2.4.7
今回のソースは http://github.com/fits/try_samples/tree/master/blog/20160801/
はじめに
以下のように @Grab を使った Spark の Groovy スクリプトを groovy コマンドで実行し、Web クライアントでアクセスしてみると、java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders
エラーが発生してしまいました。
now.groovy
@Grab('com.sparkjava:spark-core:2.5') @Grab('org.slf4j:slf4j-simple:1.7.21') import static spark.Spark.* get('/now') { req, res -> new Date().format('yyyy/MM/dd HH:mm:ss') }
groovy コマンドで上記スクリプトを実行。
実行例
> groovy now.groovy ・・・ [Thread-1] INFO org.eclipse.jetty.server.Server - Started @3213ms
/now
へアクセスすると、以下のようなエラーが発生。
エラー例(クライアント側)
$ curl http://localhost:4567/now <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 500 </title> </head> <body> <h2>HTTP ERROR: 500</h2> <p>Problem accessing /now. Reason: <pre> java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders(Ljava/lang/String;)Ljava/util/Collection;</pre></p> <hr /><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.3.6.v20151106</a><hr/> </body> </html>
エラー例(サーバー側)
> groovy now.groovy ・・・ java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders(Ljava/lang/String;)Ljava/util/Collection; at spark.utils.GzipUtils.checkAndWrap(GzipUtils.java:67) at spark.http.matching.Body.serializeTo(Body.java:69) at spark.http.matching.MatcherFilter.doFilter(MatcherFilter.java:158) at spark.embeddedserver.jetty.JettyHandler.doHandle(JettyHandler.java:50) at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:189) ・・・
このエラーは、groovy コマンドの実行時に $GROOVY_HOME/lib/servlet-api-2.4.jar (Servlet 2.4) を先にロードする事が原因で発生しているようです。
HttpServletResponse.getHeaders
は Servlet 3.0 から追加されたメソッドのため、Servlet 2.4 の API が適用されていると該当メソッドが存在せず NoSuchMethodError
になります。
回避策
エラー原因は、groovy コマンドが $GROOVY_HOME/lib/servlet-api-2.4.jar をロードする事なので、Gradle 等で実行すれば上記のようなエラーは発生しません。
しかし、今回は groovy コマンドで実行する場合の回避策をいくつか検討してみました。
- (a) -cp オプションを使用
- (b) Groovy 設定ファイル (groovy-starter.conf) を編集
- (c) $GROOVY_HOME/lib/servlet-api-2.4.jar を削除
(a) と (b) は Servlet 2.4 より先に Servlet 3.1 を適用させる方法で、(c) は servlet-api-2.4.jar をロードさせない方法です。
(a) -cp オプションを使用
Servlet 3.1 の JAR (下記では javax.servlet-api-3.1.0.jar)を入手し、groovy コマンドの -cp オプションでその JAR を指定して実行します。
こうする事でエラーは出なくなりました。
実行例(サーバー)
> groovy -cp lib_a/javax.servlet-api-3.1.0.jar now.groovy ・・・
実行例(クライアント)
$ curl http://localhost:4567/now 2016/07/31 20:46:42
(b) Groovy 設定ファイル (groovy-starter.conf) を編集
$GROOVY_HOME/lib/servlet-api-2.4.jar は groovy-starter.conf の設定 (load !{groovy.home}/lib/*.jar
) によりロードされています。
つまり、$GROOVY_HOME/lib/*.jar よりも先に、別のディレクトリ内の JAR をロードするように groovy-starter.conf を書き換え、そのディレクトリへ Servlet 3.1 の JAR を配置すれば、(a) と同様に回避できるはずです。
groovy-starter.conf 変更例
# 以下を追加 load lib_a/*.jar # load required libraries load !{groovy.home}/lib/*.jar ・・・
lib_a/javax.servlet-api-3.1.0.jar を配置して実行すると、エラーは出なくなりました。
実行例(サーバー)
> groovy now.groovy ・・・
実行例(クライアント)
$ curl http://localhost:4567/now 2016/07/31 20:48:05
備考. Groovy 設定ファイル (groovy-starter.conf) の指定方法
groovy-starter.conf を直接書き換えるのはイマイチなので、任意の Groovy 設定ファイルを使いたいところです。
startGroovy
スクリプトの内容を見ると GROOVY_CONF
環境変数で指定できそうです。
ただし、startGroovy.bat
の方は今のところ GROOVY_CONF
環境変数を考慮しておらず、Windows 環境 (groovy.bat を使う場合) では使えません。
そこで今回は、下記 postinit.bat を用意し、Windows 環境で GROOVY_CONF
に対応してみました。
%USERPROFILE%/.groovy/postinit.bat の例
if not "%GROOVY_CONF%" == "" ( set GROOVY_OPTS=%GROOVY_OPTS% -Dgroovy.starter.conf="%GROOVY_CONF%" set STARTER_CONF=%GROOVY_CONF% )
上記を配置した後、以下のように実行します。
GROOVY_CONF 環境変数の利用例 (Windows)
> set GROOVY_CONF=groovy-starter_custom.conf > groovy now.groovy ・・・
(c) $GROOVY_HOME/lib/servlet-api-2.4.jar を削除
$GROOVY_HOME/lib/servlet-api-2.4.jar を一時的に削除(拡張子を変える等)して実行します。
備考. Gradle で実行する場合
Gradle で実行する場合は、src/main/groovy/now.groovy を配置して(@Grab の箇所は削除しておく)、以下のような build.gradle を使います。
build.gradle 例
apply plugin: 'groovy' apply plugin: 'application' repositories { jcenter() } dependencies { compile 'com.sparkjava:spark-core:2.5' compile 'org.codehaus.groovy:groovy:2.4.7' runtime 'org.slf4j:slf4j-simple:1.7.21' } mainClassName = 'now'
実行例
> gradle run ・・・ :run ・・・ [Thread-1] INFO org.eclipse.jetty.server.Server - Started @1035ms
JMX で Java Flight Recorder (JFR) を実行する
Java Flight Recorder (JFR) は Java Mission Control (jmc) や jcmd コマンドから実行できますが、今回は以下の MBean を使って JMX から実行してみます。
- com.sun.management:type=DiagnosticCommand
この MBean は以下のような操作を備えており(戻り値は全て String)、jcmd コマンドと同じ事ができるようです。
- jfrCheck
- jfrDump
- jfrStop
- jfrStart
- vmCheckCommercialFeatures
- vmCommandLine
- vmFlags
- vmSystemProperties
- vmUnlockCommercialFeatures
- vmUptime
- vmVersion
- vmNativeMemory
- gcRotateLog
- gcRun
- gcRunFinalization
- gcClassHistogram
- gcClassStats
- threadPrint
(a) JFR の実行
JMX を使う方法はいくつかありますが、今回は Attach API でローカルの VM へアタッチし、startLocalManagementAgent
メソッドで JMX エージェントを適用する方法を用いました。
DiagnosticCommand には java.lang.management.ThreadMXBean のようなラッパーが用意されていないようなので GroovyMBean
を使う事にします。
jfrStart
の引数は jcmd コマンドと同じものを String 配列にして渡すだけのようです。(jfrStart 以外も基本的に同じ)
また、JFR の実行には Commercial Features のアンロックが必要です。
jfr_run.groovy
import com.sun.tools.attach.VirtualMachine import javax.management.remote.JMXConnectorFactory import javax.management.remote.JMXServiceURL def pid = args[0] def duration = args[1] def fileName = args[2] // 指定の JVM プロセスへアタッチ def vm = VirtualMachine.attach(pid) try { // JMX エージェントを適用 def jmxuri = vm.startLocalManagementAgent() JMXConnectorFactory.connect(new JMXServiceURL(jmxuri)).withCloseable { def server = it.getMBeanServerConnection() // MBean の取得 def bean = new GroovyMBean(server, 'com.sun.management:type=DiagnosticCommand') // Commercial Features のアンロック (JFR の実行に必要) println bean.vmUnlockCommercialFeatures() // JFR の開始 println bean.jfrStart([ "duration=${duration}", "filename=${fileName}", 'delay=10s' ] as String[]) } } finally { vm.detach() }
実行例
apache-tomcat-9.0.0.M4 へ適用してみます。
Tomcat 実行
> startup
以下の環境で実行しました。
- Groovy 2.4.6
- Java SE 8u92 64bit版
JFR 実行
> jps 4576 Jps 2924 Bootstrap > groovy jfr_run.groovy 2924 1m sample1.jfr Commercial Features now unlocked. Recording 1 scheduled to start in 10 s. The result will be written to: C:\・・・\apache-tomcat-9.0.0.M4\apache-tomcat-9.0.0.M4\bin\sample1.jfr
jfrStart は JFR の完了を待たずに戻り値を返すため、JFR の実行状況は別途確認する事になります。
出力結果 Recording 1 scheduled
の 1
が recoding の番号で、この番号を使って JFR の状態を確認できます。
ファイル名を相対パスで指定すると対象プロセスのカレントディレクトリへ出力されるようです。 (今回は Tomcat の bin ディレクトリへ出力されました)
(b) JFR の状態確認
JFR の実行状況を確認するには jfrCheck
を使います。
下記では recording の番号を指定し、該当する JFR の実行状況を出力しています。
jfrCheck の引数が null の場合は全ての JFR 実行状態を取得するようです。
jfr_check.groovy
import com.sun.tools.attach.VirtualMachine import javax.management.remote.JMXConnectorFactory import javax.management.remote.JMXServiceURL def pid = args[0] String[] params = (args.length > 1)? ["recording=${args[1]}"]: null def vm = VirtualMachine.attach(pid) try { def jmxuri = vm.startLocalManagementAgent() JMXConnectorFactory.connect(new JMXServiceURL(jmxuri)).withCloseable { def server = it.getMBeanServerConnection() def bean = new GroovyMBean(server, 'com.sun.management:type=DiagnosticCommand') println bean.jfrCheck(params) } } finally { vm.detach() }
実行例
recording 番号(下記では 1
)を指定して実行します。
実行例1 (JFR 実行中)
> groovy jfr_check.groovy 2924 1 Recording: recording=1 name="sample1.jfr" duration=1m filename="sample1.jfr" compress=false (running)
実行例2 (JFR 完了後)
> groovy jfr_check.groovy 2924 1 Recording: recording=1 name="sample1.jfr" duration=1m filename="sample1.jfr" compress=false (stopped)
今回作成したサンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20160519/
JDI でオブジェクトの世代(Young・Old)を判別する2
前回 の処理を sun.jvm.hotspot.oops.ObjectHeap
を使って高速化してみたいと思います。(世代の判別方法などは前回と同じ)
使用した環境は前回と同じです。
- Groovy 2.4.6
- Java SE 8u92 64bit版 (JDK)
ソースは http://github.com/fits/try_samples/tree/master/blog/20160506/
ObjectHeap で Oop を取得
ObjectReference の代わりに、sun.jvm.hotspot.oops.ObjectHeap
の iterate(HeapVisitor)
メソッドを使えば Oop を取得できます。
今回のような方法では、以下の理由で iterate メソッドの引数へ SAJDIClassLoader がロードした sun.jvm.hotspot.oops.HeapVisitor
インターフェースの実装オブジェクトを与える必要があります。
- JDI の内部で管理している Serviceability Agent API は
sun.jvm.hotspot.jdi.SAJDIClassLoader
によってロードされている
下記サンプルでは SAJDIClassLoader がロードした HeapVisitor を入手し、asType
を使って実装オブジェクトを作成しています。
また、HeapVisitor の doObj
で false を返すと処理を継続し、true を返すと中止 ※ するようです。
※ 厳密には、 対象としている Address 範囲の while ループを break するだけで、 その外側の(liveRegions に対する)for ループは継続するようです (ObjectHeap の iterateLiveRegions メソッドのソース参照)
なお、ObjectHeap は sun.jvm.hotspot.jdi.VirtualMachineImpl
から saObjectHeap()
で取得するか、sun.jvm.hotspot.runtime.VM
から取得します。
check_gen2.groovy
import com.sun.jdi.Bootstrap def pid = args[0] def prefix = (args.length > 1)? 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) // 世代の判定処理を返す generation = { heap -> def hasYoungGen = heap.metaClass.getMetaMethod('youngGen') != null [ young: hasYoungGen? heap.youngGen(): heap.getGen(0), old: hasYoungGen? heap.oldGen(): heap.getGen(1) ] } try { def uv = vm.saVM.universe def gen = generation(uv.heap()) def youngGen = gen.young def oldGen = gen.old println "*** youngGen=${youngGen}, oldGen=${oldGen}" println '' def objHeap = vm.saObjectHeap() // 以下でも可 //def objHeap = vm.saVM.objectHeap // SAJDIClassLoader がロードした HeapVisitor インターフェースを取得 def heapVisitorCls = uv.class.classLoader.loadClass('sun.jvm.hotspot.oops.HeapVisitor') // SAJDIClassLoader がロードした HeapVisitor インターフェースを実装 def heapVisitor = [ prologue: { size -> }, epilogue: {}, doObj: { oop -> def clsName = oop.klass.name.asString() if (clsName.startsWith(prefix)) { 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 "class=${clsName}, hash=${identityHash}, handle=${oop.handle}, age=${age}, inYoung=${inYoung}, inOld=${inOld}" } // 処理を継続する場合は false を返す false } ].asType(heapVisitorCls) objHeap.iterate(heapVisitor) } finally { vm.dispose() }
動作確認
前回と同じように、実行中の apache-tomcat-9.0.0.M4 へ適用してみました。
前回と異なり、クラス名が '/' で区切られている点に注意
実行例1 (Windows の場合)
> jps 3604 Bootstrap 4516 Jps
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen2.groovy 3604 org/apache/catalina/core/StandardContext *** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002149ab0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002149b40 class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000c3a577d0, age=1, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000c3a633d8, age=0, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000c3a63ef0, age=1, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000ebc46da0, age=0, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContext, hash=6f2d2815, handle=0x00000000eddfeaa0, age=1, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContext, hash=21f2e66b, handle=0x00000000eddff238, age=3, inYoung=true, inOld=false ・・・
実行例2 (Linux の場合)
$ jps 2778 Jps 2766 Bootstrap
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen2.groovy 2766 org/apache/catalina/core/StandardContext *** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007f0760019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007f076001bfc0 class=org/apache/catalina/core/StandardContext, hash=497fe2c4, handle=0x00000000f821bf90, age=0, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000f821c5d8, age=0, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000f821ca60, age=0, inYoung=true, inOld=false ・・・ class=org/apache/catalina/core/StandardContext, hash=5478de1a, handle=0x00000000fb12b310, age=1, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000fb12f6b0, age=0, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000fb131a80, age=0, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000fb1398b0, age=0, inYoung=false, inOld=true
JDI でオブジェクトの世代(Young・Old)を判別する
前回、オブジェクトの age を取得しましたが、同様の方法で今回はオブジェクトが Young 世代(New 領域)と Old 世代(Old 領域) のどちらに割り当てられているかを判別してみたいと思います。 (ただし、結果の正否は確認できていません)
使用した環境は前回と同じです。
- Groovy 2.4.6
- Java SE 8u92 64bit版 (JDK)
ソースは http://github.com/fits/try_samples/tree/master/blog/20160430/
Young・Old 世代の判別
さて、Young・Old の判別方法ですが。
Serviceability Agent API を見てみると sun.jvm.hotspot.gc_implementation.parallelScavenge
パッケージに PSYoungGen
と PSOldGen
というクラスがあり、isIn(Address)
メソッドで判定できそうです。
更に PSYoungGen と PSOldGen は sun.jvm.hotspot.gc_implementation.parallelScavenge.ParallelScavengeHeap
から取得できます。
Address (sun.jvm.hotspot.debugger
パッケージ所属) は sun.jvm.hotspot.oops.Oop
の getHandle()
か 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 API の sun.jvm.hotspot.oops.ObjectHeap
で取得するように変更すれば改善できます。
注意点
今回のように JDI の内部で管理している Serviceability Agent API を取り出して使う場合の注意点は以下の通りです。
- JDI 内部の Serviceability Agent API のクラス(インターフェースも含む)は
sun.jvm.hotspot.jdi.SAJDIClassLoader
クラスローダーによってロードされる
同じ名称のクラスでもロードするクラスローダーが異なれば別物となりますので、Java で今回のような処理を実装しようとすると、クラスのキャストができずリフレクション等を多用する事になると思います。
また、Groovy でも HeapVisitor 等を使う場合に多少の工夫が必要になります。