ちょっと話題の記事

Android のメモリ管理 #3 Heap dump でメモリリークの原因を解析する

2012.10.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Heap dump とは

Heap dump は現時点でどのようなオブジェクトがメモリに確保されているか解析するための機能です。 動作中のアプリの任意のタイミングでヒープのスナップショットをとる (HPROF形式のファイルとして出力する) ことができます。 このツールも Allocation Tracker と同様、Android SDKに含まれている機能なのでAndroidの開発環境が最低限整っていれば誰でも使用することができます。
今回は Eclipse Memory Analyzer (以下MAT) の使用方法を通して、 Heap dump を利用したメモリ解析について解説したいと思います。

Androidアプリのヒープ領域

ヒープ領域とは、動的に確保できるメモリ領域のことを指します。Androidのヒープ領域は Linuxヒープ (ネイティブヒープ) と Dalvikヒープ (Javaヒープ) の2つに分けられます。
Linuxヒープは Linux が管理しているヒープ領域で、このヒープ領域が少なくなってしまうと Activity#onLowMemory() メソッドが呼ばれ、ある一定の閾値を超えると LowMemoryKiller が実行されアプリが kill (強制終了) されます。
Dalvikヒープは DalvikVM が管理しているヒープ領域です。Linuxヒープからアプリ毎に割り振られます。アプリが確保しているオブジェクトのメモリサイズの合計がこのヒープ領域の上限を超えてしまうと OutOfMemory が発生してしまいます。
Heap dump で確認できるヒープは Dalvikヒープです。この点は正しく理解しておきましょう。

OutOfMemory を発生させてみる

まずは以下のような GridView で画像を一覧するアプリを作ってみました。GridView の使いかたはこちらの記事を参照してください。

Heap dump で Bitmap オブジェクトのメモリ使用量を解析できるのは Android 3.0 以上の端末のみなので注意してください(3.0 以前はBitmapオブジェクトをLinuxヒープに確保しているため)。

MainActivity.java

package jp.classmethod.android.sample.imageviewersample;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.GridView;

public class MainActivity extends Activity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		GridView gridView = new GridView(getApplicationContext());
		gridView.setNumColumns(4);
		gridView.setVerticalSpacing(10);
		gridView.setHorizontalSpacing(10);
		setContentView(gridView);

		BitmapAdapter adapter = new BitmapAdapter(getApplicationContext(), R.layout.item);
		for (int i = 0; i < 20; i++) {
			adapter.add(getBitmap());
		}
		gridView.setAdapter(adapter);
	}
	
	private Bitmap getBitmap() {
		Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
		Canvas canvas = new Canvas(bitmap);
		canvas.drawColor(Color.CYAN);
		return bitmap;
	}
}
[/java]
</p>

<p>
<strong>BitmapAdapter.java</strong><br />

package jp.classmethod.android.sample.imageviewersample;

import android.content.Context;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;

public class BitmapAdapter extends ArrayAdapter<Bitmap> {

	private int resourceId;

	public BitmapAdapter(Context context, int resource) {
		super(context, resource);
		resourceId = resource;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {

		if (convertView == null) {
			LayoutInflater inflater = (LayoutInflater) getContext()
					.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
			convertView = inflater.inflate(resourceId, null);
		}

		ImageView view = (ImageView) convertView;
		view.setImageBitmap(getItem(position));

		return view;
	}

}

実行結果

適当な Bitmap を 20 個生成し、 Adapter にセットしています。まだヒープ領域内で持ちきれる可能性がある量です。これを 10,000 個作るように変えて実行してみます。

OutOfMemoryError が発生してしまいました。オブジェクトの量が多すぎてヒープ領域で持ちきれるデータ量を超えてしまったようです。 このときに Heap dump してみます。HPROF ファイルに書き出すには 「Dump HPROF file」ボタンをクリックします。

MAT が eclipse にインストールされていると HPROF ファイルを eclipse 上で開くことができます。

Eclipse Memory Analyzer (MAT) を使って原因を解析する

HPROF ファイルを開くことができたので、どのクラスで OutOfMemory が発生したか調べていきます。

Leak Suspect でメモリリークの可能性があるオブジェクトを調べる

Leak Suspect という機能を使って、メモリリークの疑いのあるオブジェクトを調べることができます。 HPROF ファイルを開いたときに出てくる概要ページの下部にある「Leak Suspect」というリンクをクリックしてみてください。

すると Leak Suspects というページが開き、メモリリークの疑いがあるオブジェクトを表示してくれます。

メモリリークの疑いがかけられているオブジェクトが、ヒープ領域の中でどのくらいの割合を占めているかパイチャートで確認できます。 パイチャートの下には疑いがかけられているオブジェクトについての解析結果が表示されています。

今回のケースでは java.lang.Thread クラスがメモリリークの原因と関連しているようです。 また約 41MB のサイズがあり、ヒープ領域全体の約 84 %を占めているということが分かります。 「Details」のリンクをクリックし、より詳細に見ていきましょう。

「Shortest Paths To the Accumulation Point」を見てみると、 java.lang.Thread クラスを使用しているオブジェクトが表示されています。先ほど作成した BitmapAdapter クラスのオブジェクトが存在するので、原因は BitmapAdapter クラスで行われている処理であることが分かります。「Accumulated Objects」には BitmapAdapter クラスで保持しているオブジェクトが表示されています。 Bitmap が大量にあることが確認できたため、OutOfMemory の原因は「Bitmap オブジェクトの持ちすぎ」という解釈ができます。そこから、例えば「メモリキャッシュを実装して表示されていない Bitmap を破棄するようにする」などといった対策を考えることができます。
このようにしてメモリリークの原因を解析することができます。

まとめ

今回は MAT の Leak Suspect を使ってメモリリークの原因の解析方法をご紹介しました。 しかし Leak Suspect はあくまでメモリリークの疑いがあるオブジェクトを報告するだけですので、プログラマがこれが本当にメモリリークの原因か解析しなければいけません。 ここはプログラマの腕の見せどころです! Heap dump と MAT を積極的に活用していき、メモリリーク解析王を目指しましょう!
また、他にも dominator_tree から調べていく方法もあるのでこちらもぜひ活用していきましょう。

参考