JProfiler 도움말

메모리 프로파일링

힙에 있는 객체에 대한 정보를 얻는 방법은 두 가지가 있습니다. 한 가지는 프로파일링 에이전트가 각 객체의 할당과 가비지 컬렉션을 추적하는 것입니다. JProfiler에서는 이를 "할당 녹화"라고 부릅니다. 이 기능은 객체가 어디에서 할당되었는지 알려주며, 임시 객체에 대한 통계도 생성할 수 있습니다. 또 다른 방법은 JVM의 프로파일링 인터페이스를 통해 프로파일링 에이전트가 "힙 스냅샷"을 찍어 모든 live 객체와 그 참조를 함께 검사하는 것입니다. 이 정보는 객체가 왜 가비지 컬렉션되지 않는지 이해하는 데 필요합니다.

할당 녹화와 힙 스냅샷 모두 비용이 많이 드는 작업입니다. 할당 녹화는 런타임 특성에 큰 영향을 미치는데, java.lang.Object 생성자를 계측해야 하고, 가비지 컬렉터가 지속적으로 프로파일링 인터페이스에 보고해야 하기 때문입니다. 그래서 기본적으로 할당은 녹화되지 않으며, 녹화를 시작 및 중지해야 합니다. 힙 스냅샷은 한 번만 수행되는 작업입니다. 하지만 JVM을 몇 초 동안 멈출 수 있으며, 수집된 데이터를 분석하는 데는 힙 크기에 따라 시간이 상당히 오래 걸릴 수 있습니다.

JProfiler는 메모리 분석을 두 개의 뷰 섹션으로 나눕니다: "Live memory" 섹션에서는 주기적으로 업데이트할 수 있는 데이터를 제공하고, "Heap walker" 섹션에서는 정적인 힙 스냅샷을 보여줍니다. 할당 녹화는 "Live memory" 섹션에서 제어하지만, 녹화된 데이터는 heap walker에서도 표시됩니다.

메모리 프로파일링으로 해결할 수 있는 가장 일반적인 세 가지 문제는 다음과 같습니다: 메모리 누수 찾기, 메모리 사용량 줄이기, 그리고 임시 객체 생성 줄이기입니다. 처음 두 문제의 경우 주로 heap walker를 사용하며, JVM에서 가장 큰 객체를 누가 보유하고 있는지, 어디서 생성되었는지를 살펴보게 됩니다. 마지막 문제는 이미 가비지 컬렉션된 객체가 포함되므로, 녹화된 할당을 보여주는 live 뷰만을 참고할 수 있습니다.

인스턴스 개수 추적

힙에 어떤 객체가 있는지 개요를 얻으려면, "All objects" 뷰에서 모든 클래스와 그 인스턴스 개수의 히스토그램을 볼 수 있습니다. 이 뷰에 표시되는 데이터는 할당 녹화로 수집되는 것이 아니라, 인스턴스 개수만 계산하는 미니 힙 스냅샷을 수행하여 얻습니다. 힙이 클수록 이 작업에 시간이 더 오래 걸리므로, 이 뷰는 현재 값으로 자동 업데이트되지 않습니다.

메모리 누수를 찾을 때는 인스턴스 개수를 시간에 따라 비교하고 싶을 때가 많습니다. 모든 클래스에 대해 이를 수행하려면 뷰의 차이 비교 기능을 사용할 수 있습니다. 두 개의 all objects 덤프를 동시에 선택하면, Difference 열이 추가되고, 인스턴스 개수의 히스토그램에는 마킹 시점의 기준값이 초록색으로 표시됩니다. 새로운 all objects 덤프를 찍으면, 가장 오래된 선택된 덤프가 계속 선택된 상태로 남고, 새 덤프와의 차이가 표시됩니다.

덤프 선택기에서는 덤프가 찍힌 시점의 타임스탬프를 보여줍니다. 덤프를 더블 클릭하면 식별을 쉽게 하기 위해 라벨을 추가할 수 있습니다. all objects 덤프는 트리거 액션이나 Controller API로도 트리거할 수 있으며, 이때도 라벨을 지정할 수 있습니다.

반면 "Recorded objects" 뷰는 할당 녹화를 시작한 이후에 할당된 객체의 인스턴스 개수만 보여줍니다. 할당 녹화를 중지하면 새로운 할당은 추가되지 않지만, 가비지 컬렉션 추적은 계속됩니다. 이 방식으로 특정 use case에서 힙에 남아 있는 객체를 확인할 수 있습니다. 객체가 오랫동안 가비지 컬렉션되지 않을 수도 있으니 주의하세요. Run GC 툴바 버튼을 사용하면 이 과정을 빠르게 진행할 수 있습니다. 대부분의 동적으로 업데이트되는 뷰와 마찬가지로, Freeze 툴바 버튼을 사용해 표시되는 데이터의 업데이트를 중지할 수 있습니다.

Mark Current 툴바 버튼을 사용하면 "Recorded objects" 뷰에서도 선택한 기준값과의 차이 열을 표시할 수 있습니다. 선택한 클래스에 대해서는 컨텍스트 메뉴의 클래스 트래커에 선택 추가 액션을 통해 시간별 그래프도 볼 수 있습니다.

할당 지점

할당 녹화가 활성화되어 있으면, JProfiler는 객체가 할당될 때마다 호출 스택을 기록합니다. 정확한 호출 스택(예: stack-walking API에서 얻는 것)을 사용하지는 않는데, 이는 비용이 너무 크기 때문입니다. 대신 CPU 프로파일링에 설정된 것과 동일한 메커니즘을 사용합니다. 즉, 호출 스택은 호출 트리 필터에 따라 필터링되며, 실제 할당 지점이 호출 스택에 나타나지 않을 수도 있습니다(무시되거나 compact-filtered된 클래스의 메서드인 경우). 하지만 이러한 변화는 직관적으로 이해하기 쉽습니다: compact-filtered 메서드는 이후 compact-filtered 클래스에서 발생하는 모든 할당에 대해 책임을 집니다.

샘플링을 사용할 경우, 할당 지점은 대략적으로 표시되어 혼란스러울 수 있습니다. 시간 측정과 달리, 특정 클래스가 어디에서 할당될 수 있는지 명확히 알고 있는 경우가 많습니다. 샘플링은 정확한 값이 아닌 통계적 그림을 보여주기 때문에, java.util.HashMap.get이 자신의 클래스를 할당하는 등 불가능해 보이는 할당 지점이 나타날 수 있습니다. 정확한 숫자와 호출 스택이 중요한 분석에는 계측과 함께 할당 녹화를 사용하는 것이 좋습니다.

CPU 프로파일링과 마찬가지로, 할당 호출 스택은 호출 트리 형태로 표시되며, 호출 횟수와 시간 대신 할당 횟수와 할당된 메모리가 표시됩니다. CPU 호출 트리와 달리, 할당 호출 트리는 트리 계산 비용이 더 크기 때문에 자동으로 표시 및 업데이트되지 않습니다. JProfiler는 모든 객체뿐만 아니라 선택한 클래스나 패키지에 대해서도 할당 트리를 보여줄 수 있습니다. 기타 옵션과 함께, 현재 데이터에서 할당 트리를 계산하도록 요청하면 표시되는 옵션 대화상자에서 설정할 수 있습니다.

CPU 호출 트리의 유용한 속성 중 하나는, 각 노드가 자식 노드에서 소비된 시간을 포함하므로, 누적된 시간을 위에서 아래로 따라갈 수 있다는 점입니다. 기본적으로 할당 트리도 동일하게 동작하여, 각 노드는 자식 노드에서 발생한 할당을 포함합니다. 할당이 호출 트리의 깊은 리프 노드에서만 발생하더라도, 숫자는 위로 전파되어 최상위까지 올라갑니다. 이 방식으로 할당 호출 트리의 브랜치를 열 때마다 어떤 경로를 조사할 가치가 있는지 항상 확인할 수 있습니다. "Self-allocations"는 실제로 해당 노드에서(자식이 아닌) 발생한 할당을 의미합니다. CPU 호출 트리와 마찬가지로, 퍼센트 바는 이를 다른 색상으로 표시합니다.

할당 호출 트리에는 실제 할당이 전혀 발생하지 않는 노드가 많은 경우가 자주 있습니다. 특히 선택한 클래스에 대한 할당을 표시할 때 그렇습니다. 이러한 노드는 실제 할당이 발생한 노드까지의 호출 스택을 보여주기 위해 존재합니다. JProfiler에서는 이러한 노드를 "bridge" 노드라고 부르며, 위 스크린샷에서 볼 수 있듯이 회색 아이콘으로 표시됩니다. 경우에 따라서는 할당의 누적이 방해가 되어 실제 할당 지점만 보고 싶을 수 있습니다. 할당 트리의 뷰 설정 대화상자에서 누적되지 않은 숫자를 표시하는 옵션을 제공합니다. 이 옵션을 활성화하면 bridge 노드는 항상 0 할당을 표시하며 퍼센트 바도 없습니다.

할당 핫스팟 뷰는 할당 호출 트리와 함께 채워지며, 선택한 클래스를 생성하는 데 책임이 있는 메서드에 직접 집중할 수 있게 해줍니다. 녹화된 객체 뷰와 마찬가지로, 할당 핫스팟 뷰도 현재 상태를 마킹하고 시간에 따른 변화를 관찰할 수 있습니다. 뷰에는 핫스팟이 Mark Current Values 액션이 호출된 시점 이후 얼마나 변했는지 보여주는 차이 열이 추가됩니다. 할당 뷰는 기본적으로 주기적으로 업데이트되지 않으므로, Calculate 툴바 버튼을 클릭해 새로운 데이터 세트를 받아와 기준값과 비교해야 합니다. 옵션 대화상자에서 자동 업데이트를 활성화할 수 있지만, 힙 크기가 큰 경우에는 권장하지 않습니다.

할당 녹화 비율

모든 할당을 녹화하면 상당한 오버헤드가 발생합니다. 많은 경우, 전체 할당 숫자가 중요하지 않고 상대적인 숫자만으로도 문제를 해결할 수 있습니다. 그래서 JProfiler는 기본적으로 10번째 할당마다 한 번씩만 녹화합니다. 이렇게 하면 모든 할당을 녹화할 때와 비교해 오버헤드가 약 1/10로 줄어듭니다. 모든 할당을 녹화하고 싶거나, 더 적은 할당만으로도 충분하다면, 녹화된 객체 뷰나 할당 호출 트리 및 핫스팟 뷰의 파라미터 대화상자에서 녹화 비율을 변경할 수 있습니다.

이 설정은 세션 설정 대화상자의 "고급 설정->메모리 프로파일링" 단계에서도 찾을 수 있으며, 오프라인 프로파일링 세션에 맞게 조정할 수 있습니다.

할당 녹화 비율은 "Recorded objects"와 "Recorded throughput"에 대한 VM 텔레메트리 값에도 영향을 주며, 설정된 비율로 측정됩니다. 스냅샷을 비교할 때, 첫 번째 스냅샷의 할당 비율이 보고되고, 필요하다면 다른 스냅샷은 이에 맞게 스케일됩니다.

할당된 클래스 분석

할당 트리와 할당 핫스팟 뷰를 계산할 때, 보고 싶은 클래스나 패키지를 미리 지정해야 합니다. 이미 특정 클래스에 집중하고 있다면 이 방식이 잘 동작하지만, 사전 지식 없이 할당 핫스팟을 찾으려면 불편할 수 있습니다. 한 가지 방법은 "Recorded objects" 뷰에서 시작해, 컨텍스트 메뉴의 액션을 사용해 선택한 클래스나 패키지에 대한 할당 트리 또는 할당 핫스팟 뷰로 전환하는 것입니다.

또 다른 방법은 모든 클래스에 대한 할당 트리 또는 할당 핫스팟에서 시작해, Show classes 액션을 사용해 선택한 할당 지점 또는 할당 핫스팟에 대한 클래스를 표시하는 것입니다.

할당된 클래스의 히스토그램은 호출 트리 분석으로 표시됩니다. 이 액션은 다른 호출 트리 분석에서도 동작합니다.

클래스 분석 뷰는 정적이며, 할당 트리와 핫스팟 뷰가 다시 계산되어도 업데이트되지 않습니다. Reload Analysis 액션을 사용하면 먼저 할당 트리를 업데이트한 후, 새로운 데이터로 현재 분석 뷰를 다시 계산합니다.

가비지 컬렉션된 객체 분석

할당 녹화는 live 객체뿐만 아니라, 가비지 컬렉션된 객체에 대한 정보도 유지할 수 있습니다. 이는 임시 할당을 조사할 때 유용합니다. 많은 임시 객체를 할당하면 상당한 오버헤드가 발생할 수 있으므로, 할당 비율을 줄이면 성능이 크게 향상될 수 있습니다.

녹화된 객체 뷰에서 가비지 컬렉션된 객체를 표시하려면, 활성도 선택기를 Garbage collected objects 또는 Live and garbage collected objects로 변경하세요. 할당 호출 트리와 할당 핫스팟 뷰의 옵션 대화상자에도 동일한 드롭다운이 있습니다.

하지만 JProfiler는 기본적으로 가비지 컬렉션된 객체에 대한 할당 트리 정보를 수집하지 않습니다. live 객체만의 데이터는 훨씬 적은 오버헤드로 유지할 수 있기 때문입니다. "Allocation Call Tree" 또는 "Allocation Hotspots" 뷰에서 활성도 선택기를 가비지 컬렉션된 객체가 포함된 모드로 전환하면, JProfiler는 녹화 타입 변경을 제안합니다. 이는 프로파일링 설정의 변경이므로, 즉시 변경을 적용하면 이전에 녹화된 데이터는 모두 삭제됩니다. 미리 이 설정을 변경하고 싶다면, 세션 설정 대화상자의 "고급 설정" -> "메모리 프로파일링"에서 할 수 있습니다.

다음 단계: heap walker

더 고급 질문은 객체 간의 참조를 포함하게 됩니다. 예를 들어, 녹화된 객체, 할당 트리, 할당 핫스팟 뷰에 표시되는 크기는 shallow size입니다. 이는 클래스의 메모리 레이아웃만 포함하며, 참조된 클래스는 포함하지 않습니다. 클래스의 객체가 실제로 얼마나 무거운지 확인하려면, retained size—즉, 해당 객체가 힙에서 제거될 경우 해제되는 메모리 양—를 알아야 할 때가 많습니다.

이러한 정보는 live memory 뷰에서는 제공되지 않으며, 모든 객체를 열거하고 비용이 많이 드는 계산이 필요하기 때문입니다. 이 작업은 heap walker가 담당합니다. live memory 뷰에서 관심 지점에서 heap walker로 이동하려면 Show in Heap Walker 툴바 버튼을 사용할 수 있습니다. 이 버튼을 누르면 heap walker의 해당 뷰로 이동합니다.

힙 스냅샷이 없다면 새로 생성되고, 이미 존재한다면 JProfiler가 기존 힙 스냅샷을 사용할지 물어봅니다.

어떤 경우든, live memory 뷰와 heap walker의 숫자가 크게 다를 수 있다는 점을 이해하는 것이 중요합니다. heap walker는 live memory 뷰와는 다른 시점의 스냅샷을 보여줄 뿐만 아니라, 참조되지 않은 모든 객체를 제거합니다. 가비지 컬렉터의 상태에 따라 참조되지 않은 객체가 힙의 상당 부분을 차지할 수 있습니다.