NHN Cloud Meetup 編集部
[Kotlin]Java開発におけるメモリリーク防止策!KotlinとLambdaの美点
2019.09.19
6,993
今回は、KotlinとLambda/SAMが提供している数多くの利点の中から、メモリリークの回避方法について紹介したいと思います。
Android中心に作成しましたが、一般的なJVMにも適用できるので、Java/Kotlinの開発者には参考になると思います。
本文にあるすべてのコードは、GitHubにアップされており、直接テストすることも可能です。
Java、匿名クラス とメモリリーク
次のようなJavaで開発されたアクティビティがあります。
public class MyJavaLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_java_leak);
startAsyncTask();
}
private void startAsyncTask() {
Runnable task = new Runnable() {
@Override
public void run() {
SystemClock.sleep(20000);
}
};
new Thread(task).start();
}
}
上記のアクティビティが生成されると、(onCreate),startAsyncTask()メソッドにより、新しいスレッドが作成され、動作します。
このタスクは20秒間sleepを行います。(Thread.sleepを使わない理由は、Thread.sleepはインタラプトで終了される可能性があるためです。)つまり、上記で新規作成されたRunnableオブジェクトは、約20秒間、処理が留まるでしょう。
ここで、注目すべき点は、生成されたAnonymous Runnable(匿名クラス)のオブジェクトが、MyJavaLeakActivityという外部クラスに対して 実は参照ができるということです。
そのため、MyJavaLeakActivityが破棄されても、Runnableオブジェクトが生きていることによって、ガーベージコレクタ(GC)が動作せず(最大20秒間)MyJavaLeakActivityはメモリリークを誘発させます。もちろん、20秒後にはGCによってメモリから削除されるでしょう。
上記のJavaで書かれたMyJavaLeakActivityのバイトコードを見てみましょう。
(ビルドされたapkファイルを次のように開いてみると確認できます。)

まず、MyJavaLeakActivityファイルのバイトコードをみてみましょう。
(上図でMyJavaLeakActivity$1ファイルも生成されていることに注目してください!あとで説明します。)
(また、バイトコードのうち、ここでの内容とあまり関連しないものは、意図的に省略しました。)
.class public Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;
.super Landroid/support/v7/app/AppCompatActivity;
.source "MyJavaLeakActivity.java"
# direct methods
.method public constructor <init>()V
.registers 1
.line 7
invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V
return-void
.end method
.method private startAsyncTask()V
.registers 3
.line 17
new-instance v0, Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity$1;
invoke-direct {v0, p0}, Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity$1;-><init>(Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;)V
.line 23
.local v0, "task":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;
invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual {v1}, Ljava/lang/Thread;->start()V
.line 24
return-void
.end method
...
ここで注目すべき部分は、startAsyncWorkメソッドです。
new-instance v0, Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity$1;
invoke-direct {v0, p0}, Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity$1;-><init>(Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;)V
匿名クラスオブジェクトが生成されるとき、外部クラスの参照を持ちますが、ここでは、MyJavaLeakActivity$1インスタンスを生成して、v0変数に保存し、その値を用いて、MyJavaLeakActivityの初期化を進行しています。
内部クラスが外部クラスの参照ができるように、匿名クラスのインスタンスは、外部クラスのインスタンスの参照ができます。
MyJavaLeakActivity$1クラスを見てみましょう。
以下はMyJavaLeakActivity$1ファイルのバイトコードです。
.class Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity$1;
.super Ljava/lang/Object;
.source "MyJavaLeakActivity.java"
# interfaces
.implements Ljava/lang/Runnable;
# instance fields
.field final synthetic this$0:Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;
# direct methods
.method constructor <init>(Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;)V
.registers 2
.param p1, "this$0" # Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;
.line 17
iput-object p1, p0, Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity$1;->this$0:Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public run()V
.registers 3
.line 20
const-wide/16 v0, 0x4e20
invoke-static {v0, v1}, Landroid/os/SystemClock;->sleep(J)V
.line 21
return-void
.end method
ここでは、次の構文に注目しましょう。
# interfaces .implements Ljava/lang/Runnable; ... # instance fields .field final synthetic this$0:Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;
このクラス(MyJavaLeakActivity$1)に、Runnableインタフェースを実装しました。MyJavaLeakActivityタイプのフィールドを持ち、下のコンストラクタを使ってMyJavaLeakActivityインスタンスを受け取っています。
# direct methods .method constructor <init>(Lcom/eungpang/android/memoryleaktest/MyJavaLeakActivity;)V
つまり、このクラス(MyJavaLeakActivity$1)は、上記のJavaコードの20秒間、Runnable タスクを進行する匿名クラスですが、外部クラス(MyJavaLeakActivity)のインスタンスをコンストラクタから受け取り、フィールドに保持していることから、メモリリークを発生させます。
Kotlin、 Lambda そして SAM
上記のMyJavaLeakActivityをKotlinに移してみましょう。
class MyKotlinLeakActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_kotlin_leak)
startAsyncTask()
}
private fun startAsyncTask() {
val task = Runnable {
SystemClock.sleep(20000)
}
Thread(task).start()
}
}
単純に、KotlinのLambda/SAM(Single Abstract Method)を用いて移植しました。
コードだけを見ると、上のJavaコードと同じように動作すると思われます。バイトコードを見てみましょう。
まず、MyKotlinLeakActivityクラスのバイトコードです。
.class public final Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity;
.super Landroid/support/v7/app/AppCompatActivity;
.source "MyKotlinLeakActivity.kt"
...
# direct methods
.method public constructor <init>()V
.registers 1
.line 7
invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V
return-void
.end method
.method private final startAsyncTask()V
.registers 3
.line 17
sget-object v0, Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;->INSTANCE:Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;
check-cast v0, Ljava/lang/Runnable;
.line 20
.local v0, "task":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;
invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual {v1}, Ljava/lang/Thread;->start()V
.line 21
return-void
.end method
...
コードを見ると、Javaが変換されたバイトコードと若干の違いがあります。
Javaではnew-instanceを用いてオブジェクトを生成したのに対して(MyJavaLeakActivity$1オブジェクトの作成)、Kotlin のバイトコードでは下記のようにMyKotlinLeakActivity$startAsyncTask$task$1 staticのインスタンスを取得して、v0変数に保存されます。
sget-object v0, Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;->INSTANCE:Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;
では、MyKotlinLeakActivity$startAsyncTask$task$1 クラスはどうなったでしょうか?
上記のsmaliコードによると、自分自身のインスタンスを持つstaticフィールドがあり、Javaコードと同様に、Runnableを実装します。次のコードで確認してみましょう。
.class final Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;
.super Ljava/lang/Object;
.source "MyKotlinLeakActivity.kt"
# interfaces
.implements Ljava/lang/Runnable;
...
# static fields
.field public static final INSTANCE:Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;
# direct methods
.method static constructor <clinit>()V
.registers 1
new-instance v0, Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;
invoke-direct {v0}, Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;-><init>()V
sput-object v0, Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;->INSTANCE:Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;
return-void
.end method
.method constructor <init>()V
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final run()V
.registers 3
.line 18
const-wide/16 v0, 0x4e20
invoke-static {v0, v1}, Landroid/os/SystemClock;->sleep(J)V
.line 19
return-void
.end method
上のコードで注目すべき部分は、こちらです。
# interfaces .implements Ljava/lang/Runnable; ... # static fields .field public static final INSTANCE:Lcom/eungpang/android/memoryleaktest/MyKotlinLeakActivity$startAsyncTask$task$1;
MyKotlinLeakActivity$startAsyncTask$task$1クラスはRunnableを実装して、実際に20秒待機するロジックを含むクラスですね。また、シングルトンパターンのように、自分自身を持つstaticフィールドを有しています。
つまり、どこにもMyKotlinLeakActivityクラスインスタンスは表示されません。
これにより、Kotlinコードでは、メモリリークが発生しないのです。
結論?
整理してみましょう。
Anonymous inner classを使用すると、Outer ClassのReferenceを持ちます。それにより、メモリリークが発生する余地があります。
一方、SAM, lambdaを使用すれば、Static フィールドを作成して作業を進めるため、比較的、メモリリークの発生余地が少なくなります。
覚えておくこと!
実際には、Kotlinだからメモリリークを減らすことができるのではなく、匿名内部クラスを使用しないため、メモリリークを予防することができるのです。つまり、JavaコードでもLambda expression/SAMを用いて開発すれば、同じ効果が得られます。
付録
JavaでLambdaを使用する場合は?
すでに結論で述べたように、JavaコードでもLambdaを使ってコードを作成すると、Kotlinのコードのように、メモリリークを予防できます。次は、Javaのコードです。
public class MyJavaLambdaLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_java_leak);
startAsyncTask();
}
private void startAsyncTask() {
Runnable task = () -> {
SystemClock.sleep(20000);
};
new Thread(task).start();
}
}
上記のように、Javaで開発してビルドすると、次のようなsmaliコードが出てきます。
まず、MyJavaLambdaLeakActivityのバイトコードです。
.class public Lcom/eungpang/android/memoryleaktest/MyJavaLambdaLeakActivity;
.super Landroid/support/v7/app/AppCompatActivity;
.source "MyJavaLambdaLeakActivity.java"
...
.method static synthetic lambda$startAsyncTask$0()V
.registers 2
.line 18
const-wide/16 v0, 0x4e20
invoke-static {v0, v1}, Landroid/os/SystemClock;->sleep(J)V
.line 19
return-void
.end method
.method private startAsyncTask()V
.registers 3
.line 17
sget-object v0, Lcom/eungpang/android/memoryleaktest/-$$Lambda$MyJavaLambdaLeakActivity$caHB7J9mZR29sol_sLXUURDc30c;->INSTANCE:Lcom/eungpang/android/memoryleaktest/-$$Lambda$MyJavaLambdaLeakActivity$caHB7J9mZR29sol_sLXUURDc30c;
.line 21
.local v0, "task":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;
invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual {v1}, Ljava/lang/Thread;->start()V
.line 22
return-void
.end method
...
以下は生成された-$$Lambda$MyJavaLambdaLeakActivity$caHB7J9mZR29sol_sLXUURDc30cコードです。
.class public final synthetic Lcom/eungpang/android/memoryleaktest/-$$Lambda$MyJavaLambdaLeakActivity$caHB7J9mZR29sol_sLXUURDc30c; .super Ljava/lang/Object; .source "lambda" # interfaces .implements Ljava/lang/Runnable; # static fields .field public static final synthetic INSTANCE:Lcom/eungpang/android/memoryleaktest/-$$Lambda$MyJavaLambdaLeakActivity$caHB7J9mZR29sol_sLXUURDc30c; ...
Kotlinで開発したときのように、static フィールドを持ちながら、MyJavaLambdaLeakActivityインスタンスがなく、メモリリークを予防することができます。
Leak分析ツール:LeakCanary
- LeakCanary:A memory leak detection library for Android and Java
- 上記の分析ツールを利用すると、メモリリークを検査できます。