Deletions are marked like this. | Additions are marked like this. |
Line 69: | Line 69: |
== 改良結果 == 具体的な改良方法を示す前に、この改良の結果は以下の通り。 改良前は、数ページの遷移ですぐにデフォルトのヒープ領域64Mを使い切ってしまうが(メモリ最大値を引き上げるとその分だけ使用してしまう)、改良後は30M以下の使用にとどまっている。 {{attachment:gcviewer2.png}} |
Wicketアプリが重くて遅い
Wicketで作成したアプリが重くて遅く、とても実用にならない場合がある。これはどんな場合かというと、
- アプリケーションをスタンドアロンのjarとしてまとめ、それを実行したとき。つまり、アプリそれ自体に例えばJettyを内蔵させてスタンドアロンのアプリケーションとして実行した場合。
確認していないが、おそらくwarファイルの場合はあまり問題が出ないものと思われる(遅くなる可能性があるが)。また、例えばEclipse上でのそのアプリを開発中の場合は問題が顕在化しない。これはなぜかというと。
- プログラムをjarファイルの形にまとめた時にだけ問題が現れる。
からである。warファイルも同じようなものであるが、サーブレットコンテナ上で実行される場合は、それが展開された形になる(たしか)ので、問題が現れない(後述するように、実際は若干問題があるのだが、気がつかない)。
現象
以下は、あるアプリを数分間動作させ、数枚のページ遷移を実行したものである(VM引数として-Xloggcをつけ、gcviewerで表示)。 すぐにデフォルトの64Mを使いきってしまうことがわかる。もちろんアプリの動作は非常に重く、操作を続行するとメモリ不足が起こる場合もある。
また、ここには示さないが、このときの状態をEclipse Memory Anlyzerを使って観察してみると、 「sun.net.www.protocol.jar.URLJarFile」というオブジェクト、あるいはそのfinalizeをするためのFinalizerに埋め尽くされていることがわかる。
http://www.docjar.com/html/api/sun/net/www/protocol/jar/URLJarFile.java.html
何らかの理由でURLJarFileというオブジェクトが大量発生するのだが、このクラスにはfinalizeが使われている。finalize付のオブジェクトの場合、そう簡単に廃棄されてはくれない。一説によると、二回のGCが必要とのことである。
このため、不要になってもメモリ上に残ってしまいアプリのメモリを圧迫する。また、そもそもこのようにメモリを使うオブジェクトを大量に作成すること自体が問題である。
原因
Wicket自体のコードをまだ完全には追いきれていないのだが、これはWicketがリソースを取得する際に発生する。ここでリソースと呼ぶのは、主にはXXXPage.javaに対応するXXXPage.htmlというファイルである。
これらのファイルが、OSのファイルシステム中にある場合(warファイルが展開された状態や、Eclipse上での開発途中)では、Wicketは単純にそのファイルを読み込む。これに対して、Jarファイルの中に埋め込まれている場合には、Wicketはそのjarファイルをまるまるメモリにロードしてしまうようである。その際に、sun.net.www.protocol.jar.URLJarFileを使用している模様である。
もちろん、これはWicketが意図的にやっていることではなく、Wicketの作業の途中でJava-APIを呼び出すと結局、個々のリソース取得のたびに「sun.net.www.protocol.jar.URLJarFile」が使用されてしまうようである。
また、これは開発者側が作成したXXXPage.htmlというファイルに限らず、Wicketの配布jarファイル中にあるFeedbackPanel.htmlなどのリソースにも適用されてしまう。したがって、仮に開発アプリにhtml等のリソースが一つもなくとも、あるいは開発アプリをjarにまとめなくとも、wicketの配布jarをそのまま使う限りはこの問題が必ず発生する。
対策の方針
問題の根本は、
- 一つのリソースが要求されたとき、そのリソースが存在するjarファイルがまるまる読み込まれてしまう
ことにある。これを避けるためには、Wicketのリソース取得部分を変更する。
WicketはResourceStreamLoaderを使ってリソースを取得するが、これは差し替え可能である。自前のApplicationクラスのinit()の中で以下を記述する。
getResourceSettings().setResourceStreamLocator(new MyResourceStreamLocator());
MyResourceStreamLocatorを新規作成し、この中でWicketの要求するリソースストリームを返すようにする。 これを擬似的なコードで示す。
public class MyResourceStreamLocator extends ResourceStreamLocator { public IResourceStream locate(final Class<?> clazz, String path) { if (pathとして示されたリソースが何らかのjarファイル中にある) { // そのjarファイルから自前でリソースを取得し、IResourceStreamにして返す。 // 注意:ただしこのとき、IFixedLocationResourceStreamも実装したクラスでなければならない。 } // pathとして示されたリソースを自前で扱えない場合や存在しなかった場合、元の処理を呼び出す。 return super.locate(clazz, path); } }
肝となるのは、pathで示されたリソースを該当するjarファイルからいかに軽く・速く取得するかである。
改良結果
具体的な改良方法を示す前に、この改良の結果は以下の通り。 改良前は、数ページの遷移ですぐにデフォルトのヒープ領域64Mを使い切ってしまうが(メモリ最大値を引き上げるとその分だけ使用してしまう)、改良後は30M以下の使用にとどまっている。