lightbulb_outline Help shape the future of the Google Play Console, Android Studio, and Firebase. Start survey

Memory Profiler로 Java 힙 및 메모리 할당 보기

Memory Profiler는 Android Profiler의 한 구성 요소로서, 버벅거림, 멈춤, 심할 경우 앱의 비정상 종료로 이어질 수 있는 메모리 누수와 메모리 변동을 파악하는 데 도움이 됩니다. 앱의 메모리 사용 현황을 실시간 그래프로 보여주고, 힙 덤프를 캡처할 수 있게 해주고, 가비지 수집을 강제 실행하며, 메모리 할당을 추적합니다.

Memory Profiler를 열려면 다음 단계를 따르세요.

  1. View > Tool Windows > Android Profiler를 클릭합니다(툴바에서 Android Profiler 를 클릭해도 됨).
  2. Android Profiler 툴바에서 프로파일링하려는 기기 및 앱 프로세스를 선택합니다. USB를 통해 기기를 연결했는데 목록에 표시되지 않을 경우 USB 디버깅을 활성화했는지 확인하세요.
  3. **MEMORY **타임라인의 아무 곳이나 클릭하면 Memory Profiler가 열립니다.

또는 dumpsys로 명령줄에서 앱 메모리를 검사할 수 있고 logcat에서 GC 이벤트를 확인할 수도 있습니다.

앱 메모리를 프로파일링해야 하는 이유

Android는 관리되는 메모리 환경을 제공합니다. 그래서 Android는 앱에서 어떤 객체를 더 이상 사용하지 않는다고 판단하면 가비지 수집기가 미사용 메모리를 다시 힙에 내놓습니다. Android가 미사용 메모리 찾기를 시작하는 방법은 꾸준히 개선되고 있지만, 모든 Android 버전의 어떤 지점에서는 시스템이 개발자의 코드를 잠깐 일시 중지해야 합니다. 대부분의 경우, 이러한 일시 중지는 감지할 수 없는 수준입니다. 하지만 앱이 시스템에서 수집할 수 있는 속도보다 빠르게 메모리를 할당하는 경우 수집기가 할당 요청을 충족시킬 만큼 충분한 메모리를 확보하는 동안 앱이 지연될 수 있습니다. 이런 지연으로 인해 앱이 프레임을 건너뛰고 눈에 띄게 느려질 수 있습니다.

앱이 속도 저하를 표시하지 않더라도 메모리 누수가 발생할 경우 앱이 백그라운드에 있는 동안에도 그 메모리를 유지할 수 있습니다. 이런 동작으로 인해 불필요한 가비지 수집 이벤트를 강제하게 됨으로써 시스템의 메모리 성능 중 나머지 부분이 저하될 수 있습니다. 결국, 시스템으로서는 메모리를 확보하려고 앱 프로세스를 종료할 수밖에 없습니다. 이후에 사용자가 앱으로 돌아올 때 앱을 완전히 다시 시작해야 합니다.

이런 문제를 예방하려면 Memory Profiler를 사용하여 다음 작업을 수행해야 합니다.

  • 성능 문제의 원인일 수 있는 타임라인에서 바람직하지 않은 메모리 할당 패턴이 있는지 찾아봅니다.
  • Java 힙을 덤프하여 특정 시점에 어떤 객체가 메모리를 고갈시키는지 확인합니다. 오랜 기간에 걸쳐 힙 덤프를 여러 차례 수행하면 메모리 누수 식별에 도움이 될 수 있습니다.
  • 정상적인 사용자 상호작용과 극단적인 사용자 상호작용 중의 메모리 할당을 기록하여 코드가 짧은 시간에 너무 많은 객체를 할당하거나 누출되는 객체를 할당하는 곳이 정확히 어딘지 파악합니다.

앱의 메모리 사용량을 줄일 수 있는 프로그래밍 방법에 대한 자세한 내용은 앱의 메모리 관리를 참조하세요.

Memory Profiler 개요

Memory Profiler를 처음 열면 앱의 메모리 사용 현황을 자세히 보여주는 타임라인과 가비지 수집을 강제 실행하고 힙 덤프를 캡처하고 메모리 할당을 기록하기 위한 액세스 도구가 보일 것입니다.

그림 1. Memory Profiler

그림 1에 표시된 것처럼, Memory Profiler의 기본 뷰에는 다음 사항이 포함됩니다.

  1. 가비지 수집 이벤트를 강제 적용하는 버튼
  2. 힙 덤프를 캡처하는 버튼
  3. 메모리 할당을 기록하는 버튼. Android 7.1 또는 이전 버전을 실행 중인 기기에 연결되어 있을 때만 이 버튼이 나타납니다.
  4. 타임라인을 확대/축소하는 버튼
  5. 라이브 메모리 데이터로 앞으로 이동하는 버튼
  6. activity 상태, 사용자 입력 이벤트 및 화면 회전 이벤트를 표시하는 이벤트 타임라인
  7. 다음을 포함하는 메모리 사용 타임라인
    • 각 메모리 범주에서 사용 중인 메모리의 양을 보여주는 누적 그래프로, 왼쪽의 y축에 메모리 양이 표시되며 맨 위에 색상 키가 표시됩니다.
    • 파선은 할당된 객체의 수를 나타내는데 오른쪽의 y축에 표시됩니다.
    • 각 가비지 수집 이벤트에 대한 아이콘

하지만 Android 7.1 또는 이전 버전을 실행 중인 기기를 사용하는 경우에는 기본적으로 일부 프로파일링 데이터가 표시되지 않습니다. "Advanced profiling is unavailable for the selected process"라는 메시지가 나타나면 고급 프로파일링을 활성화 하여 다음 사항을 확인해야 합니다.

  • 이벤트 타임라인
  • 할당된 객체의 수
  • 가비지 수집 이벤트

Android 8.0 이상에서는 디버그 가능한 앱에 대해 고급 프로파일링이 항상 활성화됩니다.

메모리 카운트 방법

Memory Profiler(그림 2) 맨 위에 보이는 숫자는 Android 시스템에 따라 앱이 커밋한 모든 개인 메모리 페이지 수를 기준으로 합니다. 시스템이나 다른 앱과 공유하는 페이지는 이 수에 포함되지 않습니다.

그림 2. Memory Profiler의 맨 위에 있는 메모리 카운트 범례

메모리 카운트의 범주는 다음과 같습니다.

  • Java: Java 또는 Kotlin 코드에서 할당된 객체의 메모리
  • Native: C 또는 C++ 코드에서 할당된 객체의 메모리

    앱에 C++를 사용하지 않더라도 여기서 몇몇 네이티브 메모리가 사용되는 것을 볼 수도 있습니다. 자신이 작성한 코드가 Java 또는 Kotlin일지라도 이미지 자산과 기타 그래픽을 처리할 때와 같이, Android 프레임워크는 네이티브 메모리를 사용하여 개발자 대신 다양한 작업을 처리하기 때문입니다.

  • Graphics: GL 표면, GL 텍스처 등을 비롯하여, 화면에 픽셀을 표시하기 위해 그래픽 버퍼 큐에 사용되는 메모리 (참고로, 이 메모리는 전용 GPU 메모리가 아니라 CPU와 공유됨)

  • Stack: 앱에서 네이티브 스택과 Java 스택에 모두 사용되는 메모리입니다. 이 메모리는 보통 앱에서 실행 중인 스레드 수와 관계가 있습니다.

  • Code: dex 바이트코드, 최적화 또는 컴파일된 dex 코드, .so 라이브러리, 글꼴 등과 같이, 앱이 코드와 리소스를 위해 사용하는 메모리

  • Other: 앱에서 사용하는 메모리 중 시스템이 확실히 분류하지 못하는 메모리

  • Allocated: 앱에서 할당한 Java/Kotlin 객체의 수로, C 또는 C++에서 할당되는 객체는 계산하지 않습니다.

    Android 7.1 및 이전 버전을 실행 중인 기기에 연결할 때, Memory Profiler가 실행 중인 개발자 앱에 연결되었을 때만 이 할당 카운트가 시작됩니다. 프로파일링을 시작하기 전에 할당되는 객체는 계산되지 않습니다. 하지만 Android 8.0에는 모든 할당을 계속 추적하는 온디바이스 프로파일링 도구가 포함되어 있으므로, 항상 이 숫자는 Android 8.0 이상에서 작동하는 앱에서 나타나는 Java 객체의 총 개수를 나타냅니다.

이전의 Android Monitor 도구에서 확인되는 메모리 카운트와 비교해 볼 때, 새로운 Memory Profiler는 메모리를 다르게 기록하므로 메모리 사용량이 더 많은 것처럼 보일 수 있습니다. Memory Profiler는 몇몇 범주를 더 모니터링하므로 메모리 총량이 늘어나지만, Java 힙 메모리에만 관심이 있다면 "Java" 숫자가 이전 도구에서 확인한 값과 비슷할 것입니다.

Java 숫자는 아마 Android Monitor에서 본 숫자와 정확히 일치하지는 않겠지만, 새로운 숫자는 Zygote에서 분기된 이후로 앱의 Java 힙에 할당된 모든 물리적 메모리 페이지 수를 나타냅니다. 따라서 이 숫자는 앱이 실제로 사용 중인 물리적 메모리의 양을 정확하게 표시합니다.

참고: 현재, Memory Profiler는 앱에서 실제로 프로파일링 도구에 속한 일부 가양성 네이티브 메모리 사용량도 보여줍니다. 10만 개까지의 객체를 위해 최대 10MB의 메모리가 추가됩니다. 이러한 도구의 향후 버전에서는 이들 숫자가 데이터에서 필터링을 통해 걸러질 것입니다.

메모리 할당 보기

메모리 할당은 메모리에 있는 각각의 객체가 할당된 방법을 보여줍니다. 특히, Memory Profiler는 객체 할당에 대해 다음 정보를 보여줄 수 있습니다.

  • 할당된 객체의 유형과 이들 객체가 사용하는 공간의 양
  • 어떤 스레드에 있는지를 포함하여, 각 할당의 스택 추적
  • 객체가 할당 취소된 시점(Android 8.0 이상을 실행 중인 기기를 사용할 때만 해당)

기기에서 Android 8.0 이상을 실행 중인 경우 다음과 같이 언제든 객체 할당을 볼 수 있습니다. 그냥 타임라인에서 클릭한 채로 드래그하여 할당을 보고 싶은 영역을 선택하면 됩니다(동영상 1 참조). Android 8.0 이상에는 앱의 할당을 계속 추적하는 온디바이스 프로파일링 도구가 포함되어 있으므로 기록 세션을 시작할 필요가 없습니다.

동영상 1. Android 8.0 이상을 사용할 경우 기존 타임라인 영역을 선택하면 객체 할당을 볼 수 있음

기기에서 Android 7.1 또는 이전 버전을 실행 중인 경우에는 Memory Profiler 툴바에서 Record memory allocations 을 클릭하세요. Android Monitor는 기록하는 동안 앱에서 발생하는 모든 할당을 추적합니다. 다 마쳤으면 Stop recording (같은 버튼. 동영상 2 참조)을 클릭하여 할당을 볼 수 있습니다.

동영상 2. Android 7.1 또는 이전 버전을 사용할 경우에는 메모리 할당을 명시적으로 기록해야 함

타임라인의 한 영역을 선택하거나 Android 7.1 또는 이전 버전을 실행 중인 기기로 기록 세션을 마치면 타임라인 아래에 할당된 객체의 목록이 나타나는데, 객체가 클래스 이름별로 분류되고 힙 수를 기준으로 정렬되어 표시됩니다.

참고: Android 7.1 및 이전 버전에서는 최대 65,535개의 할당을 기록할 수 있습니다. 기록 세션이 이 제한을 초과하는 경우 최근 65,535개의 할당만 기록에 저장됩니다. (Android 8.0 이상에서는 실질적 제한이 없습니다.)

할당 기록을 검사하려면 다음 단계를 따르세요.

  1. 목록을 살펴보며 비정상적으로 힙 개수가 많고 누수가 발생할 수 있는 객체를 찾습니다. Class Name 열 헤더를 클릭하여 사전순으로 정렬하면 알려진 클래스를 쉽게 찾을 수 있습니다. 그런 다음, 클래스 이름을 클릭합니다. 오른쪽에 나타나는 Instance View 창에는 그림 3.1과 같이 해당 클래스의 각 인스턴스가 표시됩니다.
  2. Instance View 창에서 인스턴스를 클릭합니다. 그러면 아래에 Call Stack 탭이 나타나면서 그 인스턴스가 할당된 위치와 정확히 어떤 스레드에 할당되었는지 표시됩니다.
  3. Call Stack 탭에서 아무 라인이나 클릭하면 편집기에서 해당 코드로 이동할 수 있습니다.

그림 3. 할당된 각 객체에 대한 세부 정보가 오른쪽의 Instance View에 나타남

기본적으로, 왼쪽에 있는 할당 목록은 클래스 이름을 기준으로 정렬됩니다. 목록 맨 위에서 오른쪽에 있는 드롭다운을 사용하여 다음 정렬 방법 사이에서 전환할 수 있습니다.

  • Arrange by class: 클래스 이름을 기준으로 모든 할당을 그룹화합니다.
  • Arrange by package: 패키지 이름을 기준으로 모든 할당을 그룹화합니다.
  • Arrange by callstack: 모든 할당을 해당 호출 스택으로 그룹화합니다.

힙 덤프 캡처

힙 덤프는 앱에서 어떤 객체가 힙 덤프를 캡처하는 시점에 메모리를 사용하는 중인지 보여줍니다. 특히 장시간의 사용자 세션 후, 힙 덤프는 메모리에 더 이상 남아 있으면 안 된다고 생각했는데 여전히 메모리에 있는 객체를 보여줌으로써 메모리 누수를 식별하는 데 도움이 될 수 있습니다. 힙 덤프를 캡처하고 나면 다음 내용을 볼 수 있습니다.

  • 앱이 할당한 객체의 유형과 각 객체의 수
  • 각각의 객체가 사용 중인 메모리의 양
  • 코드에서 각 객체에 대한 참조가 유지되는 위치
  • 객체가 할당된 위치에 대한 호출 스택 (할당을 기록하는 동안 힙 덤프를 캡처할 때, 현재는 Android 7.1 및 이전 버전에서만 힙 덤프와 함께 호출 스택을 사용할 수 있습니다.)

그림 4. 힙 덤프 보기

힙 덤프를 캡처하려면 Memory Profiler 툴바에서 Dump Java heap 을 클릭하세요. 힙을 덤프하는 동안 Java 메모리의 양이 일시적으로 증가할 수 있습니다. 힙 덤프가 앱과 동일한 프로세스에서 발생하고 데이터를 수집하려면 일부 메모리가 필요하므로 이는 정상적인 현상입니다.

그림 4와 같이, 힙 덤프가 메모리 타임라인 아래에 나타나 힙에 있는 모든 클래스 유형을 보여줍니다.

참고: 덤프가 생성되는 시점에 대해 더 정확하게 확인해야 할 경우에는 dumpHprofData()를 호출하여 앱 코드의 중요한 지점에서 힙 덤프를 생성할 수 있습니다.

힙을 검사하려면 다음 단계를 따르세요.

  1. 목록을 살펴보며 비정상적으로 힙 개수가 많고 누수가 발생할 수 있는 객체를 찾습니다. Class Name 열 헤더를 클릭하여 사전순으로 정렬하면 알려진 클래스를 쉽게 찾을 수 있습니다. 그런 다음, 클래스 이름을 클릭합니다. 오른쪽에 나타나는 Instance View 창에는 그림 5.1과 같이 해당 클래스의 각 인스턴스가 표시됩니다.
  2. Instance View 창에서 인스턴스를 클릭합니다. 아래에 나타나는 References 탭에 그 객체에 대한 모든 참조가 표시됩니다.

    또는 인스턴스 이름 옆의 화살표를 클릭하여 인스턴스의 모든 필드를 확인한 다음에 필드 이름을 클릭하여 모든 참조를 확인합니다. 필드에 대한 인스턴스 세부 정보를 보고 싶으면 필드에서 마우스 오른쪽 버튼을 클릭하고 Go to Instance를 선택합니다.

  3. References 탭에서 메모리 누수를 일으킬 수 있는 참조를 식별하는 경우 그 참조를 마우스 오른쪽 버튼으로 클릭하고 Go to Instance를 선택합니다. 그러면 힙 덤프에서 해당 인스턴스가 선택되고 자체 인스턴스 데이터를 보여줍니다.

기본적으로, 힙 덤프는 각각의 할당된 객체에 대한 스택 추적을 보여주지는 않습니다. 스택 추적을 가져오려면 메모리 할당 기록을 시작한 후에 Dump Java heap을 클릭해야 합니다. 그러면 Instance View에서 인스턴스를 선택하고 그림 5와 같이 References 탭과 나란히 Call Stack 탭을 볼 수 있습니다. 하지만 기록 할당을 시작하기 전에 일부 객체가 할당되었을 가능성이 있으므로 해당 객체에 대해서는 호출 스택을 사용할 수 없습니다. 호출 스택을 포함하고 있는 인스턴스는 아이콘 에 '스택' 배지와 함께 표시됩니다. (하지만 아쉽게도, 스택 추적에서는 할당 기록을 수행해야 하므로 현재 Android 8.0에서는 힙 덤프에 대한 스택 추적을 볼 수 없습니다.)

힙 덤프에서 다음과 같은 원인으로 인한 메모리 누수가 있는지 살펴보세요.

  • Activity, Context, View, Drawable, 그리고 Activity 또는 Context 컨테이너에 대한 참조가 있을 수 있는 다른 객체에 대한 수명이 긴 참조
  • Activity 인스턴스를 보유하고 있을 수 있는 비정적 내부 클래스(예: Runnable)
  • 필요 이상으로 긴 객체를 가진 캐시

그림 5. 힙 덤프 캡처에 필요한 기간이 타임라인에 표시되어 있음

클래스의 목록에서 다음 정보를 확인할 수 있습니다.

  • Heap Count: 힙에 있는 인스턴스의 개수
  • Shallow Size: 이 힙에 있는 모든 인스턴스의 총 크기(단위: 바이트)
  • Retained Size: 이 클래스의 모든 인스턴스로 인해 유지되는 메모리의 총 크기(단위: 바이트)

클래스 목록의 맨 위에서 왼쪽 드롭다운 목록을 사용하여 다음 힙 덤프 간을 전환할 수 있습니다.

  • Default heap: 시스템에서 아무런 힙도 지정하지 않은 경우
  • App heap: 앱이 메모리를 할당하는 기본 힙
  • Image heap: 부팅 시간 중 미리 로드되는 클래스를 포함하는 시스템 부팅 이미지. 여기서는 할당이 절대로 이동하거나 사라지지 않도록 보장됩니다.
  • Zygote heap: Android 시스템에서 앱 프로세스가 분기되는 copy-on-write 힙

기본적으로, 힙에 있는 객체의 목록은 클래스 이름을 기준으로 정렬됩니다. 다른 드롭다운을 사용하여 다음과 같은 정렬 방법 사이에서 전환할 수 있습니다.

  • Arrange by class: 클래스 이름을 기준으로 모든 할당을 그룹화합니다.
  • Arrange by package: 패키지 이름을 기준으로 모든 할당을 그룹화합니다.
  • Arrange by callstack: 모든 할당을 해당 호출 스택으로 그룹화합니다. 기록 할당 중에 힙 덤프를 캡처하는 경우에만 이 옵션이 작동합니다. 그렇지만 기록을 시작하기 전에 힙에 할당된 객체일 가능성이 있으므로 그런 할당이 먼저 나타나며, 단순히 클래스 이름별로 표시됩니다.

기본적으로, 이 목록은 Retained Size 열을 기준으로 정렬됩니다. 열 헤더 중 어느 것이든 클릭하면 목록 정렬 방식을 변경할 수 있습니다.

Instance View에서 각 인스턴스에는 다음이 포함됩니다.

  • Depth: 임의의 GC 루트에서 선택한 인스턴스까지 가장 짧은 홉 수
  • Shallow Size: 이 인스턴스의 크기
  • Retained Size: (도미네이터 트리 이 인스턴스가 우세한 메모리의 크기

힙 덤프를 HPROF로 저장

힙 덤프를 캡처한 후 프로파일러가 실행 중인 동안에만 Memory Profiler에서 데이터를 볼 수 있습니다. 프로파일링 세션을 종료하면 힙 덤프를 잃게 됩니다. 따라서 이후에 리뷰할 목적으로 힙 덤프를 저장하고 싶으면 타임라인 아래의 툴바에서 Export heap dump as HPROF file 을 클릭하여 HPROF 파일로 힙 덤프를 내보내세요. 이때 나타나는 대화상자에서 .hprof 접미사를 붙여 파일을 저장해야 합니다.

그러면 Android Studio에서 빈 편집기 창으로 파일을 드래그하거나 파일 탭 표시줄에 드롭하여 다시 열 수 있습니다.

jhat 등의 다른 HPROF 분석기를 사용하려면 HPROF 파일을 Android 형식에서 Java SE HPROF 형식으로 변환해야 합니다.

android_sdk/platform-tools/ 디렉토리에 제공되는 hprof-conv 도구로 변환할 수 있습니다. 원본 HPROF 파일과 변환된 HPROF 파일을 쓸 위치를 나타내는 두 가지 인수를 포함한 hprof-conv 명령어를 실행합니다. 예:

hprof-conv heap-original.hprof heap-converted.hprof

메모리 프로파일링 기술

Memory Profiler를 사용하는 동안 앱 코드에 스트레스를 가해 강제로 메모리 누수를 일으켜보세요. 앱에서 메모리 누수를 유발하는 한 가지 방법은 힙을 검사하기 전에 잠깐 동안 앱이 실행되도록 두는 것입니다. 그러면 조금씩 누수되다가 힙의 할당 상한에 이르게 됩니다. 하지만 누수가 적을수록 앱을 더 오래 실행해야 이러한 현상을 볼 수 있습니다.

다음 중 한 가지 방법으로 메모리 누수를 트리거할 수도 있습니다.

  • 기기를 다양한 activity 상태에서 작동하면서 세로 모드에서 가로 모드로 돌렸다가 다시 되돌리기를 여러 차례 반복합니다. 이처럼 기기를 돌리면 앱에서 Activity, Context 또는 View 객체 누수가 발생할 수 있는데, 이는 시스템에서 Activity를 다시 생성하고 앱이 이러한 객체 중 하나에 대한 참조를 유지할 경우 이에 대한 가비지 수집을 수행할 수 없기 때문입니다.
  • 다양한 activity 상태에서 자신의 앱과 다른 앱 사이를 전환합니다(Home 화면으로 이동한 후 다시 앱으로 돌아옴).

팁: monkeyrunner 테스트 프레임워크를 사용해 위 절차를 수행할 수도 있습니다.