Heap Dump(힙 덤프)는 애플리케이션의 동적 메모리 스냅샷으로, 모든 라이브 객체에 대한 완전한 정보를 포함합니다: 클래스, 크기, 상호 참조 및 GC 루트로부터의 접근 가능성. Heap Dump는 메모리 누수 분석 및 리소스 소비 최적화를 위한 주요 도구입니다. Android Developers에 따르면, heap dump 분석은 순환 참조, 잊혀진 리스너 및 해제되지 않은 정적 참조를 포함하여 최대 95%의 메모리 누수를 탐지할 수 있습니다.
핵심 요점
Heap dump는 가상 머신 힙의 완전한 덤프입니다 — 모든 동적으로 생성된 객체가 상주하는 메모리 영역. Java 및 Kotlin에서는 Android의 Dalvik/ART 힙이고, Swift 및 Objective-C에서는 iOS의 ARC 관리 힙입니다. Heap dump는 모든 객체, 해당 클래스, 크기, 필드, 다른 객체에 대한 참조 및 GC 루트로부터의 접근 가능성 플래그를 캡처합니다.
Heap dump의 주요 목적은 메모리 누수 탐지입니다. 누수는 애플리케이션이 더 이상 필요하지 않은 객체에 대한 참조를 계속 유지하여 가비지 컬렉터에 의한 수집을 방지할 때 발생합니다. 일반적인 원인: 액티비티가 소멸될 때 등록 해제되지 않은 이벤트 리스너; 컨텍스트에 대한 참조가 있는 싱글톤; self를 캡처하는 클로저; 데이터가 제거 없이 추가되는 정적 컬렉션. Heap dump는 정확한 그림을 제공합니다: 어떤 객체가 “살아” 있는지, 어떤 것이 불필요한지, 그리고 누가 실제로 이를 참조하고 있는지.
Google I/O에 따르면, Android 앱 충돌 보고서의 60% 이상이 OutOfMemoryError와 관련되어 있으며, 80%의 경우 근본 원인은 heap dump를 통해 탐지 가능한 메모리 누수입니다. iOS 앱의 경우 상황은 유사합니다: retain cycle로 인한 누수는 충돌의 가장 흔한 원인 중 하나이며, Xcode의 Allocations 인스트루먼트를 통해 식별됩니다.
Heap dump는 다음과 같은 증상이 나타날 때 수행해야 합니다: 애플리케이션이 반복적인 작업(화면 간 앞뒤 이동) 중에 선형적으로 메모리를 소비합니다; 화면을 닫은 후 메모리가 기준 수준으로 돌아오지 않습니다; OutOfMemoryError 또는 iOS의 메모리 경고가 발생합니다; 메모리 제한 초과로 인해 애플리케이션이 종료됩니다. Instagram 및 Spotify와 같은 대규모 모바일 프로젝트에서는 정기적인 heap dump 수집이 엔지니어링 문화 프로토콜의 일부입니다.
Android Studio는 Memory Profiler를 제공합니다 — 실시간으로 힙 덤프를 캡처하기 위한 내장 도구. View → Tool Windows → Profiler를 통해 액세스할 수 있습니다. 애플리케이션을 실행한 후 세션을 선택하고 Memory 탭으로 이동한 다음 Dump Java Heap을 클릭합니다. Android Studio가 애플리케이션을 일시 중지하고 ART 힙 덤프를 수행한 후 분석을 위해 결과를 로드합니다. 덤프 파일은 .hprof 형식입니다 — 대부분의 메모리 분석기와 호환되는 HPROF 표준입니다.
덤프를 로드한 후 Android Studio는 열이 있는 객체 테이블을 표시합니다: Allocations(인스턴스 수), Native Size(ART 힙 외부 메모리), Shallow Size(객체 자체의 메모리), Retained Size(전체 하위 그래프를 포함한 객체의 메모리). 클래스 이름으로 필터링, retained size로 정렬 및 패키지별 검색을 통해 문제 영역을 빠르게 찾을 수 있습니다.
// 일반적인 누수 — onDestroy에서 등록 해제되지 않은 리스너
class MainActivity : AppCompatActivity() {
private val sensorManager by lazy {
getSystemService(SENSOR_SERVICE) as SensorManager
}
private val listener = MySensorListener()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sensorManager.registerListener(listener,
sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT),
SensorManager.SENSOR_DELAY_NORMAL)
}
override fun onDestroy() {
super.onDestroy()
// ❌ sensorManager.unregisterListener(listener) 누락
// → Activity가 GC되지 않으며 heap dump가 누수를 표시함
}
}
Dominator Tree 탭은 가장 많은 메모리를 보유한 객체를 표시합니다. 객체가 dominator tree에서 제거되면 해당 객체가 보유한 모든 메모리가 가비지 컬렉션에 사용 가능해집니다. 이는 핵심 도구입니다: 수천 개의 객체를 스캔하는 대신 메모리의 80~90%를 제어하는 10~20개에 집중합니다. Google에 따르면, dominator tree 분석은 누수 지점을 찾는 가장 효과적인 방법이며 분석 시간을 몇 시간에서 몇 분으로 단축합니다.
Xcode Instruments는 힙 덤프 작업을 위한 두 가지 도구를 제공합니다: Allocations — 실시간 소비 그래프와 함께 힙 덤프 캡처; Leaks — retain cycle 분석을 통한 자동 누수 검색. Allocations는 힙의 모든 객체, 해당 크기, 생성(allocations) 및 해제(deallocations) 수를 표시합니다. 특정 클래스의 생성 수와 해제 수의 차이는 잠재적 누수를 나타냅니다.
Allocations에서 힙 덤프 캡처는 Snapshot Memory 버튼으로 수행합니다 — 도구가 애플리케이션을 일시 중지하고 전체 덤프를 가져옵니다. 그 후 표준 보기를 사용할 수 있습니다: 클래스별 객체 목록, 각 객체의 콜 트리 및 보고서 생성기. Android Studio와 달리 Xcode는 .hprof를 사용하지 않고 Instruments와 호환되는 자체 .trace 형식으로 데이터를 저장합니다.
// 일반적인 iOS 누수 — 클로저를 통한 retain cycle
class NetworkManager {
var onComplete: ((Data) -> Void)?
func startRequest() {
// ❌ 클로저가 self를 캡처 — retain cycle
onComplete = { data in
self.process(data)
}
}
func process(_ data: Data) {}
}
Leaks 인스트루먼트는 참조 그래프 분석을 통해 retain cycles 및 누수를 자동으로 감지합니다. 누수되는 객체를 보라색 아이콘으로 표시하고 루트(GC root)까지의 경로를 보여줍니다. retain cycle을 제거하려면 클로저 캡처에 [weak self] 또는 [unowned self]를 추가하기만 하면 됩니다. Swift를 사용하는 팀에서 Leaks 인스트루먼트의 정기적인 실행은 CI 파이프라인의 필수 단계입니다.
// 수정 — self에 대한 약한 참조
onComplete = { [weak self] data in
guard let self else { return }
self.process(data)
}
Heap dump를 올바르게 분석하려면 세 가지 주요 지표를 이해해야 합니다. Shallow size는 객체가 직접 점유하는 메모리 양입니다: 필드, 헤더 및 정렬. 일반적인 Java/Kotlin 객체의 경우 shallow size는 16~40바이트입니다. Retained size는 객체의 shallow size와 이 객체를 통해서만 접근 가능한 모든 객체의 총 shallow size입니다(즉, 제거되면 가비지가 되는 객체). Retained size는 메모리 소비에 대한 객체의 실제 영향을 보여줍니다.
| 지표 | 설명 | 예시 |
|---|---|---|
| Shallow size | 객체 자체의 크기(바이트) | Bitmap (100×100) = 40,016 B |
| Retained size | Shallow size + 보유하는 모든 것 | View Tree가 있는 Activity = 2~5 MB |
| Deep size | Retained size + 다른 그래프의 중첩 객체 | 어댑터가 있는 ScrollView = 10~50 MB |
Dominator tree는 각 객체가 “지배자”를 참조하는 구조입니다 — 접근 가능성을 제어하는 객체. 지배자가 제거되면 해당 하위 트리의 모든 객체가 가비지가 됩니다. Dominator tree 분석은 어떤 객체가 가장 많은 메모리를 보유하는지 찾는 가장 빠른 방법입니다. Eclipse MAT에 따르면, 90%의 누수가 5분 안에 top-20 dominator tree를 검토하여 탐지됩니다.
Heap dump를 통한 누수 분석 프로세스는 여러 단계로 구성됩니다. 1단계: 메모리를 해제해야 하는 작업을 수행합니다(화면 닫기, 작업 완료). 2단계: GC를 호출하고 heap dump를 가져옵니다. 3단계: 파괴되었어야 할 객체를 찾습니다. 4단계: 의심스러운 객체에 대해 Path to GC Roots를 실행합니다 — 객체를 살아있게 유지하는 참조 체인. 체인의 마지막 참조가 누수 원인입니다.
Path to GC Roots 기능은 Android Studio Profiler, Eclipse MAT 및 Xcode Instruments에서 사용할 수 있습니다. GC 루트에서 문제 객체까지의 최단 참조 체인을 보여줍니다. 약한(weak) 참조와 부드러운(soft) 참조를 제외하면 실제로 가비지 컬렉션을 방해하는 강한(strong) 참조만 얻을 수 있습니다. Square Engineering에 따르면, Android 앱의 누수 70%는 단 두 가지 패턴으로 인해 발생합니다: Activity 또는 Context에 대한 정적 참조와 등록되었지만 등록 해제되지 않은 리스너.
// 정적 참조를 통한 누수 예시
object AppCache {
private val cache = mutableMapOf<String, Any>()
fun storeActivityReference(activity: Activity) {
cache["current_activity"] = activity // ❌ 누수!
}
}
// 수정: 약한 참조
object AppCacheFixed {
private val cache = mutableMapOf<String, WeakReference<Any>>()
}
비교 모드 기술은 누수를 찾는 가장 효과적인 방법 중 하나입니다. 반복 작업 전후에 힙 덤프를 가져옵니다. 주요 클래스의 인스턴스 수를 비교합니다: 모든 액티비티가 닫혔음에도 Activity 수가 증가했다면 — 누수입니다. Android Studio 및 Eclipse MAT는 차이점을 강조 표시하는 자동 덤프 비교를 지원합니다. Google에 따르면, 덤프 비교는 누적 효과로 인해 단일 분석에서는 보이지 않는 누수를 발견할 수 있습니다.
실제 프로젝트의 힙 덤프 분석을 기반으로 입증된 메모리 최적화 방법이 개발되었습니다. WeakReference 사용: 캐시, 콜백 및 수명이 긴 객체의 컨텍스트 참조에 사용합니다. 리스너 등록 해제: Android의 경우 onPause/onDestroy, iOS의 경우 deinit에서 수행합니다. 대규모 정적 컬렉션 방지 — 필요한 경우 크기 제한이 있는 LruCache를 사용합니다. Bitmap 최적화: 올바른 inSampleSize로 이미지를 로드하고 디스크 캐시와 함께 Glide 또는 Picasso를 사용합니다.
CI 파이프라인에 정기적인 힙 덤프 캡처를 포함합니다. 계측된 UI 테스트를 실행하고 주요 사용자 시나리오를 수행하며 힙 덤프를 기준선과 비교하는 작업을 설정합니다. retained size가 기준선보다 5% 이상 증가하면 빌드가 회귀로 표시됩니다. 이 접근 방식은 Airbnb, Uber 및 높은 품질 기준을 가진 다른 회사에서 실행됩니다. Uber Engineering에 따르면, CI에서 자동 힙 덤프 분석을 구현하면 분기당 메모리 관련 버그가 70% 감소했습니다.
// CI에서 자동 힙 덤프를 위한 Gradle 태스크 예시
task profileMemory(type: Exec) {
commandLine 'adb', 'shell',
'am start -n com.example/.MainActivity'
// 로딩 대기 중
doLast {
exec { commandLine 'adb', 'shell',
'am broadcast -a com.example.DUMP_HEAP' }
}
}
자주 묻는 질문
Shallow size는 객체 자체의 크기(필드 + 헤더)입니다. Retained size는 객체와 객체가 제거될 때 가비지가 되는 모든 객체의 크기입니다. Retained size는 메모리 소비에 대한 객체의 영향을 나타내는 주요 지표입니다.
Android Studio Profiler에서 기기와 프로세스를 선택하고 Dump Java Heap을 클릭합니다. 또는 명령줄을 통해: adb shell am dumpheap PID /sdcard/dump.hprof, 그런 다음 adb pull.
힙 덤프에는 모든 라이브 객체가 포함됩니다. 애플리케이션이 캐시, Bitmap 또는 대용량 데이터를 처리하는 경우 덤프가 수백 메가바이트에 도달할 수 있습니다. 클래스별로 필터링하거나 Eclipse MAT를 사용하여 인덱스만 로드합니다.
네, Eclipse MAT(Memory Analyzer Tool)를 사용하세요 — .hprof 파일 분석을 위한 무료 도구입니다. Dominator tree, path to GC roots, 덤프 비교 및 Leak Suspects Report를 통한 자동 누수 탐지를 지원합니다.
덤프 자체 — 네, 덤프 수집이 모든 스레드를 중지하기 때문입니다(stop-the-world). 덤프 없이 — 아니요. 제어된 환경(테스트 벤치, CI)에서 덤프를 가져오고 프로덕션에서는 가져오지 마세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.