NHN Cloud Meetup 編集部
[Kotlin]Java開発におけるメモリリーク防止策!KotlinとLambdaの美点
2019.09.19
6,745
今回は、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
- 上記の分析ツールを利用すると、メモリリークを検査できます。