힙 분석과 메모리 프로파일링으로 누수 진단하기
Heap Analysis, Memory Profiling, Leak Detection의 진단 방식과 Valgrind, AddressSanitizer 기반 메모리 문제 분석 방법을 정리한다.
2026-08-14 · 최초 발행 2026-01-25
힙에서 확인해야 할 상태
동적 할당 메모리는 런타임에 malloc, new 등으로 확보된다. 할당과 해제를 프로그래머가 명시적으로 다루는 환경에서는 누수뿐 아니라 반복된 할당·해제로 인한 파편화도 운영 문제로 이어질 수 있다. 힙은 일반적으로 낮은 주소에서 높은 주소 방향으로 성장한다.
힙 분석은 현재 점유 중인 메모리와 그 분포를 확인하고, 어느 코드 경로가 할당을 만들었는지 추적하는 작업이다. 도달 가능한 객체와 실제 사용 중인 객체를 구분하면 불필요한 할당을 줄이거나 메모리 재사용 방식을 검토할 근거가 생긴다.
| 메트릭 | 확인하는 내용 | 중요도 |
|---|---|---|
| Allocated Size | 현재 할당된 총 메모리 크기 | 높음 |
| Live Objects | 도달 가능한 객체 수 | 높음 |
| Allocation Rate | 시간당 메모리 할당 속도 | 중간 |
| Fragmentation | 메모리 파편화 정도 | 중간 |
| Peak Usage | 최대 메모리 사용량 | 높음 |
할당 이력을 수집하는 방식
메모리 프로파일링은 할당·해제의 빈도와 크기, 시간에 따른 사용량 변화, 메모리 레이아웃과 접근 패턴, 객체 수명을 대상으로 한다. 이 데이터를 통해 메모리 사용이 집중되는 경로와 병목을 찾는다.
샘플링 기반 방식은 주기적으로 메모리 상태를 수집해 오버헤드를 낮춘다. 반면 계측 방식은 모든 할당·해제 호출을 후킹해 더 정확한 데이터를 남긴다. 중요 경로에는 instrumentation을, 나머지에는 sampling을 적용하는 혼합 방식도 가능하다. 선택 기준은 정확도와 성능 오버헤드의 균형이다.
할당 시점의 호출 스택을 기록하면 많은 메모리를 만드는 hot path를 확인할 수 있다. 동일한 스택의 할당을 합산하고, Flame Graph로 시각화하면 전체 사용량 속에서 할당 핫스팟의 위치가 드러난다.
Valgrind Massif는 시간에 따른 힙 사용량 그래프를 제공한다. gperftools(TCMalloc)는 CPU와 heap 프로파일링을 통합하며, jemalloc profiling은 내장 기능으로 낮은 오버헤드를 목표로 한다. Heaptrack은 GUI 기반의 힙 메모리 추적 도구다.
누수는 도달 가능성으로 판별한다
누수는 포인터가 완전히 사라져 해제할 수 없는 Definitely Lost, 누수된 객체가 참조하던 Indirectly Lost, 내부 포인터만 남아 의심되는 Possibly Lost, 종료 시점까지 포인터가 남아 있는 Still Reachable로 나눠 볼 수 있다.
참조 추적은 모든 포인터 참조를 따라가 도달할 수 없는 메모리를 가려낸다. 마크 앤 스윕은 루트에서 출발해 도달 가능한 객체를 마킹한 뒤 남은 객체를 누수로 판정한다. 할당 시점을 세대로 구분해 오래된 할당부터 살피거나, 서로 다른 시점의 힙 스냅샷을 비교해 증가분을 찾는 방법도 있다.
Valgrind Memcheck은 각 바이트의 할당 상태를 추적하는 Shadow Memory를 유지한다. 할당 블록 전후의 Red Zone으로 오버플로우를 찾고, 해제된 메모리 접근이나 초기화되지 않은 메모리 사용도 감지한다. 종료 시에는 누수 유형별 Leak Summary를 제공한다.
AddressSanitizer(ASan)는 LLVM/GCC에 통합된 메모리 오류 탐지기다. 8바이트당 1바이트 메타데이터를 사용하는 Shadow Memory 최적화로 오버헤드를 줄이며, Use-after-free와 heap buffer overflow를 즉시 탐지한다. 실행 속도는 Valgrind 대비 2배 정도 빠르다.
증상에서 원인으로 좁혀 가는 사례
점진적으로 메모리 사용량이 커지는 서버에서는 요청 객체의 해제를 먼저 의심할 수 있다.
// 문제 코드
void process_requests() {
while (true) {
Request* req = parse_request();
handle_request(req);
// free(req) 누락
}
}
이 경우 Valgrind는 "definitely lost"를 보고할 수 있으며, parse_request()에서 할당한 메모리가 해제되지 않은 것이 원인이다. handle_request() 뒤에 free(req)를 추가하는 방식으로 해결한다.
Python의 양방향 연결 리스트에서는 순환 참조도 확인 대상이다.
# 문제 코드
class Node:
def __init__(self, value):
self.value = value
self.next = None
self.prev = None
# 양방향 연결 리스트에서 순환 참조
Python GC가 수집하지 못하는 객체가 늘어나면 gc.get_objects()로 객체 수를 모니터링할 수 있다. 순환 참조 때문에 reference count가 0이 되지 않는지 확인하고, weakref 사용이나 명시적인 del 구현을 검토한다.
사용 가능한 메모리가 남아 있는데도 할당이 실패한다면 파편화가 원인일 수 있다. jemalloc stats로 파편화 정도를 확인하고, 다양한 크기의 빈번한 할당·해제가 문제를 심화했는지 본다. 메모리 풀이나 slab allocator 도입이 대응 방법이 된다.
운영 중에는 RSS와 힙 덤프를 함께 본다
런타임에서 관찰할 지표는 RSS(Resident Set Size), VSZ(Virtual Size), Heap Size, Mapped Memory다. RSS는 프로세스가 실제로 점유한 물리 메모리이고, VSZ는 가상 주소 공간의 크기다. Heap Size는 brk/sbrk로 할당된 힙 영역의 총 크기이며, Mapped Memory는 mmap으로 매핑된 영역을 뜻한다.
Prometheus와 Node Exporter로 시스템 메모리 메트릭을 수집하고 Grafana에서 시계열로 확인할 수 있다. RSS 임계치 초과 알림과 애플리케이션별 메모리 풀 사용률 같은 Custom Metrics를 함께 두면 문제를 좁히기 쉽다.
특정 조건에서는 jemalloc prof.dump로 힙 프로파일 덤프를 남기고, gcore로 실행 중인 프로세스의 코어 덤프를 생성할 수 있다. GDB 스크립트로 분석을 자동화하고, 서로 다른 시점의 덤프를 비교해 증가분을 분석한다.
더 낮은 계층에서 보는 메모리 접근
Dynamic Binary Instrumentation은 소스 없이 바이너리만 있는 상황에서도 메모리 누수를 분석할 수 있다. Pin Framework는 런타임 코드 삽입으로 메모리 접근을 추적하고, DynamoRIO는 동적 코드 조작 프레임워크를 제공한다. Frida는 모바일 및 데스크톱 애플리케이션의 동적 분석에 사용된다.
커널 레벨에서는 eBPF로 메모리 할당 이벤트를 추적할 수 있다. bcc tools의 memleak, slabratetop, 하드웨어 PMU를 활용하는 perf mem, 커널과 사용자 공간 이벤트를 스크립팅하는 SystemTap도 분석 수단이다.
메모리 사용 패턴에는 Time Series Anomaly Detection, 유사한 할당 패턴을 묶는 Clustering, 증가 추세를 다루는 Predictive Modeling을 적용할 수 있다. 로그 데이터와 메모리 패턴의 상관관계를 분석하는 Automated Root Cause Analysis도 이 범주에 속한다.
언어와 런타임에 맞춘 도구 선택
C/C++에서는 Valgrind Memcheck, AddressSanitizer, Windows 환경의 Dr. Memory, 버퍼 오버플로우 탐지에 특화된 Electric Fence를 활용할 수 있다.
Java는 Java VisualVM으로 힙 덤프와 프로파일링을 수행하고, Eclipse MAT(Memory Analyzer Tool)로 메모리를 분석한다. JProfiler는 상용 프로파일러이며, jmap을 통한 Heap Histogram은 클래스별 인스턴스 수를 확인하는 데 쓰인다.
Python에서는 표준 라이브러리 tracemalloc, 라인별 사용량을 측정하는 memory_profiler, 객체 참조 그래프를 시각화하는 objgraph, 사용량 요약과 누수 탐지를 위한 pympler가 있다.
Rust 바이너리도 Valgrind로 분석할 수 있다. heaptrack은 Rust 애플리케이션의 힙을 추적하고, Miri는 Rust MIR 인터프리터로 UB(Undefined Behavior)를 탐지한다. cargo-flamegraph는 메모리 할당 flame graph 생성에 사용된다.
개발과 운영에 연결하는 분석 체계
개발 초기부터 메모리 분석 도구를 통합하고, 빌드 파이프라인에 메모리 테스트를 넣는다. Debug 빌드에서는 ASan과 MSan을 활성화하고, 메모리 관리 코드는 코드 리뷰에서 집중적으로 확인한다.
운영 환경에서는 jemalloc profiling처럼 낮은 오버헤드 도구로 상시 모니터링하고, 서비스별 Memory Budget을 설정한다. 메모리가 부족할 때 기능을 축소하는 Graceful Degradation과 장기 실행 서비스의 예방적 재시작도 검토 대상이다.
릴리스 간 메모리 사용량을 비교하는 Regression Testing, Pyroscope·Parca를 활용한 Continuous Profiling, 메모리 증가율 기반의 동적 Alerting Thresholds, 주간 분석 리포트 자동 생성을 조합하면 분석이 일회성 조사에 머물지 않는다.
Heap Analysis, Memory Profiling, Leak Detection은 메모리 누수와 파편화를 조기에 발견하고 시스템 안정성과 성능을 다루기 위한 기법이다. 언어별 도구 선택, CI/CD 통합, 자동화된 분석 파이프라인은 운영 비용과 서비스 품질에 영향을 준다. eBPF와 머신러닝 기반 이상 탐지는 6G 시대의 대규모 분산 시스템에서도 더 정교한 메모리 운영을 가능하게 한다.