[Kotlin]Java開発におけるメモリリーク防止策!KotlinとLambdaの美点

今回は、KotlinLambda/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

すべてのソースコード

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop