![見出し画像](https://assets.st-note.com/production/uploads/images/110041840/rectangle_large_type_2_ceddd207297543715fad1993655af240.jpeg?width=1200)
Javassistとメモリリーク
何かと便利なJavassistですが、使い方によってはメモリリークが発生することをご存じでしょうか。今回はJavassistのメモリリーク回避方法を紹介したいと思います。
下準備
まずはメモリリークの検証コードを実行する下準備をします。
今回はIntelliJで環境を構築しました。
プロジェクトの作成
IntelliJでプロジェクトを新規作成します。
言語:Java
ビルドシステム:Gradle
JDK:11.0.14
Gradle DSL:Groovy
![](https://assets.st-note.com/img/1687933197838-170HrxGnMB.png)
依存ライブラリの指定
build.gradleを編集して、Javassistを利用できるようにします。
Javassistのバージョンは3.29.2-GAを使用しました。
dependencies {
implementation("org.javassist:javassist:3.29.2-GA")
}
検証に使用するIFとClassを作成
Javassist経由で利用するIFとClassを作成します。
package com.wingarc.note.javassist;
public interface Executable {
int exec();
}
public class Template implements Executable {
@Override
public int exec() {
return 0;
}
}
※メモリリークを確認することが目的のため、中身は適当です。
メモリリークの検証
メモリリーク1
下準備で用意したTemplate Classを元に、新しいClassを作成するコードを用意しました。
package com.wingarc.note.javassist;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.concurrent.atomic.AtomicInteger;
public class Main1 {
private static final String TEMPLATE_CLASS = "com.wingarc.note.javassist.Template";
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
AtomicInteger classNum = new AtomicInteger();
long start = System.nanoTime();
for (int i = 0; i < 100000; i++) {
CtClass ctClass = classPool.getAndRename(TEMPLATE_CLASS,
TEMPLATE_CLASS + '_' + classNum.incrementAndGet());
@SuppressWarnings("unchecked")
Class<Executable> clazz = (Class<Executable>) ctClass.toClass();
Executable executable = clazz.getDeclaredConstructor().newInstance();
System.out.println(i + ": " + executable.exec());
}
long end = System.nanoTime();
System.out.println((end - start) / 1000000 + "ms");
}
}
処理の内容は以下のようになっています。
Template Classを元に、CtClassを作成
CtClassの書き換え(メモリリークの確認が目的のため省略)
CtClassからClassを作成
作成したClassのインスタンスを作成
作成したインスタンスのexecメソッドを実行
1~5を10万回繰り返す
このコードをヒープ割り当て16MB(-Xmx16m)で実行すると、15000回ほど繰り返したところでOutOfMemoryErrorが発生します。
.
.
.
15072: 0
15073: 0
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
メモリリーク2
メモリリーク1で紹介したコードは、メモリリークとしてわかりやすいケースです。
原因はCtClassをdetachしていないことにあります。
CtClassをdetachしないと、ClassPoolからの参照が残りメモリリークします。
次のコードではCtClassをdetachしてClassPoolからの参照が残らないようにしました。
package com.wingarc.note.javassist;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.concurrent.atomic.AtomicInteger;
public class Main2 {
private static final String TEMPLATE_CLASS = "com.wingarc.note.javassist.Template";
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
AtomicInteger classNum = new AtomicInteger();
long start = System.nanoTime();
for (int i = 0; i < 100000; i++) {
CtClass ctClass = classPool.getAndRename(TEMPLATE_CLASS,
TEMPLATE_CLASS + '_' + classNum.incrementAndGet());
try {
@SuppressWarnings("unchecked")
Class<Executable> clazz = (Class<Executable>) ctClass.toClass();
Executable executable = clazz.getDeclaredConstructor().newInstance();
System.out.println(i + ": " + executable.exec());
}
finally {
ctClass.detach();
}
}
long end = System.nanoTime();
System.out.println((end - start) / 1000000 + "ms");
}
}
同じようにヒープ割り当て16MBで実行すると、6万回を過ぎたあたりでOutOfMemoryErrorが発生します。
1.5万回からは増えていますが10万回の完遂はできません。
.
.
.
60080: 0
60081: 0
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.io.BufferedInputStream.<init>(BufferedInputStream.java:209)
at java.base/java.io.BufferedInputStream.<init>(BufferedInputStream.java:189)
at javassist.CtClassType.getClassFile3(CtClassType.java:221)
at javassist.CtClassType.getClassFile2(CtClassType.java:178)
at javassist.CtClassType.setName(CtClassType.java:382)
at javassist.ClassPool.getAndRename(ClassPool.java:386)
at com.wingarc.note.javassist.Main2.main(Main2.java:18)
原因と対策
ClassPoolからの参照が無くなったことで、リークする要素は一見なくなったように見えます。実際、Javassistがメモリリークしているわけではありません。それではどこでメモリリークが発生しているのでしょうか?
メモリリークの原因調査がテーマではないため、調査方法については省略しますが、メモリリークの箇所はClassLoaderになります。
ClassLoaderが解放されないので、ClassLoaderが保持しているClass情報も解放されないのです。
ClassLoaderの対策をしたコードは次のようになります。
package com.wingarc.note.javassist;
import javassist.ClassPool;
import javassist.CtClass;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.atomic.AtomicInteger;
public class Main3 {
private static final String TEMPLATE_CLASS = "com.wingarc.note.javassist.Template";
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
AtomicInteger classNum = new AtomicInteger();
ClassLoader parent = Main3.class.getClassLoader();
long start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
CtClass ctClass = classPool.getAndRename(TEMPLATE_CLASS,
TEMPLATE_CLASS + '_' + classNum.incrementAndGet());
try {
/*
* メモリリーク対策で毎回異なるクラスローダーを使用する
*/
URLClassLoader loader = new URLClassLoader(new URL[0], parent);
@SuppressWarnings("unchecked")
Class<Executable> clazz = (Class<Executable>) ctClass.toClass(loader, null);
Executable executable = clazz.getDeclaredConstructor().newInstance();
System.out.println(i + ": " + executable.exec());
}
finally {
ctClass.detach();
}
}
long end = System.nanoTime();
System.out.println((end - start) / 1000000 + "ms");
}
}
使い捨てのURLClassLoaderを使用することで、ClassLoader含めて解放されるようになります。
ヒープ割り当て16MBでも100万回の実行を完遂します。
.
.
.
999998: 0
999999: 0
209835ms
さいごに
最後まで目を通していただきありがとうございます。
メモリリークとしては、インスタンスのリークが一般的ですし、開発の現場で直面することも多いと思います。
Classのリークは普通の実装で発生することはなく、Javassistの使い方によって発生する特殊なリークですので、気付いていないだけでリークしていた。ということもあるのではないでしょうか。