일반적인 메모리 사용량과 메모리 누수를 구분하는 것은 그리 간단하지 않은 경우가 많습니다. 하지만 과도한 메모리 사용과 메모리 누수는 동일한 증상을 보이므로 동일한 방식으로 분석할 수 있습니다. 분석은 의심스러운 객체를 찾고, 해당 객체가 왜 여전히 힙에 남아 있는지 파악하는 두 단계로 진행됩니다.
새 객체 찾기
메모리 누수가 있는 애플리케이션이 실행되면 시간이 지남에 따라 점점 더 많은 메모리를 소비하게 됩니다. 메모리 사용량의 증가를 감지하는 가장 좋은 방법은 VM 텔레메트리와 "All objects" 및 "Recorded objects" 뷰의 차이 비교 기능을 사용하는 것입니다. 이들 뷰를 통해 문제의 유무와 심각도를 판단할 수 있습니다. 클래스 히스토그램 테이블의 차이(difference) 열만으로도 문제가 무엇인지 파악할 수 있는 경우도 있습니다.
메모리 누수를 더 깊이 분석하려면 힙 워커의 기능이 필요합니다. 특정 유스케이스와 관련된 메모리 누수를 상세히 조사하려면 "Mark heap" 기능이 가장 적합합니다. 이 기능을 사용하면 이전의 특정 시점 이후로 힙에 남아 있는 새 객체를 식별할 수 있습니다. 이러한 객체에 대해 여전히 정당하게 힙에 남아 있는지, 아니면 더 이상 필요하지 않은 객체임에도 잘못된 참조로 인해 유지되고 있는 것인지 확인해야 합니다.
관심 있는 객체 집합을 분리하는 또 다른 방법은 할당 녹화를 사용하는 것입니다. 힙 스냅샷을 생성할 때 녹화된 모든 객체를 표시하는 옵션을 선택할 수 있습니다. 그러나 할당 녹화를 특정 유스케이스로만 제한하고 싶지 않을 수도 있습니다. 또한 할당 녹화는 오버헤드가 크기 때문에 Mark Heap 액션을 사용하는 것이 상대적으로 훨씬 적은 영향을 미칩니다. 마지막으로, 힙을 마킹한 경우 힙 워커의 헤더에 있는 Use new 및 Use old 하이퍼링크를 통해 어떤 선택 단계에서든 이전 객체와 새 객체를 선택할 수 있습니다.
가장 큰 객체 분석
메모리 누수로 인해 사용 가능한 힙이 가득 차면, 프로파일된 애플리케이션의 다른 메모리 사용량은 미미해 보일 정도가 됩니다. 이 경우 새 객체를 조사할 필요 없이 가장 큰 비중을 차지하는 객체가 무엇인지 분석하기만 하면 됩니다.
메모리 누수 속도가 매우 느려서 오랜 시간 동안 두드러지지 않을 수도 있습니다. 이러한 메모리 누수가 눈에 띌 때까지
프로파일링하는 것은 비실용적일 수 있습니다. OutOfMemoryError가 발생할 때 자동으로
HPROF 스냅샷을 저장하는 JVM 기본 기능을 사용하면
일반적인 메모리 소비보다 메모리 누수가 더 큰 비중을 차지하는 스냅샷을 확보할 수 있습니다. 실제로 다음 옵션을
-XX:+HeapDumpOnOutOfMemoryError
운영 시스템의 VM 파라미터에 항상 추가해 두는 것이 좋습니다. 이렇게 하면 개발 환경에서 재현하기 어려울 수 있는 메모리 누수를 분석할 수 있는 방법을 확보할 수 있습니다.
메모리 누수가 지배적이라면 힙 워커의 "Biggest objects" 뷰에서 상위 객체들이 실수로 유지된 메모리를 포함하고 있을 것입니다. 가장 큰 객체 자체는 정상적인 객체일 수 있지만, 이들의 도미네이터 트리를 열어보면 누수된 객체로 이어집니다. 단순한 상황에서는 단 하나의 객체가 힙의 대부분을 차지하기도 합니다. 예를 들어 맵을 사용하여 객체를 캐시하고 이 캐시가 전혀 지워지지 않는다면, 그 맵이 가장 큰 객체의 도미네이터 트리에 나타나게 됩니다.
가비지 컬렉터 루트로부터의 강한 참조 체인 찾기
객체는 강하게 참조(strongly referenced)되는 경우에만 문제가 될 수 있습니다. "강하게 참조됨"이란 가비지 컬렉터 루트에서 해당 객체로 이어지는 참조 체인이 적어도 하나 존재한다는 것을 의미합니다. "가비지 컬렉터 루트"(줄여서 GC 루트)는 가비지 컬렉터가 인식하고 있는 JVM 내의 특수한 참조입니다.
GC 루트로부터의 참조 체인을 찾으려면 "Incoming references" 뷰나 힙 워커 그래프에서 Show Paths To GC Root 액션을 사용할 수 있습니다. 이러한 참조 체인은 실제로 매우 길 수 있으므로, 일반적으로 "Incoming references" 뷰에서 더 쉽게 해석할 수 있습니다. 참조는 아래에서 최상위 레벨의 객체를 향합니다. 검색 결과에 해당하는 참조 체인만 펼쳐지며, 동일한 레벨의 다른 참조는 노드를 닫았다가 다시 열거나 컨텍스트 메뉴에서 Show All Incoming References 액션을 실행하기 전까지는 표시되지 않습니다.
GC 루트 유형 및 참조 노드에서 사용되는 기타 용어에 대한 설명을 보려면 트리 범례를 사용하세요.
트리에서 노드를 선택하면 비모달(non-modal) 트리 범례가 선택한 노드에서 사용된 모든 아이콘과 용어를 강조 표시합니다. 대화상자에서 특정 행을 클릭하면 하단에 설명이 표시됩니다.
가비지 컬렉터 루트의 중요한 유형으로는 스택에서의 참조, JNI를 통해 네이티브 코드에서 생성된 참조, 활성 스레드 및 현재 사용 중인 객체 monitor와 같은 리소스가 있습니다. 또한 JVM은 중요한 시스템을 유지하기 위해 몇 가지 "고정(sticky)" 참조를 추가합니다.
클래스와 클래스로더는 특수한 순환 참조 체계를 가지고 있습니다. 클래스는 다음과 같은 조건이 충족될 때 해당 클래스로더와 함께 가비지 컬렉션됩니다.
- 해당 클래스로더에 의해 로드된 어떤 클래스도 활성 인스턴스를 가지고 있지 않은 경우
- 클래스로더 자체가 해당 클래스들을 제외하고는 참조되지 않는 경우
-
클래스로더 컨텍스트를 제외하고는 어떤
java.lang.Class객체도 참조되지 않는 경우
대부분의 경우 클래스는 관심 있는 GC 루트로 가는 경로의 마지막 단계입니다. 클래스 자체는 GC 루트가 아닙니다. 그러나 커스텀 클래스로더가 사용되지 않는 모든 상황에서는 클래스를 GC 루트로 취급하는 것이 적절합니다. 이것이 가비지 컬렉터 루트를 검색할 때 JProfiler의 기본 모드이지만, 루트 경로 옵션 대화상자에서 이를 변경할 수 있습니다.
GC 루트로의 최단 경로를 해석하는 데 어려움이 있다면 추가 경로를 검색할 수 있습니다. 다만 GC 루트로의 모든 경로를 검색하는 것은 경로 수가 너무 많아질 수 있으므로 일반적으로 권장되지 않습니다.
라이브 메모리 뷰와 달리 힙 워커는 참조되지 않는 객체를 절대 표시하지 않습니다. 그러나 힙 워커가 오직 강하게 참조된 객체만 표시하는 것은 아닙니다. 기본적으로 힙 워커는 소프트 참조(soft reference)로만 참조되는 객체도 유지하지만, 약한 참조(weak reference), 팬텀 참조(phantom reference) 또는 파이널라이저 참조(finalizer reference)로만 참조되는 객체는 제거합니다. 소프트 참조는 힙이 고갈되지 않는 한 가비지 컬렉션되지 않으므로, 이를 포함하지 않으면 대규모 힙 사용량을 설명할 수 없기 때문에 포함됩니다. 힙 스냅샷을 생성할 때 표시되는 옵션 대화상자에서 이 동작을 조정할 수 있습니다.
디버깅 목적으로는 힙 워커에 약하게 참조된 객체를 포함하는 것이 유용할 수 있습니다. 나중에 약하게 참조된 객체를 제거하려면 "Remove objects retained by weak references" 인스펙션을 사용할 수 있습니다.
GC 루트 경로를 검색할 때 힙 워커 옵션 대화상자에서 객체를 유지하도록 선택한 참조 유형이 고려됩니다. 이렇게 하면 GC 루트 경로 검색을 통해 객체가 힙 워커에 유지된 이유를 항상 설명할 수 있습니다. GC 루트 경로 검색을 위한 옵션 대화상자에서 허용 가능한 참조 유형을 모든 약한 참조로 확장할 수 있습니다.
전체 객체 집합 제거하기
지금까지는 단일 객체만 살펴보았습니다. 실제로는 동일한 유형의 많은 객체가 메모리 누수의 일부인 경우가 많습니다. 대개의 경우 단일 객체 분석 결과는 현재 객체 집합의 다른 객체에도 유효합니다. 관심 대상 객체들이 서로 다른 방식으로 참조되는 보다 일반적인 경우에는 "Merged dominating references" 뷰를 통해 어떤 참조가 현재 객체 집합을 힙에 유지하는 역할을 하는지 확인하는 데 도움을 받을 수 있습니다.
도미네이팅 참조 트리의 각 노드는 해당 참조를 제거할 경우 현재 객체 집합에서 몇 개의 객체가 가비지 컬렉션 대상이 될 수 있는지를 보여줍니다. 여러 가비지 컬렉터 루트에 의해 참조되는 객체는 지배적인 인입 참조(dominating incoming reference)가 없을 수 있으므로, 이 뷰는 객체 중 일부에 대해서만 도움이 되거나 심지어 비어 있을 수도 있습니다. 그런 경우에는 병합된 인입 참조(merged incoming reference) 뷰를 사용하여 가비지 컬렉터 루트를 하나씩 제거해야 합니다.

























