アプリケーションが新しいスレッドを作成するたびに、オペレーティング システムはそれをサポートするためのメモリを割り当てる必要があります。スレッドは一般に、完全なプロセスと比較して「軽量」と見なされますが、メモリコストはゼロではありません。スレッドのリークは、Android でメモリをリークする非常に一般的な方法です。
スレッドのコスト
Android でスレッドが作成されると、次の 2 つの主要なコンポーネントがメモリを消費します。
- ネイティブ スタック:
pthreadライブラリは、ローカル変数とコールフレームを格納するスレッドのスタックを割り当てます。Android では、通常、デフォルトで 1 MB です。 - 内部 JVM/カーネル構造: ART と Linux カーネルは、スレッドを追跡して管理するためにデータ構造(
pthread_internal_tやjava.lang.Threadオブジェクトなど)を割り当てる必要があります。
スタック メモリと mmap
Android は、MAP_ANONYMOUS | MAP_NORESERVE フラグを使用して mmap を使用して 1 MB のネイティブ スタックを割り当てることを理解することが重要です。
つまり、OS は 1 MB の仮想メモリ アドレス空間を予約しますが、1 MB の物理 RAM をすぐに割り当てるわけではありません。物理メモリは、スレッドが実際にコードを実行してスタックにデータをプッシュするときにのみ割り当てられます(ページイン)。何も実行しないスレッドは、スタックに非常に少ない物理 RAM を消費します。
実践型演習: スレッドのリーク
MemoryLab サンプル アプリケーションを使用して、アプリケーションが数百のアイドル状態のスレッドを作成したときに何が起こるかを確認します。
1. 起動とベースライン
アプリを起動して、ベースラインのメモリ読み取りを行います。
adb shell am start -W -n com.android.memorylab/.MainActivity
adb shell dumpsys meminfo -s com.android.memorylab
出力の Stack 行と Native Heap 行を確認します。これらは小さくなる可能性があります。
2. 100 個のスレッドを作成する
[Create 100 Threads] ボタンをタップします。アプリケーションは 100 個の新しいスレッドを生成します。スタック メモリが実際に物理 RAM にページインされるように(ツールで確認できるように)、各スレッドは待機を開始する前に、スタック上の小さな 10 KB の配列を割り当てて書き込みます。
スレッドがアクティブな間に、meminfo コマンドをもう一度実行します。
adb shell dumpsys meminfo -s com.android.memorylab
結果:
新しい出力をベースラインと比較します。スレッドがアクティブな間は、大幅な増加が見られます。
Stack: 1 MB 以上増加しました。これは、100 個のスレッド スタックに割り当てられた物理ページを表します(それぞれ少なくとも 10 KB に触れています)。Native Heap/Private Other: 各スレッドにシステムによって割り当てられた内部pthread構造により増加しました。
クリーンアップするには、[Destroy All Threads] ボタンをタップします。
3. Perfetto でスレッドを表示する
Perfetto は、スレッドのライフサイクルと数を経時的に追跡するのに最適です。

スレッド分析用の PerfettoSQL
トレースをクエリして、アプリケーションによって生成されたスレッドの数を正確にカウントできます。
SELECT p.name AS process_name, COUNT(t.id) as thread_count
FROM thread t JOIN process p USING (upid)
WHERE p.name = 'com.android.memorylab' AND t.name LIKE 'LeakedThread-%';
このようなクエリを使用して、スクリーンショットに示すように、カウンタ付きのデバッグ トラックを生成できます。