Android OutOfMemory 분석


안드로이드에서 비트맵 이미지를 불러온다거나 애니메이션시 메모리부족으로 인해 응용 프로그램이 종료되는 문제점을 한번쯤을 격었을 것이다. 이런 경우 이미지나 객체를 할당된것은 메모리로 부터 끊어 GC되어 메모리로 부터 제거되도록 해야한다. 메모리와 객체가 연결이 끊기지 않은 상태로 힙 한계에 다르면 프로세스는 OutOfMemory 예외가 발생 하게된다.  

주요 발생 원인

1. 지속적으로 많은 메모리를 요구하고 어떤 지점에서 프로세스의 최대 힙 메모리를 넘어 작업 하는 경우

2. 객체가 GC에 의해 메모리로 부터 제거되지 않아 누수되는 경우

3. 큰 비트맵을 스캐일링하지 않고 바로 로드하는 경우 

안드로이드에서 메모리 누수 테스트의 가장 쉬운 방법은 Activity 지속적으로 회전하거나, Activity를 지속적으로 들어갔다가 빠져 나가면서 메모리가 정상적으로 반환되지 않고 누수되는지 힙을 분석해보면 된다. 

가장 평범하면서 실수를 많이 하는 메모리 누수 예제

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
    super.onCreate(state);
    ImageView label = new ImageView(this);
    if (sBackground == null) {
        sBackground = getDrawable(R.drawable.large_bitmap);
    }

    label.setBackgroundDrawable(sBackground);
    setContentView(label);
}

화면을 회전하게 되면 onCreate가 새롭게 호출 되며, 기존의 ImageView에 sBackgroud와 연결이 끊기지 않고, 이 ImageView는 또다시 Activity를 참조하고 있기 때문에 Activity가 종료 되어도 메모리가 해지되지 않는 아주 평범하면서 최악의 메모리가 정리 되지 않는 상태가 된다. 

이클립스의 DDMS에서 또는 adb shell에서  힙 메모리 덤프떠서 메모리의 상태를 분석 하면 된다.




분석 하기

덤프를 뜨게 되면 사용되는 객체별로 메모리의 할당량을 그래프를 통해서 볼 수 있다. 여기서 다른 객체에 비해 메모리를 가장 많이 차지하는 (a)AnimationDrawable 가 가장 의심스럽다고 판단된다.  

좀 더 상세하게 살펴 보면 다음과 같이 2개의 힙사이즈를 볼 수 있다.

Shallow Heap : 객체 하나당 크기.

Retained Heap : 참조가 유지하고 있는 모든 객체의 크기.

예를 들어 Retained Heap은 B와 C가 A를 참조 하고 있기때문에 300이 되고, 각각의 Shallow Heap은 100이 된다. 

툴 상단 리스트형태로 보게되면 좀 더 상세하게 힙메모리를 분석 할 수있다. 여기서 Retained Heap사이즈를 통해 메모리가 누수되는지 추측이 가능하다. mDrawable을 보면 Shallow Heap이 408 Retained Heap이 776인것을 볼 수있는데, 776이 나오는 것은 하위 모든 Retained Heap + 자신의 Shallow Heap을 더하면 된다. 

mDrawable의 Retained Heap 776으로 표시 되어 있지만, 실제로 계산 해보면  408+24+24+64+296 = 816 이 나온다. 즉 어디선가 40만큼의 메모리를 참조 하고 있다는 것이다. 결론적으로 24, 24, 64는 최소의 단위이기때문에 mImageViewAnimated가 메모리가 누수되고 있다고 판단 할 수 있으며, 이 부분을 수정을 해야한다.

런타임시 힙 덤프 뜨기

Thread.UncaughtExceptionHandler를 이용해서 예외발생시 OutOfMemoryError일 경우 android.os.Debug.dumpHprofData를 이용해서 덤프를 뜰 수 있다. 

public class MyActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Thread.currentThread().setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
    }

    public static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread thread, Throwable ex) {
            if (ex.getClass().equals(OutOfMemoryError.class)) {
                try {
                    android.os.Debug.dumpHprofData("/sdcard/dump.hprof");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            ex.printStackTrace();
        }
    }
}

이렇게 런타입시 OutOfMemoryException이 발생하면 /sdcard/dump.hprof가 생성된다. 이 파일을 보기위해서 달빅 고유포멧을 자바표준 포멧으로 바꾸기위해 Android SDK에 있는 hprof-conv툴을 쓰면된다. 

$hprof-conv 원본.hprof 표준변환된.hprof

이렇게 변환 후 MAT툴을 톨해 분석 하면된다.

참고: http://blogs.innovationm.com/android-out-of-memory-error-causes-solution-and-best-practices/