Android デバイス上のメモリ リーク発見と不具合特定方法

信頼性(Reliability)テストとは、「障害の発生のしにくさ」をテストすること。

システムやサービスが使えなくなる頻度やその間隔を示す指標である平均故障間隔(Mean Time Between Failures)を用いて品質状況を判断する。

 

このテストの中ではリソースリークも不具合報告対象になる。

 

 

不具合報告に対してエンジニアが不具合箇所を特定できない場合、次のような要求がくることがある。

 

特定するためにログ追加したAPKを作成したので、こちらで再現をお願いします。

 

なめてるの?

 

テスターに再現させて不具合治し続けてるからプロジェクト計画が遅延するんだよ!

不具合発生する毎にログ追加した再現依止めろや!

 

こんな解析しているから、僕の台湾出張が伸びたんだろ!

 

僕はエンジニアでもテスターでもないけれど「品質戦略立てたのお前だから責任とれ」とかいわれて、この数ヶ月間テストして解析して修正箇所を特定してきた。

リーク解析手順はプロセス化しやすいので記載しておく。

メモリやファイルディスクリプタなど、リソース枯渇に関する問題は、使用可能なリソースが時間と共に減少する問題である。

利用可能なリソースが減少すると、システムの性能が低下したり不安定になる。

これは、システムを再起動しなければ回復しない。

リソースリークの問題は、一定期間・一定間隔でリソース使用量を採取し、統計を解析する事が重要となる。

 

主なリソースリークと確認方法は次のとおり。

測定目的 取得コマンド 確認点
システム上のメモリ使用数チェック cat /proc/meminfo Buffers、Cashed
slabを増加チェック cat /proc/meminfo Slab
kernel slab memory leak検出 cat /proc/slabinfo
threadの増加チェック cat /proc/meminfo KernelSlack
kernel slack thread leak検出 ps -t
userlandプロセスのmemory leakチェック dumpsys meminfo PSS
global Reference leakチェック call->Dump.dumpReferenceTable() logcat
FD(file descriptor)増加チェック lsof

今回はメモリリークについて記載する。

メモリリークしているプロセスの特定方法

メモリリークに限っていえば色々なツールや方法でリークを発見可能。

例えば「ps」コマンドでも「dumpsys」コマンドでもよい。

Android10からは Android デバイス上からパフォーマンス トレースを取得するためのツール「perfetto」なども存在する……らしい。

perfetto  |  Android Studio  |  Android Developers
perfetto は Android スマートフォンからパフォーマンス トレースを取得するためのツールです。

「dumpsys」コマンドを使った方法を紹介する。

「t」オプションはタイムアウト時間を秒単位で指定(デフォルト値は 10 秒)。

「c」オプションは処理しやすいコンパクトな表示となる。

dumpsys  |  Android Studio  |  Android Developers
dumpsys は、Android デバイス上で稼働し、システム サービスに関する情報を提供するツールです。

 

これを例えば10分毎に出力し、出力されたデータをグラフ化する。

グラフ作成はgnuplotでもExcelを使っても良い。

一定期間中にほぼ単調増加しているのであれば、そのモジュールはメモリリークの疑いがある。

この時点で不具合報告対象ではあるが、リークしている量に応じて不具合報告の重篤度は変わるため

2時間で1MBのメモリリークは修正必須

などとエンジニアチームと議論して予め決めておいた方がよい。

dumpsysで出力させた結果内容の説明

メモリリークしているプロセス名が特定出来ているなら次のように「dumpsys」を使うことができる。

出力結果は次の通り。

それぞれの内容を説明してる日本語サイトは少ない。

用語 説明 補足
Uptime スリープ時間を除く、起動から現在までの時間 ミリ秒 (ms) 単位で示す
Realtime スリープ時間を含む、起動から現在までの時間 ミリ秒 (ms) 単位で示す
Native Heap cのmallocからヒープ領域を参照 C++ によって要求されたメモリはネイティブ プロセス、Java によって要求されたメモリはJava プロセス
Dalvik Heap Java の新しい Java ヒープ領域を指す 仮想メモリが占​​めるスペースだけ
Pss Total 実際の物理メモリを占有するスペース
Private Dirty プライベート常駐メモリ プロセスのメモリ空間は物理メモリとは異なる仮想メモリであり、プロセスは物理メモリ RAM を直接操作できない。必要に応じて、OSがプロセスを物理メモリに適用できるようにマップする
Heap Size 占有されているメモリの合計 (ヒープ heap)
Heap Alloc 仮想アドレス内の割り当てスペース
Heap Free 空きメモリ

この中で大事なのはPrivate Dirty。

Dalvik Heap (= Java Heap) と Native Heap の「Private Dirty」列を見ると、Java ヒープ上の SystemUI のメモリ使用量が 9M、Native ヒープ上で 7M であることがわかる。

このデータを

  • テスト前
  • テスト後

で取得して(もしくは定期的に取得して)Dalvik HeapとNative Heap のどちらが単調増加傾向にあるのかをグラフ化するとよい。

[おまけ] PSコマンド、TOPコマンド

各プロセスの仮想メモリ使用量は、psコマンドでは「VSZ」、topコマンドでは「VIRT」として確認できる。

また各ユーザープロセスの物理メモリ使用量を確認するには、psコマンドでは「RSS」、topコマンドでは「RES」がある。

ユーザー空間 カーネル空間
仮想メモリ カーネルへの影響:なし
他プログラムへの影響:なし
発生事象:メモリを消費したプログラムでENOMEMエラーが発生する
カーネルへの影響:限定的
他プログラムへの影響:なし
OSハングアップという観点では考慮不要
物理メモリ カーネルへの影響:なし
他プログラムへの影響:あり
発生事象:
 ・スワップアウト発生
 ・ページキャッシュ減少
 ・OOM-Killer発生
カーネルへの影響:あり
他プログラムへの影響:あり
発生事象:
 ・スワップアウト発生
 ・ページキャッシュ減少
 ・OOM-Killer発生

この中で、カーネルへの影響があり、OS停止に至り、リブートする可能性があるのは、「カーネル空間」の「物理メモリ」を消費しているとき。

減り続ける利用可能メモリ……そしてついにリブート!

メモリリーク解析のドリルダウン

メモリリークの解析には「ドリルダウン(掘り下げて詳細な情報を得る)」が必要となる。

Debugging memory usage on Android - Perfetto Tracing Docs

ヒープ ダンプ(hprof ファイル)を出力

Javaレイヤリークの疑いがあるのであればJavaVMのダンプ出力ファイル(特定の時点でのJavaプロセスメモリのスナップショット)であるHPROFファイルを取得する。

 

ヒープ ダンプ(hprof ファイル)を出力するだけならコマンドラインから可能(プロセス番号が「2099」の場合)

プロセス名が分かっていれば「ps -ef」でプロセスを表示し、プロセス番号を特定する。

【Linux】

【Windows】

もしくは直接プロセス名を指定してもよいし、出力名を指定しなくてもよい。

ファイル名が指定されていない場合、デフォルトのパスとファイル名は次のようになる。

 

/data/local/tmp/heapdump-日付-時間.prof

GC(ガーベージコレクション)で回収するメモリか確認する

GC(ガーベージコレクション)で回収するメモリは、dalvik と art のメモリ。

【GC(ガベージコレクション)】
プログラムが使用しなくなったと判断されたメモリをプログラマが関与することなくヒープに戻すメカニズム。

「am dumpheap -g」コマンドを使用すると、強制的にGCを実施することができる。

このため、

【GC前】

  1. adb shell am dumpheap プロセス名 /data/local/tmp/before_heapdump.hprof
  2. adb shell dumpsys meminfo プロセス名 > /data/local/tmp/dumpsys_before_gc.txt

【GC後】

  1. adb shell am dumpheap -g プロセス名 /data/local/tmp/after_gc_heapdump.hprof
  2. adb shell dumpsys meminfo プロセス名 > /data/local/tmp/dumpsys_after_gc.txt

を比較して、Dalvik heapが信頼性(Reliability)テスト実施前の使用量まで減っているのであれば、java管理のメモリは問題なく回収されている。

 

GCを実施すると「natvice」と「jar」のメモリ使用量が増えることがある。

これは、おそらくheapファイルを保存するのに使用されるnatviceなメモリなんじゃないかな……。

 

なお、am dumpheapコマンドのオプションは次のとおり。

  • –user: ユーザー ID、デフォルトの UserHandle.USER_CURRENT
  • -n: natice のヒープダンプを取得します
  • -g: 取得する前に 1 回 gc します。
  • -m: パラメータを非表示にする
全てのデータ取得の流れは次のようになる。

【初期準備】

  1. adb shell am dumpheap プロセス名 /data/local/tmp/start_heapdump.hprof

【テスト中】

定期的に実行する

  1. adb shell dumpsys meminfo プロセス名

【テストが終了】

  1. adb shell am dumpheap プロセス名 /data/local/tmp/before_heapdump.hprof
  2. adb shell dumpsys meminfo プロセス名 > /data/local/tmp/dumpsys_before_gc.txt
  3. adb shell am dumpheap -g プロセス名 /data/local/tmp/after_gc_heapdump.hprof
  4. adb shell dumpsys meminfo プロセス名 > /data/local/tmp/dumpsys_after_gc.txt

【hprofとdumpsys結果の回収】

  1. adb pull /data/local/tmp/start_heapdump.hprof .
  2. adb pull /data/local/tmp/before_gc_heapdump.hprof .
  3. adb pull /data/local/tmp/after_gc_heapdump.hprof .
  4. adb pull /data/local/tmp/dumpsys_before_gc.txt .
  5. adb pull /data/local/tmp/dumpsys_after_gc.txt .

GCで回収されないJava メモリリークの解析方法

メモリ使用量などのアプリのパフォーマンスを監視するのには以前は「Dalvik Debug Monitor Server (DDMS)」というAndroid SDK に含まれるツールを使うのが定番だった。

ただし、DDMS は Google によってサポートされなくなり、今ではAndroid Memory Profiler が定番。

 

Android Studioを起動し、File→Openでhprofファイルを読みこむ。

 

表示されている意味はそれぞれ次の通り。

  • Allocations: ヒープ内の割り当ての数
  • Native Size: Java で割り当てられたオブジェクト用のメモリが表示される
  • Shallow Size: このオブジェクト タイプによって使用されている Java メモリの合計量(バイト単位)
  • Retained Size: このクラスのすべてのインスタンスのために保持されているメモリの合計サイズ(バイト単位)
Memory Profiler を使用してアプリのメモリ使用量を調べる  |  Android Studio  |  Android Developers
Android Profiler のコンポーネントであり、スタッタリング、フリーズ、アプリのクラッシュを引き起こす可能性があるメモリリークやメモリチャーンの特定に役立つ、Memory Profiler について説明します。

 

残念ながら、今回のメモリリークにGCで回収されないJavaレイヤのメモリは見つからなかったので解析方法は省略。

GCで回収されず、Native heapがメモリリークしている場合

adb shell showmap PID(Android の場合) を使用するか /proc/PID/smaps を見ることで、リークの疑いのあるオブジェクトを見つける。

 

上の例で[anon:libc_malloc]がリークしてそうだと分かったとする。

ネイティブ領域のメモリリーク解析方法

Androidアプリ上では C/C++ などのNativeコードがない場合でも、Native メモリが使用されることがよくある。

例えばNative ライブラリを内包してたり、一部のフレームワーク API (Regex など) の実装がNativeコードを通じて内部実装されてる事もある。

例えば、Nativeのpthreadのリークだと分かったが、Java・Kotlinレイヤでquit()、nullを忘れていた……など。

 

今から記載するNativeレイヤのリーク解析は、端末のroot権限がないと実施できないしパッケージのシンボル情報も必要なので端末メーカの方法。

https://gerrit.pixelexperience.org/plugins/gitiles/bionic/+/59e8910b172f836bf050b8d4afd7ab963917fa8e/libc/malloc_debug/README.md
  • 「libc.debug.malloc.program」「libc.debug.malloc.options」にリークの疑いのあるプロセスを指定
  • 「kill -47 <プロセスID>」でバックトレースをdump
  • 「native_heapdump_viewer.py」を使ってシンボル情報を与えることでdump情報からコード情報に変換しHTMLファイルを作成する
  • 「テスト開始前」「テスト開始後」のリーク箇所を特定する

ここはAndroidのデバイスのビルド環境に依存するために、現場によって異なる。

可能であれば、テスト前・後のHTMLファイルを比較すればメモリリークしているソースコードの行数まで一発で分かる。

おわりに

「デバッグを考慮した設計」ができない人は開発者に向いてない。

まぁ僕は開発者にはなれなかったけどね……

デバッグ(不具合解析)の手順概要(ログ解析、再現、デバッガツール)
エンジニアの責務範囲は「設計」フェーズから書き出しても幅広い。図の中の青色がエンジニアの責務。そんな中「第三者評価」で不具合が報告されると、デバッグ(不具合解析)という作業が追加される。...

エンジニアスキルによらずに不具合解析できるようにAI(人工知能)使う活動も何年も進めてる……日の目を見ることはあるのかな。

タイトルとURLをコピーしました