Perm領域不足

"java.lang.OutOfMemoryError"が発生しているシステムの相談を受けたのだが、調べてみると純粋にヒープが足りないのではなく、通常は早い段階でサチるはずのPerm領域が原因で発生していることが解った。

どうせプリコンパイルしたJSPクラスが大量にロードされているんじゃないかと思ったのだが、そうではなく、プロファイルの結果では以下のクラスのインスタンスが原因ではないか、という結論になった。

sun.reflect.GeneratedConstructorAccessorXXX
sun.reflect.GeneratedMethodAccessorXXX

こんなクラス見たことないが、どうやらリフレクションでJVMが一時的に生成するものらしい。しかし、なぜリフレクションの度にこいつらが生成されるのか皆目見当がつかない。

いずれにせよ動的に生成されるクラスであり、ロードされて次第にPerm領域を圧迫していくのでたちが悪い。(強制FullGCを実行することでアンロードすることは可能)
この動的プログラミング真っ盛りの時代にリフレクションを使うなという時代錯誤的な対応はしたくないが、かといって解決策も見つかっていない。困った。

追記:
以前、Attatch APIに言及した際にコメント頂いたこともあるskさんにPerm領域のチューニングに関しての記事へのリンクを教えて頂いた。

Permanent領域のチューニング/第6回 HotSpot VMの特長を知る 2/2 - @ITjavaパフォーマンスチューニング

症状からして

デフォルトクラスローダでロードしたクラスや、ロードに使用したクラスローダへの参照が残っているクラスの場合、フルGCが実行されてもその対象とはなりません

ロードされているクラスはsun.reflectパッケージなので、この辺が怪しそうだ。

WebRowSetImpl

いろいろ調べてWebRowSetReader/Writer以前に、WebRowSetImplつまりSunのRI実装は駄目駄目だということが解った。
自作のWebRowSetReaderをテストしているのだが、生成されたXMLのカレント行に相当するXMLを要素を読み込み、行を挿入しようとすると、

java.sql.SQLException: 行の挿入に失敗
	at com.sun.rowset.CachedRowSetImpl.insertRow(CachedRowSetImpl.java:5444)

と、例外が出るのだが詳細情報にアクセスすることができないので原因を調べることができない。

試しに実装クラスにcom.sun.rowset.WebRowSetImplではなく、Oracle JDBCドライバで提供されている実装クラスoracle.jdbc.rowset.OracleWebRowSetに変えて同様にテストを実行してみると今度は

java.sql.SQLException: 行のすべての列が設定されていません
	at oracle.jdbc.driver.SQLStateMapping.newSQLException(SQLStateMapping.java:70)
	at oracle.jdbc.driver.DatabaseError.newSQLException(DatabaseError.java:112)
	at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:173)
	at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:229)
	at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:403)
	at oracle.jdbc.rowset.OracleCachedRowSet.insertRow(OracleCachedRowSet.java:1978)

と例外が変わる。内部の実装が違うので一概には言えないが、仮に原因が同じだとしてOracleの実装は例外が10倍は具体的だ。
暫くはOracleの実装を使ってテストしたほうが良いかもしれない。

同じ「非接続型データベースアクセスコンポーネント」として、.NETで相当するのはADO.NETのDataTableおよびDataSetということになるのだが、その完成度は段違いだ。

WebRowSetWriter/Reader

最小限の構成(データソース名+SQL文のみ)でサーバに送りつけて、結果だけを貰うことができない等、仕様に不満があるのと、Sunの参照実装であるためソースコードも見ることができないので、結局自分で書くことにした。※1
スタックトレースから見るにSAXパーサを使っているようだが、全面的にStAX(Java6で追加されたプルパーサ)で組みなおすことにする。SAXはソースコードの見通しが悪くて嫌いなのだ。(DOMはもっと嫌いだが) ※2


※1 ぶっちゃけSunの実装はバギーだと思う。昔からそうだがJSDKは使用するユーザ数の少ないクラスはバグも多い。
※2 要素数の多いXMLをパースするSAXコンテントハンドラは、エディタ等で追う場合にウインドウ分割するか二つのウインドウが欲しくなることが多い。あと、現在の状態をプッシュしておく必要があったりするのも嫌だ。

どこを直すべきか

Tomcat6(6.0.10)におけるHttp1.1系ProcessorクラスのHTTPレスポンスを返す際の前処理では、以下のようにHTTP1.1で且つKeepAliveが指定されている場合は問答無用に"Transfer-Encoding"を"Chunked"に設定している。

if (entityBody && http11 && keepAlive) {
    outputBuffer.addActiveFilter
        (outputFilters[Constants.CHUNKED_FILTER]);
    contentDelimitation = true;
    headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
} else {
    outputBuffer.addActiveFilter
        (outputFilters[Constants.IDENTITY_FILTER]);
}

"Transfer-Encoding : Chunked"はそれをサポートしていないクライアント(最近では滅多に無いが、プロプライエタリな、ソケットから起こしたクライアントだと結構あったりする)は正常に処理できない。

対処としては

1. Tomcat6に独自でパッチを充てる(特定の条件下でChunkedを無効にする)
2. クライアント側にChunkedのサポートを追加する

当然2.案が真っ当だと思うだろうが、担当者によって意見が分かれるのがここだ。

HTTPによるリモートDBとの同期(その5)

WebRowSet、問題山積だったが動かすことができた。

昨日書いた通りだが、内部RowSetMetaDataインタフェースを空にしている状態(まだ一度もDBにアクセスしていない状態に相当)ではRowSetシリアライズ/デシリアライズができない(例外が発生)

仕方が無いので、クライアント側でのWebRowSetインスタンスの生成は諦め、データソース名とSQL文を普通のHTTP POSTなパラメタとして送信し、受けたサーバ側でWebRowSetのインスタンスを生成した後にクライアントに返すようにした。

  • クライアント側からはデータソース名とSQL文だけをHTTP POSTで送信
sql=SELECT * FROM Hoge.FooTable&datasource=HogeDataSource
  • サーバ側ではパラメタを使ってWebRowSetを生成しクライアントに返す
WebRowSetImpl rowSet = new WebRowSetImpl();
rowSet.setDataSourceName(req.getParameter("datasource"));
rowSet.setCommand(req.getParameter("sql"));
〜
rowSet.execute();
rowSet.writeXml(new OutputStreamWriter(resp.getOutputStream(), encoding));

この方法でなんとか動いたが、クライアントから見て同じURLに対して送信するMIMEタイプが一度目(application/x-www-form-urlencoded)と二度目(text/xml)で違うのが非常に気に入らない。
まあ、そもそもWebRowSetImplはrt.jarに格納されているもののSunのRI実装なので、自分で最適な実装を用意するのが筋なのかもしれない。