Androidのメモリリークについて知っておくべきこと。

Ali Asadi
2019/06/30 – 16 min read

Javaのコアな利点の1つは、「Java。 JVM(Java Virtual Machine)の正確には、ガベージコレクタ(GC)です。 メモリからの解放を気にすることなく、新しいオブジェクトを作ることができるのです。 ガベージコレクタは、私たちのためにメモリの割り当てと解放を引き受けてくれます。 GC がどのように動作するかを十分に理解していない場合、ガベージコレクタがメモリを解放するのを防ぐことができます。

GC の動作方法をよく理解せずにコードを書くと、アプリでメモリ リークが発生することがあります。 これらのリークは、解放されていないメモリを浪費することでアプリに影響を与え、最終的にはメモリ不足の例外や遅延の原因となります。

Failure to release unused objects from the memory

Failure to release unused objects from the memory は、GC がメモリからクリアできない未使用オブジェクトがアプリケーションに存在することを意味します。 未使用オブジェクトを保持するメモリユニットは、アプリケーションの終了まで、または(メソッドの終了まで)占有されます。

Until the end of the method? うん、その通りだ。 アプリケーションの終了までメモリユニットを占有するリークと、メソッドの終了までメモリユニットを占有するリークの2種類があるんだ。 前者は明確だ。 2つ目はもっと説明が必要です。 これを説明するために、例を挙げてみましょう メソッドXがあるとします。メソッドXはバックグラウンドで何か長いランナータスクを行っており、終了するまでに1分かかります。 また、メソッドXはこの間、未使用のオブジェクトを保持しています。 その場合、メモリユニットは占有され、タスクが終了するまでの1分間、未使用オブジェクトをクリアすることができない。 メソッドの終了後、GCは未使用オブジェクトをクリアしてメモリを取り戻すことができます。

以上、とりあえず知っておいてほしいことは、後ほどコードと可視化でこの話に戻ることにします。 きっと楽しいですよ。 👹😜

ちょっと待った!!🥴

コンテキストに飛ぶ前に、基本的なことから始めましょう。

RAM (Random access memory) は、Android デバイスまたはコンピュータのメモリで、現在実行中のアプリケーションとそのデータを保存するために使用されます。

RAM の 2 つの主要なキャラクター、1 つはヒープ、2 つ目はスタックについて説明するつもりです。 それでは、楽しいパートに移りましょう🤩🍻

Heap &Stack

あまり長くならないようにしていますね。 本題に入りましょう。簡単に説明すると、スタックは静的なメモリ割り当てに使用され、ヒープは動的なメモリ割り当てに使用されます。 ヒープとスタックの両方が RAM に格納されていることだけは覚えておいてください。

ヒープメモリの詳細

Java ヒープメモリは、仮想マシンがオブジェクトを割り当てるために使用されます。 オブジェクトを作成すると、常にヒープに作成されます。 JVM や DVM などの仮想マシンは定期的にガベージ コレクション (GC) を実行し、もう参照されないすべてのオブジェクトのヒープ メモリを将来の割り当てに利用できるようにします。

円滑なユーザー体験を提供するために、Android では実行中のアプリケーションごとにヒープ サイズにハード リミットを設定しています。 ヒープ サイズの制限はデバイスによって異なり、デバイスが持っている RAM の量に基づきます。

Have ever wonder what the heap size for your application is?

Let’s discover this together.If you app hits this heap limit and try to allocate more memory, it will be terminate. アンドロイドには、Dalvik VM (DVM)があります。 DVMは、モバイルデバイス用に最適化された独自のJava仮想マシンです。 メモリ、バッテリー寿命、およびパフォーマンスのために仮想マシンを最適化し、各アプリケーションのメモリ量を分配する役割を果たします。

  1. dalvik.vm.heapgrowthlimit: この行は、アプリケーションのヒープ サイズで Dalvik が開始する方法に基づいています。 これは、各アプリケーションのデフォルトのヒープサイズです。 アプリが到達できる最大値です!
  2. dalvik.vm.heapsize: この行は、ヒープを大きくするための最大ヒープサイズを表します。 アプリケーション マニフェストでより大きなヒープをアンドロイドに要求する (android:largeHeap=”true”) ことで実現できます。

アプリでより大きなヒープを使用しないでください。 このステップの副作用を正確に知っている場合にのみ、それを行ってください。

Here is a table showing what heap size you got based on your device RAM:

+==========================+=========+=========+===================+
| DVM | 1GB RAM | 2GB RAM | 3GB RAM OR HIGHER |
+==========================+=========+=========+===================+
| DEFAULT(heapgrowthlimit) | 64m | 128m | 256m |
+--------------------------+---------+---------+-------------------+
| LARGE(heapsize) | 128m | 256m | 512m |
+--------------------------+---------+---------+-------------------+

Remember the more ram you have the high heap size will be. より高い RAM を持つすべてのデバイスが 512m を超えるわけではないことを念頭に置き、デバイスが 3GB 以上の場合、ヒープ サイズが 512m より大きいかどうかを確認するために、デバイスを調査してください。 getMemoryClass() または getLargeMemoryClass() (大きなヒープが有効な場合) のメソッドを使用すると、実行時に最大ヒープサイズを確認できます。

  • getMemoryClass() を使用します。 デフォルトの最大ヒープサイズを返します。
  • getLargeMemoryClass(): デフォルトの最大ヒープサイズを返します。 マニフェストで Large Heap フラグを有効にした後、利用可能な最大ヒープ サイズを返します。
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

How does it work in the real world?

When do we use the heap and when do we use the stack understand this simple application will use the himap.

application

以下のイメージは、アプリのヒープとスタックを表し、各オブジェクトはアプリを実行するとどこにポイントし格納するかを示しています。

では、アプリ実行について、各行を停止して、アプリケーションがいつオブジェクトを割り当て、それらをヒープまたはスタックの中に格納するかを説明します。 また、アプリケーションがいつスタックとヒープからオブジェクトを解放するかも見ていきます。

  • 行 1 – JVM はメインメソッド用にスタック メモリ ブロックを作成しました。

  • 2行目 – この行で、プリミティブローカル変数を作成します。 この変数は作成され、メインメソッドのスタックメモリに格納されます。

  • 第3行 -ここで私はあなたの注意を必要としています!!! この行では、新しいオブジェクトを作成します。 このオブジェクトはメインメソッドのスタックに作成され、ヒープに格納されます。 スタックには参照、オブジェクトとヒープのメモリアドレス(ポインタ)が格納され、ヒープには元のオブジェクトが格納されます。

  • 第4行 -第3行と同じです。

  • 5行目-JVMはfooメソッドのためにスタックメモリブロックを作成する。

  • 6行目 -新しいオブジェクトを作成する。 オブジェクトはfooメソッドのスタックメモリに作成されますが、3行目で作成したオブジェクトのヒープメモリアドレスをスタックに格納しています。 5行目で渡した値(3行目のオブジェクトのヒープメモリアドレス)です。 Javaは常に値で参照を渡すことを覚えておいてください。

  • 第7行 – 新しいオブジェクトを作っているところです。 スタックに作成されたオブジェクトは、ヒープ内の文字列プールを指しています。

  • 8行目 – fooメソッドの最終行でメソッドは終了しています。 そして、オブジェクトはfooメソッドのスタックブロックから解放されます。

  • 9行目-8行目と同じ、メインメソッドの最後のラインで、メソッドは終了している。 そして、メインメソッドのスタックブロックがフリーになります。

ヒープからメモリを解放するのはどうでしょうか。 もうすぐです。 コーヒー☕️を飲みながら、進んでください。

メソッドが終了するとどうなりますか。

figure 1

Figure 1 では、fooメソッドが終了するとき、オブジェクトは自動的に解放および再生成されます。 7117>

figure 2

図2では、同じように、fooメソッドのスタックメモリまたはスタックブロックは解放と取り戻される。 mainメソッドが終了したとき。 スタックメモリまたはメインメソッドのスタックブロックは、自動的に解放され、再生されます。

結論

今、スタック内のオブジェクトは一時的であることが私たちに明らかであることです。 メソッドが終了すると、オブジェクトは解放され、再利用されます。

スタックは LIFO (Last-In-First-Out) データ構造です。 箱のように見ることができます。 この構造を使用することにより、プログラムはプッシュとポップという 2 つの単純な操作を使用して、すべての操作を簡単に管理できます。

あなたが変数やメソッドのように何かを保存する必要があるたびにそれはプッシュとスタックポインタを上に移動させます。 メソッドから終了するたびに、前のメソッドのアドレスに戻るまで、スタックポインタからすべてをポップします。

What about the Heap?

Heap はスタックとは異なるものです。 ヒープメモリからオブジェクトを解放したり取り戻したりするためには、助けが必要です。

そのために、Java、より正確にはJVMは我々を助けるためのスーパーヒーローを作りました。 私たちはそれをガベージ・コレクターと呼んでいます。 彼は私たちのために大変な仕事をやってくれるのです。 そして、未使用のオブジェクトを検出し、それらを解放し、メモリ内のより多くのスペースを取り戻すことを気にかけます。

どのようにガベージコレクタは動作しますか

簡単なことだ。 ガベージコレクタは、使用されていない、または到達できないオブジェクトを探しています。 ヒープ内に、それへの参照が指されていないオブジェクトがある場合、ガベージコレクタは、メモリからそれを解放し、より多くのスペースを再利用する処理を行います。

GC

GC ルートは、JVM が参照するオブジェクトです。 それらはツリーの初期オブジェクトである。 ツリー内のすべてのオブジェクトは、1つ以上のルートオブジェクトを持っています。 アプリケーションまたはGCルートがそれらのルートまたはそれらのオブジェクトに到達できる限り、ツリー全体は到達可能である。

Garbage Collector が実行されるとどうなりますか。

今のところ、これはアプリケーションの現在のメモリ状態です。 スタックはクリアで、ヒープは未使用のオブジェクトでいっぱいです。

Before GC

GC実行後の結果は、次のようになります。

After GC

GCはヒープからすべての未使用オブジェクトを解放しクリアします。

Man! 待ち望んでいたメモリリークはどうなった? 笑)あと少し、あと少しです。 私たちのシンプルなアプリケーションでは、アプリは偉大でシンプルに書かれていました。 GCがヒープのオブジェクトを解放するのを妨げるようなコードには何も問題がなかった。 そしてそのために、GCはヒープ・メモリからすべてのオブジェクトを解放し、再要求するのです。 続けてください。 次のセクションでメモリリークの例をたくさん紹介します😃

メモリリークはいつ、どのように起こるのですか?

メモリリークは、スタックがヒープ内の未使用オブジェクトをまだ参照しているときに起こります。

この視覚表現では、スタックから参照されているがもう使用されていないオブジェクトがある場合、それがあることがわかります。 ガベージコレクタは、これらのオブジェクトが使用中であることを示すため、メモリからそれらを解放または開放することは決してありません。 AsyncTasks、Handler、Singleton、Threadsなどを使って簡単に作ることができます。

スレッド、シングルトン、リスナーを使ったいくつかの例で、どのようにリークを起こし、それを回避・修正するのかを説明します。

How can we cause a leak using thread?

この例では、バックグラウンドでスレッドを実行するアクティビティを開始するつもりです。

既知のように、内部クラスはその包含クラスへの暗黙の参照を保持します。

舞台裏では、アクティビティは実際にこのように見えます。

DownloadTask は ThreadActivity の参照を保持しています。

では、タスクまたはスレッドを開始するとどうなるのでしょうか。

通常のフロー

画像では、アプリケーションのヒープとスタックを提示しています。

ユーザーはアプリケーションを起動し、ThreadActivityを開き、ダウンロードタスクが終了するまで画面で待機しました。 ユーザーは20秒間待ちました。 なぜ20秒なのでしょうか? これは、スレッドがタスクを完了するのにかかる時間だからです。

Task Running

バックグラウンドでタスクが実行されている状態です。 ユーザーはダウンロードタスクの完了を20秒間待ちました。 タスクが完了すると、スタックはrun()メソッドブロックを解放します。

DownloadTask を保持している参照は存在しない。 GCはDownladTaskオブジェクトを未使用オブジェクトとみなし、そのため、次のGCサイクルはヒープメモリからそれをクリアします。

GCはヒープから未使用オブジェクトをクリアする。 さて、ユーザーがアクティビティを閉じたとき。 メインメソッドは、スタックから解放され、GC の次のサイクルで、GC はヒープメモリから ThreadActivity をクリアします。

Perfect!