한 줄 정의
핵심 메시지
프로파일링은 “어떤 코드를 더 들여다봐야 하는가”를 식별하는 작업입니다. 자바에서는 크게 실행 프로파일링(어디서 CPU 시간을 쓰는가)과 할당 프로파일링(어디서 메모리를 쓰는가) 두 갈래로 나뉩니다.
중요한 사실은 프로파일러는 항상 숫자를 보여주지만 그 숫자가 진실인지는 별개라는 점입니다. 전통적인 샘플링 프로파일러는 세이프포인팅 편향(safepointing bias) 때문에 핫 메서드를 잘못 짚는 경우가 흔합니다. 그래서 perf, 비동기 프로파일러(async-profiler)처럼 핫스팟 내부의
AsyncGetCallTrace나 하드웨어 성능 카운터를 활용해 편향을 우회하는 도구들이 등장했습니다.
쉽게 말하면
성능 튜닝의 첫 단추는 “어디가 느린가”를 정확히 찾는 일입니다. 그런데 측정 도구가 편향된 결과를 내면 엉뚱한 곳을 최적화하게 됩니다. 프로파일러가 “이 메서드가 80%를 차지합니다”라고 알려줘도, 그 80%가 단지 “여기에 세이프포인트가 자주 있어서 샘플이 거기 몰린 것”일 수 있습니다.
이 장은 두 가지를 함께 다룹니다. 첫째, 어떤 도구가 있고 어떻게 쓰는지(VisualVM, JMC, perf, 비동기 프로파일러). 둘째, 그 도구들이 왜 거짓말을 하는지, 거짓말을 줄이는 도구들은 어떻게 동작하는지.
왜 중요한가?
측정이 곧 진실은 아니다
벤치마크와 프로파일링 도구는 항상 숫자를 보여줍니다. 다만 그 숫자가 무엇을 측정한 것인지는 명확하지 않을 때가 많습니다. 인지적 편향까지 더해지면 “이게 문제일 거야”라는 가설을 무리해서 끼워 맞추기 쉽습니다. 신중한 성능 엔지니어는 다양한 도구로 교차 검증하면서 실제 상황을 파악하려 노력합니다.
프로파일링 전에 이미 후보가 있어야 한다
프로파일링은 이미 의심되는 성능 이슈가 있을 때 그 원인을 좁히기 위한 도구입니다. 무작정 프로파일러를 켜고 답을 찾는 게 아니라, 다음 같은 경로로 먼저 문제를 인지해야 합니다.
- 개발 단계나 CI 파이프라인의 성능 저하 테스트
- 사용자 수용 테스트 또는 별도의 성능 테스트 환경
- 프로덕션의 변화(카나리아 테스트로 관찰된 동작)
- 이전에는 문제가 없었지만 데이터 증가·용량 부족으로 새로 드러난 문제(인덱싱 부족 등)
실행 프로파일링 전에 GC부터 확인하라
애플리케이션이 사용자 모드에서 CPU를 거의 100% 쓰고 있다면 실행 프로파일링으로 해결할 성능 문제일 가능성이 높습니다. 하지만 프로파일링을 시작하기 전에 반드시 가비지 컬렉션을 먼저 확인해야 합니다. GC가 문제라면 실행 프로파일링이 아니라 GC 튜닝이 필요합니다. GC 로그와 애플리케이션 로그를 살펴보는 것이 우선입니다.
프로덕션에서만 보이는 문제
일부 문제는 프로덕션의 데이터 양에서만 드러납니다. 스테이징에서는 데이터가 적어 누락된 인덱스나 비싼 SELECT가 문제를 일으키지 않다가, 프로덕션 부하에서 비로소 N+1·풀스캔이 모습을 드러냅니다. 따라서 프로덕션 환경에서도 동작 가능한 프로파일링 도구 선택이 중요합니다.
핵심 내용
12.1 프로파일링 소개
자바 가상 머신의 프로파일링·모니터링 도구는 보통 저수준 계측을 활용해 데이터를 처리합니다. 이 데이터는 GUI 콘솔이나 SaaS 같은 외부 도구로 실시간 전송되거나, 나중에 분석하기 위해 로그로 저장됩니다. 계측은 애플리케이션 시작 시 로드되는 에이전트 형태이거나, 실행 중인 자바 가상 머신에 동적으로 연결되는 방식으로 작동합니다.
세 가지 도구 부류
| 도구 부류 | 역할 |
|---|---|
| 모니터링 도구 (monitoring tool) | 시스템과 현재 상태를 광범위하게 관찰 |
| 경고 시스템 (alerting system) | 비정상적이거나 이상 동작을 감지하고 알림 |
| 프로파일러 (profiler) | 실행 중인 애플리케이션을 심층 분석 |
세 부류는 목적이 다르지만 서로 연관이 있으며, 성공적인 프로덕션 환경에서는 모두 함께 쓰는 경우가 많습니다.
잘못된 진단의 비용
“성능 문제를 해결하려면 가장 먼저 문제를 일으키는 원인을 정확히 찾아야 한다”는 원칙이 7.5절 ‘간단한 시스템 모델’에서도 강조되었습니다. 잘못된 진단은 큰 비용을 초래합니다. 도구는 숫자를 제공하지만, 그 숫자가 실제로 문제 해결에 도움이 되는지는 알 수 없습니다.
브라이언 괴츠
벤치마크는 항상 숫자를 보여줍니다. 설령 그 숫자가 의미가 없더라도요. 우리는 항상 뭔가를 측정하지만, 그 측정이 무엇인지 명확하지 않을 때가 많습니다.
도널드 커누스의 격언
도널드 커누스
뛰어난 프로그래머는 중요한 코드가 어떤 것인지 신중히 판단한 후, 그 코드에 대해 철저히 검토합니다. 단, 그 코드는 반드시 명확히 식별된 다음이어야 합니다.
이 문구의 무게중심은 “철저히 검토”가 아니라 **“명확히 식별된 다음”**입니다. 프로파일링은 이 식별 단계를 도와주는 도구이지, 식별을 대체하지 않습니다.
잠금 경합·SLA·DB I/O
CPU가 가득 차지 않은 상황에서도 실행 프로파일링은 유용할 수 있습니다.
- 애플리케이션이 지연 SLA를 충족하지 못할 때 원인 분석
- 잠금 경합(lock contention)이 높아 여러 코어를 효과적으로 못 쓰는 상황 식별
- 특정 코드가 비용 큰 SELECT 쿼리를 삽입해 DB I/O 병목을 만든 경우(N+1 포함)
12.2 GUI 기반 프로파일링 도구
12.2.1 비주얼 가상 머신 (VisualVM)
비주얼 가상 머신은 실행 프로파일러와 메모리 프로파일러를 모두 포함하는 간단한 무료 도구입니다. 프로덕션 프로파일러로 쓰기엔 기능이 제한적이지만, 개발·QA 환경에서 애플리케이션 동작을 분석하려는 성능 엔지니어에게는 충분히 유용합니다.
Monitor 탭이 시작점
Monitor 탭은 CPU·힙·클래스·스레드의 기본 텔레메트리를 한눈에 보여주며, 프로파일링 작업의 출발점으로 활용됩니다. Profiler 탭으로 가면 실행 중인 메서드와 각 메서드의 CPU 사용량 비율을 볼 수 있습니다.
한계: UI의 분석 깊이가 얕다
비주얼 가상 머신의 UI는 심층적인 분석 기능이 제한적이어서, 대부분의 성능 엔지니어는 곧 더 강력한 도구로 전환하게 됩니다. “처음 시작” 도구이지 “최종 도구”는 아닙니다.
12.2.2 JDK 미션 제어 (JMC + JFR)
JFR과 JDK 미션 제어는 오라클이 BEA 시스템즈를 인수하면서 도입한 프로파일링·모니터링 기술입니다. 별도로 동작하지만 서로 밀접한 관계가 있습니다.
| 구성 요소 | 역할 |
|---|---|
| JFR (Java Flight Recorder) | OS·JVM·JDK 라이브러리 관련 이벤트를 수집하는 저수준 이벤트 기반 성능 데이터 수집 도구. 핫스팟 JVM에 내장 |
| JMC (JDK Mission Control) | 그래픽 구성 요소. 초기 설치 시 JMX 콘솔과 JFR 데이터 처리를 포함하며, 미션 제어 내에서 쉽게 플러그인을 설치할 수 있음 |
역사: JRockit에서 핫스팟·OpenJDK로
원래 BEA의 JRockit 자바 가상 머신을 위한 도구였으며, JRockit 단종 과정에서 오라클 JDK 상업 버전으로 이전되었습니다. 자바 11에서 JFR이 OpenJDK에 기부되었고, JDK 미션 제어는 독립 프로젝트로 전환되었습니다. 이후 JFR은 OpenJDK 8u272에서 역이식되어, 최근 OpenJDK 8 릴리스에서도 사용할 수 있습니다. 팀은 애플리케이션에 추가할 맞춤형 JFR 이벤트를 구축할 수도 있습니다.
JFR 활성화 방법
프로파일링을 위해 대상 애플리케이션에서 JFR이 활성화되어야 합니다.
- 애플리케이션을 실행할 때 JFR 플래그를 활성화한 상태로 시작
- 애플리케이션이 실행 중인 상태에서 동적으로 연결
- 애플리케이션을 실행한 뒤,
jcmd명령어를 사용해 로컬 JVM에서 JFR 녹화 시작
프로덕션에서는 JFR 덤프가 더 쉽다
JMX를 통해 라이브 프로덕션 환경에 직접 연결하는 것은 어려울 수 있으므로, JFR 덤프 파일을 추출하는 방식이 더 쉬운 접근일 때가 많습니다.
자동 분석과 메서드 프로파일링
녹화가 완료되면 메인 창에 자동 분석 결과가 표시됩니다. 왼쪽 탐색 메뉴의 Method Profiling 항목을 선택하면 핫 메서드(hot method) — 즉 애플리케이션 실행 시간의 대부분을 차지하는 메서드 — 를 분석할 수 있습니다. 환경이 지원하면 플레임 그래프(flame graph) 같은 시각화 기법도 활용 가능합니다.
JMC의 한계
JDK 미션 제어는 전반적으로 훌륭한 GUI 기반 프로파일링 도구지만, 이런 유형 도구에는 한계가 있습니다. 특히 데스크톱 기반 프로파일링을 10·11장에서 다룬 관측성 기법에 적용하는 것은 쉽지 않습니다. 즉 GUI 프로파일러는 개발·디버깅 무대에는 좋지만, 분산 시스템의 상시 관측에는 부적합합니다.
12.3 샘플링과 세이프포인트 편향
샘플링이라는 절충
전통적인 실행 프로파일러는 스택 추적을 주기적으로 샘플링해 각 스레드에서 실행 중인 코드를 확인합니다. 모든 메서드의 진입·종료를 추적하는 데이터 수집 비용이 과도하기 때문에, 일정 간격으로 샘플링된 스냅샷을 사용합니다.
예를 들어 뉴 렐릭 자바 에이전트의 스레드 프로파일러는 100 ms마다 샘플링을 수행합니다. 이는 과도한 오버헤드 없이 수행할 수 있는 최대 빈도에 대한 경험적 규칙(rule of thumb)입니다.
빈도 선택의 트레이드오프
| 빈도 | 결과 |
|---|---|
| 너무 높음 | 오버헤드가 커져 성능 민감 애플리케이션에 부적합 |
| 너무 낮음 | 중요한 동작을 놓치거나 실제 성능을 반영하지 못함 |
커크 페퍼다인 (Kirk Pepperdine)
프로파일러를 사용할 때는 세부 정보를 확인하기 위해 사용하는 도구여야 합니다. 프로파일러가 예상치 못한 결과를 보여준다면 문제가 있습니다.
세이프포인팅 편향
샘플링은 데이터 속에서 문제를 숨길 가능성을 제공할 뿐 아니라, 많은 프로파일러가 자바 가상 머신 세이프포인트에서만 샘플링합니다. 이를 세이프포인팅 편향(safepointing bias)이라 부르며, 두 가지 결과를 초래합니다.
- 모든 스레드가 세이프포인트에 도달해야 샘플을 수집 가능
- 샘플은 세이프포인트 상태에 있는 애플리케이션 상태만 수집 가능
첫 번째는 실행 중인 프로세스에서 프로파일링 샘플을 생성하는 데 추가적인 오버헤드를 발생시킵니다. 두 번째는 샘플이 세이프포인트 상태에서만 수집되므로 샘플 포인트 분포를 제대로 반영하지 못합니다.
샘플링은 실행 중인 코드만 보여준다
프로파일이 샘플링 방식이라는 사실은 매우 중요합니다. 차단(blocking)이나 대기도 비효율적인 알고리즘만큼 성능 문제를 유발할 수 있는데, 샘플링 프로파일러는 이를 놓치기 쉽습니다.
GetCallTrace()와 오버헤드
샘플링 실행 프로파일러는 핫스팟 C++ API의 GetCallTrace() 함수로 각 스레드의 스택 샘플을 수집합니다. 일반적으로 에이전트 내에서 수집되며, 이후 로그로 기록되거나 후속 처리됩니다.
초기 구현에서 GetCallTrace()는 심각한 오버헤드를 가지고 있었습니다. 활성 애플리케이션 스레드가 N개 있는 경우, 스택 샘플을 수집하려면 자바 가상 머신이 N번의 세이프포인트를 발생시켜야 했습니다. 이 오버헤드는 자바 8 이전에서 샘플 수집 빈도 상한선의 주요 원인 중 하나였습니다.
JEP 312: 개별 스레드 정지 기술
이 문제는 JEP 312 — 개별 스레드 정지 기술(thread-local handshakes)을 통해 어느 정도 해결되었습니다. 이 기술은 셰넌도어나 ZGC 컬렉션 구현의 필수 기반이기도 합니다. 자바 11 이후에서는 스레드 프로파일링 오버헤드가 크게 줄었습니다.
세이프포인트 시간 측정
성능 엔지니어는 애플리케이션에서 얼마나 많은 세이프포인트 시간이 소모되는지 반드시 확인해야 합니다. 다음 플래그가 매우 유용합니다.
-XX:+PrintGCApplicationStoppedTime
이 플래그는 GC 로그에 세이프포인트 시간에 대한 추가 정보를 기록합니다. 일부 도구는 이 데이터를 자동 분석해 세이프포인트와 OS 커널에 의해 발생하는 정지 시간을 구분합니다.
카운트 루프: 세이프포인팅 편향이 만든 함정
for (int i = 0; i < LIMIT; i += 1) {
// 루프 본문에는 '간단한' 연산만 포함됩니다
}여기서 ‘간단한’의 의미는 의도적으로 정의되지 않습니다. JIT 컴파일러가 수행할 최적화 종류에 따라 동작이 달라지기 때문입니다(메서드가 실제로 루프 본문 내에 존재하지 않는 경우를 의미합니다).
LIMIT가 클 때: 역분기에 세이프포인트 삽입
LIMIT 값이 크면 JIT 컴파일러는 이 코드를 루프 상단으로 돌아가는 역분기(back branch)로 포함합니다. JIT 컴파일러는 루프백 엣지(loop-back edge)에 세이프포인트 검사를 삽입합니다. 큰 루프의 경우 각 반복마다 세이프포인트 기회가 생긴다는 의미입니다.
LIMIT가 작을 때: 루프 펼치기로 세이프포인트가 사라짐
LIMIT 값이 작으면 JIT 컴파일러는 이 루프를 펼치기(unroll) 처리합니다. 작은 루프를 실행하는 스레드가 루프가 완료될 때까지 세이프포인트를 발생시키지 않는다는 뜻입니다.
flowchart LR L[루프 크기 LIMIT] --> Big{LIMIT 크기} Big -->|크다| Back[역분기 + 세이프포인트 삽입] Big -->|작다| Unroll[루프 펼치기 → 세이프포인트 없음] Back --> Sample1[샘플 수집 가능] Unroll --> Sample2[루프 완료까지 샘플 수집 불가]
결과: 편향된 그림
세이프포인트에서만 샘플링하면 루프 크기와 작업 특성에 민감하게 의존하는 편향이 직접적으로 발생합니다. 루프 펼치기로 만들어진 긴 코드 블록은 샘플이 전혀 수집되지 않을 수 있고, 정확하고 신뢰할 수 있는 성능 분석이 불가능해집니다. 이는 단순한 이론적 우려가 아니라 실제로 일어나는 일입니다.
다행히 샘플링 프로파일러를 대신할 대안이 있습니다.
12.4 최신 프로파일러
전통 샘플링 프로파일러보다 더 나은 통찰력과 정확성을 제공하는 세 가지 오픈 소스 도구가 있습니다.
- perf
- 비동기 프로파일러 (async profiler)
- 어니스트 프로파일러 (honest profiler)
12.4.1 perf
perf는 리눅스에서 실행되는 애플리케이션을 위한 경량 프로파일링 도구로, 자바/자바 가상 머신 애플리케이션에만 한정되지 않고 하드웨어 성능 카운터를 읽어 동작합니다. 리눅스 커널의 tools/perf 디렉터리에 포함되어 있습니다.
하드웨어 성능 카운터
성능 카운터는 성능 분석가들이 관심을 갖는 하드웨어 이벤트를 계산하는 물리적 레지스터입니다.
- 실행된 명령어 수
- 캐시 누락
- 분기 예측 실패
이것이 애플리케이션 프로파일링의 기초를 형성합니다.
JVM 동적 부분 매핑: perf-map-agent
자바는 자바 런타임 환경의 동적인 특성 때문에 perf에 추가적인 과제를 제공합니다. JIT 컴파일된 메서드는 perf 입장에서는 “알 수 없는 메모리 영역”이므로, JVM 실행의 동적 부분을 매핑할 수 있는 브릿지가 필요합니다.
이 브릿지가 perf-map-agent입니다. 알 수 없는 메모리 영역(JIT 컴파일된 메서드)에서 perf용 동적 심볼을 생성하는 에이전트입니다. 핫스팟의 동적으로 생성된 인터프리터와 가상 디스패치를 위한 점프 테이블도 엔트리를 생성해야 합니다. perf-map-agent는 C로 작성된 에이전트와, 필요한 경우 실행 중인 자바 프로세스에 에이전트를 연결하는 작은 자바 부트스트랩으로 구성됩니다.
-XX:+PreserveFramePointer 플래그
자바 8u60에서는 perf와의 상호작용을 개선하기 위한 새로운 플래그가 추가되었습니다.
-XX:+PreserveFramePointer
기본값이 false로 설정되어 있어, 자바 애플리케이션을 perf로 프로파일링할 때는 반드시 명시적으로 활성화하는 것이 권장됩니다.
JIT 최적화 일부 비활성화
이 플래그를 활성화하면 JIT 컴파일러 최적화 일부가 비활성화되므로 성능이 약간 저하될 수 있습니다.
플레임 그래프 읽는 법
perf가 생성하는 숫자 데이터의 가장 인상적인 시각화는 플레임 그래프입니다. 실행 시간이 정확히 어디에서 소비되고 있는지 매우 세부적으로 보여줍니다.
| 축/요소 | 의미 |
|---|---|
| x축 | 스택 프로파일의 분포 (시간 흐름이 아님). 알파벳 순서로 정렬 |
| y축 | 스택 깊이를 아래로부터 위로 세워서 표시 |
| 각 사각형 | 스택 프레임. 폭이 넓을수록 해당 스택에 더 자주 나타남 |
| 맨 위 사각형 | 특정 CPU에서 실행 중인 내용. 그 아래는 상위 계층 |
| 색상 | 원래는 인접 프레임을 시각적으로 구분하기 위한 무작위 색상 |
x축이 시간이 아니라는 것의 의미
가장 중요한 사실은 x축이 시간을 나타내지 않는다는 점입니다. 플레임 그래프는 샘플을 알파벳 순으로 정렬하고, 가능한 한 프레임을 병합합니다. 이렇게 하면 프로파일 전체의 큰 그림을 더 잘 파악할 수 있습니다.
플레임 그래프 vs 플레임 차트
플레임 그래프의 그레그(Gregg)의 원래 개념에는 몇 가지 개선과 변형이 포함됩니다. 일부 구현은 일정한 색상 체계로 의미를 부여합니다(예: 자바 프레임인지 네이티브 프레임인지 표시).
중요한 변형 중 하나는 플레임 차트(flame chart)입니다. 처음 구글 크롬의 웹킷 개발자 도구를 위해 개발되었습니다.
| 구분 | x축 | 적합한 곳 |
|---|---|---|
| 플레임 그래프 | 알파벳 순 | 다중 스레드 자바 애플리케이션 |
| 플레임 차트 | 시간 순 | 단일 스레드의 시간 기반 패턴 (자바스크립트) |
플레임 차트는 시간 기반 패턴을 시각화할 수 있는 장점이 있지만, 단일 스레드의 패턴만 제대로 표시할 수 있습니다. 자바 애플리케이션에는 덜 유용한 이유입니다.
컨테이너 환경 주의점
perf는 하드웨어 성능 카운터에 기반하므로, CPU 프로파일링에 의존하는 이벤트가 컨테이너나 seccomp 환경에서 사용 불가능할 수 있습니다. 컨테이너 위에서 perf를 돌릴 계획이라면 사전에 검증해야 합니다.
12.4.2 비동기 프로파일러 (async-profiler)
비동기 프로파일러의 주요 목표는 다음과 같습니다.
- 대부분의 다른 프로파일러에서 발생하는 세이프포인트 편향을 제거
- 기존 프로파일러보다 훨씬 낮은 오버헤드
AsyncGetCallTrace 활용
이를 달성하기 위해 비동기 프로파일러는 핫스팟 내부의 비공개 API 호출인 AsyncGetCallTrace(AGCT)를 사용합니다. 따라서 비동기 프로파일러는 OpenJDK가 아닌 자바 가상 머신에서는 동작하지 않습니다. 다만 OpenJDK 기반 JVM(어댑티움·아마존·마이크로소프트·오라클·레드햇·애저 줄루 빌드)과 핫스팟 JVM에서는 잘 작동합니다.
무화면(headless) 모드 운영
비동기 프로파일러 같은 도구는 데이터 수집 도구로서 무화면 모드(headless mode)에서 실행되는 경우가 많습니다. 시각화는 별도 도구나 사용자 정의 스크립트가 담당합니다.
동작 방식
비동기 프로파일러는 perf에 의존하므로 perf가 동작하는 OS(주요 리눅스)에서만 작동합니다. 구현은 실행 중인 스레드를 인터럽트하기 위해 유닉스 신호 SIGPROF를 사용합니다. 그 다음 비공개 메서드인 AsyncGetCallTrace()로 호출 스택을 수집합니다.
개별 스레드만 인터럽트 → 전역 동기화 없음
개별 스레드만 인터럽트하기 때문에 전역 동기화 이벤트가 절대 발생하지 않습니다. 전통적인 샘플링 프로파일러에서 흔히 나타나는 리소스 경쟁과 오버헤드를 방지할 수 있습니다.
비동기 콜백 내에서 호출 추적은 비잠금 링 버퍼에 기록되고, 전용 독립 스레드가 애플리케이션을 멈추지 않고 데이터를 로그로 기록합니다.
스키드(skid) 문제
비샘플링 프로파일러의 작동 방식에 대해 알아야 할 추가 세부 사항이 있습니다.
CPU가 ‘외부에서 트리거’된 인터럽트(캐시 누락 등)를 받을 때, 명령어 스트림에서 CPU는 어떤 지점에서 인터럽트되는가?
이 질문은 현대의 명령어 재정렬(out-of-order, OOO)을 지원하는 CPU에 중요하며, 대부분의 현대적인 서버 애플리케이션에서 사용됩니다.
낮은 수준의 프로파일러(perf 등)의 출력을 보면, L1 캐시 누락 이벤트와 같은 이벤트로 태그된 명령어가 실제로 해당 이벤트를 유발하지 않았을 수 있습니다. 이는 스키드(skid)로 알려져 있으며, 이벤트를 실제로 유발한 명령어와 이벤트가 태그된 명령어 사이의 거리로 정의됩니다.
세이프포인트 편향의 또 다른 형태
자바 가상 머신에 특화된 문제는 세이프포인트 편향의 숨겨진 형태가 있다는 것입니다. AGCT 또는 유사한 기술을 사용하는 비샘플링 프로파일러는 호출 스택을 수집할 때 세이프포인트 편향이 없지만, 마지막 프레임의 해석은 기록된 디버그 정보에 치우치는 편향이 있으며, 기본적으로 이 정보는 세이프포인트에 있습니다.
해결책은 다음 플래그입니다.
-XX:+DebugNonSafepoints
이를 가능한 한 빨리 활성화하면 더 정확한 해석을 제공합니다.
timer_create 기반 새 샘플링 엔진
비동기 프로파일러는 컨테이너에 영향을 미치는 perf_events 문제를 해결하려고 시도하며, timer_create를 기반으로 하는 새로운 CPU 샘플링 엔진을 추가했습니다. 이 엔진은 기존 샘플링 엔진의 장점을 결합하지만, 커널 스택을 수집할 수 없다는 작은 단점이 있습니다.
이는 최신 비동기 프로파일러가 기본적으로 컨테이너에서 작동하며, 타이밍 편향을 감소하고 파일 디스크립터를 소모하지 않는다는 것을 의미합니다.
깃허브 문서 참조
비동기 프로파일러를 독립적으로 사용하려면 애플리케이션에 맞는 설정과 스크립트를 작성해야 하는 번거로움이 있습니다. 이러한 복잡한 과정에 대해서는 비동기 프로파일러의 깃허브 페이지 같은 외부 리소스를 참고하는 것이 좋습니다.
어니스트 프로파일러 (honest profiler)
마지막으로, 몇 년 전까지만 해도 인기 있었던 비동기 프로파일러의 대안으로 어니스트 프로파일러가 있습니다.
- 비동기 프로파일러와 동일한 내부 API를 사용
- 핫스팟 자바 가상 머신에서만 실행되는 오픈 소스 도구
- 현재는 활발히 유지 보수되지 않는 것으로 보임
새 프로젝트에서는 사용 비권장
새로운 프로젝트에서는 어니스트 프로파일러를 사용하지 않는 것이 좋습니다. 기존에 설치된 경우에는 다른 도구로 전환하는 것을 권장합니다.
12.5 JDK 플라이트 레코더(JFR)
JFR은 12.2.2에서 잠깐 다뤘던 것처럼, OpenJDK의 일부로 제공되는 낮은 수준의 오버헤드 도구입니다. 핫스팟 자바 가상 머신에서 실행 중인 애플리케이션을 프로덕션 환경에서도 사용할 수 있도록 설계되었습니다.
프로덕션 프로파일러의 요건
프로덕션 환경에서 사용하는 프로파일러는 일반 사용이나 높은 부하 시나리오에서도 허용 가능한 정도로 오버헤드가 작아야 합니다. JFR은 이를 이벤트 기반 도구라는 구조로 해결합니다. 어떤 프로파일을 선택하느냐에 따라 수집하는 데이터가 달라집니다.
두 가지 기본 프로파일
JFR은 기본적으로 두 가지 프로파일을 제공합니다. JDK 설치 디렉터리에 XML 구성 파일로 저장되어 있습니다.
| 프로파일 | 파일명 | 특성 |
|---|---|---|
| 지속 (Default·Continuous) | default.jfc | 항상 활성화되는 프로파일링용. 메모리 할당 프로파일링에 필요한 세부 정보는 부족할 수 있음 |
| 프로파일링 (Profiling) | profile.jfc | 훨씬 더 자세한 데이터를 제공하지만 실행 시 오버헤드가 더 높음 |
맞춤형 프로파일
JFR의 고급 사용자는 애플리케이션을 관리하는 그룹의 성능 관심사에 더 적합한 맞춤형 프로파일(custom profile)을 생성할 수 있습니다. 이 프로파일에는 다른 이벤트 집합을 포함할 수 있습니다.
실측 오버헤드
오라클의 발표와 데모에 따르면, JFR 지속 프로파일을 사용하는 애플리케이션은 성능에 약 ~1% 정도의 영향을 미친다고 합니다. 메모리 할당 프로파일링이 포함된 프로파일에서는 약 ~3% 정도의 영향이 관찰되었습니다.
논문 “Don’t Trust Your Profiler: An Empirical Study on the Precision and Accuracy of Java Profilers”(Humphrey Burchell 외, MPLR 2023)에 따르면 JFR은 항상 활성화된(always-on) 프로파일링을 수행할 수 있다는 결론입니다. 다만 일부 애플리케이션은 리소스 요구 사항으로 이러한 오버헤드도 감당하지 못할 수 있습니다.
다른 관측성 도구의 기반
이러한 이유로, JFR은 많은 다른 관측성·모니터링 도구의 기반으로 사용되었습니다. 데이터독과 뉴 렐릭은 JFR 데이터를 입력 데이터로 사용하는 실행 프로파일러를 제공합니다.
JFR 이벤트의 구조
JFR로 가능한 작업을 이해하는 핵심은 이벤트와 해당 이벤트에 포함된 데이터입니다. JFR 이벤트는 타입이 있는 데이터이며, 각 이벤트 유형은 이름과 구조를 가지고 있습니다.
jdk.CPULoad {
startTime = 11:51:57.745
jvmUser = 8.75%
jvmSystem = 0.57%
machineTotal = 13.50%
}
jdk.JavaMonitorEnter {
startTime = 11:51:58.065
duration = 12.1 ms
monitorClass = jdk.jfr.internal.PlatformRecorder (classLoader = bootstrap)
previousOwner = "RMI TCP Connection(idle)" (javaThreadId = 32)
address = 0x12CE66508
eventThread = "JFR Periodic Tasks" (javaThreadId = 26)
}
jdk.CPULoad는 시계열을 나타내며 jvmUser, jvmSystem, machineTotal 같은 여러 필드를 포함합니다. 다른 이벤트는 다른 구조를 가지며, 예를 들어 가비지 컬렉션 이벤트는 세부적으로 나뉘어 수집 사이클의 단일 단계에만 해당할 수 있습니다.
임계값 기반 이벤트
jdk.JavaMonitorEnter 같은 잠금 이벤트는 임계값을 가지며, JFR이 특정 시간이 초과된 경우에만(예: 10ms 이상) 해당 이벤트를 기록하도록 설정할 수 있습니다. 모든 잠금 진입을 기록하면 데이터가 폭발하므로, 느린 잠금만 골라 기록하는 식의 필터링이 가능합니다.
jcmd로 JFR 제어
JDK에는 JFR 덤프 파일을 분석할 수 있는 간단한 명령어 도구가 포함되어 있습니다. 이 덤프 파일은 JDK 미션 제어 GUI를 사용하거나, 명령줄에서 jcmd로 실행 중인 자바 프로세스를 제어하거나, 자바를 시작할 때 적절한 명령줄 스위치를 추가하는 방법으로 사용할 수 있습니다.
jcmd를 사용하는 경우, 파일을 생성하려면 기록 시작 → 덤프 생성 → 기록 중지 순으로 실행해야 합니다.
jcmd <pid> JFR.start name=MyRecording settings=default
jcmd <pid> JFR.dump filename=my-recording.jfr
jcmd <pid> JFR.stop시작 시 활성화
자바 애플리케이션이 시작될 때 JFR 기록을 시작하려면, 다음과 같은 명령어 스위치를 추가하고 적절한 옵션을 제공해야 합니다.
-XX:StartFlightRecording:<options>
JFR 기록은 고정된 기간 동안 진행될 수도 있고, 링 버퍼 모드(ring-buffer mode)를 사용할 수도 있습니다. 덤프 파일을 생성한 후에는 JDK에서 제공하는 jfr 도구를 사용하여 그 안에 포함된 이벤트를 확인할 수 있습니다.
jfr 도구 하위 명령어
jfr도구에는 많은 하위 명령어가 있습니다. 모든 명령어를 보려면jfr --help를 실행하세요.
jfr print --events CPULoad,JavaMonitorEnter recording.jfr위 명령어로 jdk.CPULoad와 jdk.JavaMonitorEnter 이벤트만 골라 볼 수 있습니다. jfr 도구는 출력 결과를 XML 또는 JSON 형식으로도 생성할 수 있습니다.
SAP JFR 이벤트 브라우저
SAP 팀은 JFR 이벤트를 위한 이벤트 브라우저를 유지 관리하고 있으며, OpenJDK의 LTS 또는 최신 기능 릴리스를 지원합니다.
코드로 JFR 덤프 처리
JFR에서 생성된 덤프 파일을 코드로 처리하는 것도 어렵지 않습니다. RecordingFile 객체를 사용하며, 이 객체를 반복적으로 처리하면 필요한 데이터를 쉽게 추출할 수 있습니다.
String fileName = // ... 일부 JFR 파일
var recording = new RecordingFile(Paths.get(fileName));
while (recording.hasMoreEvents()) {
var event = recording.readEvent();
if (event != null) {
var details = decodeEvent(event);
if (details == null) {
System.err.println("Failed to recognize details");
} else {
// 세부 정보를 처리할 예정이지만, 지금은 로그만 남김
System.out.println(details);
}
}
}Predicate 기반 디코딩 패턴
JFR 이벤트를 분석하려면 decodeEvent() 메서드에서 데이터를 디코딩해야 합니다. 한 가지 방법은 정적 컬렉션에 mapper를 정의하고, 이를 다음과 같이 적용하는 것입니다.
public Map<String, String> decodeEvent(final RecordedEvent e) {
for (var ent : mappers.entrySet()) {
if (ent.getKey().test(e)) {
return ent.getValue().apply(e);
}
}
return null;
}
private static Predicate<RecordedEvent> makePredicate(String s) {
return e -> e.getEventType().getName().startsWith(s);
}
private static final Map<Predicate<RecordedEvent>,
Function<RecordedEvent, Map<String, String>>> mappers =
Map.of(makePredicate("jdk.CPULoad"),
ev -> Map.of("timestamp", ""+ ev.getStartTime(),
"user", ""+ ev.getDouble("jvmUser"),
"system", ""+ ev.getDouble("jvmSystem"),
"total", ""+ ev.getDouble("machineTotal")
));makePredicate()는 특정 JFR 이벤트가 분석 대상인지 판단하는 Predicate 객체를 생성합니다. 이후 이벤트 데이터를 문자열 맵으로 변환하여 출력합니다.
SQL 같은 인터페이스: JFR 분석
JFR 데이터를 다루는 오픈 소스 도구 중 군나르 몰링(Gunnar Morling)이 개발한 JFR 분석은 주목할 만합니다. JFR 기록 파일을 다루기 위해 SQL과 비슷한 방식으로 데이터를 쿼리할 수 있는 인터페이스를 제공합니다. 표준 JDBC 코드를 사용하여 쉽게 통합할 수 있습니다.
12.6 프로파일링의 운영적 측면
프로파일러는 애플리케이션의 저수준 실행 동작을 이해하거나 문제를 진단하기 위해 사용되는 개발자 도구입니다. 반면, 도구 스펙트럼의 다른 끝에는 관측성 도구 또는 운영 모니터링 도구가 있습니다. 후자는 시스템의 현재 상태를 시각화하고, 시스템이 정상적으로 작동하는지 또는 이상이 있는지를 판단할 수 있도록 팀을 지원합니다.
두 도구가 다루는 영역이 매우 넓기 때문에 한 권의 책으로 모든 도구를 다루는 것은 불가능합니다. 이 분야에서는 단순하거나 빠른 해결책이 없습니다. 특정 도메인에 적합한 방법과 기술을 직접 찾아야 합니다.
12.6.1 운영 도구로서의 JFR 활용
JFR은 오랜 기간 동안 사용되어 왔으며, 이는 장점이자 단점이 될 수 있습니다.
| 측면 | 내용 |
|---|---|
| 장점 | 프로덕션 환경에서 철저히 검증된 도구이며, 핫스팟 자바 가상 머신과 같이 통합되어 있어 뛰어난 성능 |
| 단점 | 개발 초기에는 프로파일링 방식이 제한적이었고, 덤프 파일을 생성한 뒤 개발자 컴퓨터로 옮겨 오프라인 분석을 수행하는 방식이 주로 사용됨 |
클라우드 네이티브에서의 제약
오늘날 클라우드 네이티브 애플리케이션 환경에서는 오프라인 분석 방식이 실현하기 어렵거나 불편합니다. JFR이 클라우드 네이티브 환경에서도 유용한 도구가 되려면 새로운 패턴과 새로운 방법론이 필요합니다.
링 버퍼 구성
JFR을 사용하는 일반적인 운영 패턴 중 하나는 링 버퍼 구성에서 시작하는 것입니다. 보통 애플리케이션을 시작할 때 미리 구성된 JFR 기록 옵션을 함께 전달하여 -XX:StartFlightRecording 스위치를 사용하는 방식으로 수행됩니다.
-XX:StartFlightRecording:disk=true,filename=/sandbox/service.jfr,maxage=4h,settings=profile
이 설정은 JFR에게 ‘프로파일링’ 프로파일에 해당하는 이벤트를 최대 4시간 동안 메모리에 저장하도록 지시합니다. 이후 새로운 이벤트가 들어오면 이전 이벤트가 삭제되며, 기록을 요청하면 현재 버퍼 상태를 /sandbox/service.jfr 파일로 저장합니다.
Go back in time 분석
이 방식은 필요 시 파일을 덤프하여, 사건이 발생한 이후에도 ‘과거 특정 시점’(go back in time) 분석이 가능하게 합니다. 단, 이 기능은 링 버퍼가 충분히 큰 경우에만 가능합니다. 이러한 기술은 장애 복구(outage recovery) 과정에서 매우 유용합니다.
메모리·디스크·I/O 함정
| 자원 | 위험 |
|---|---|
| 컨테이너 메모리 | 이벤트를 버퍼링하려면 추가 메모리가 필요. 컨테이너 메모리 한계에 가까운 경우, 예상치 못한 활동 증가가 JFR 이벤트 버퍼의 크기를 초과하게 만들어 OOM-killer가 애플리케이션을 강제 종료할 수 있음 |
| maxage 한계 | JFR이 이벤트 기반이라 생성 데이터 크기는 JVM 활동 수준에 따라 변동. 일정하지 않음 |
| maxsize 사용 시 | JFR이 컨테이너를 종료시키지 않도록 보장할 수 있지만, JFR 링 버퍼가 제공하는 되돌아보기 시간 창(look-back time window)이 정확히 얼마나 되는지 100% 확신할 수 없음 |
| 디스크 I/O | JFR은 파일 시스템에 데이터를 스풀하므로 충분한 디스크 공간이 필요. 무상태로 설계된 컨테이너에서 과도한 I/O가 도커·쿠버네티스에 의해 제거되지 않도록 주의 |
JFR 이벤트 스트리밍
자바 17과 21을 포함한 최신 자바는 JFR 이벤트 스트리밍 기능도 제공합니다. 이 기능은 프로그램이 JFR 이벤트에 대해 콜백을 받을 수 있는 API를 제공합니다. 핵심 클래스는 RecordingStream으로, 개발자가 특정 이벤트 유형에 관심을 등록하고 이를 처리할 콜백 객체를 지정할 수 있습니다.
자바 17+ 시장 점유율 한계
JFR 이벤트 스트리밍은 관측성 도구를 구축하기 위한 기반으로 훨씬 적합하지만, 2024년 4월 기준으로 자바 17 이상의 시장 점유율이 약 ~35%에 불과합니다. 이 때문에 다른 도구 기능이 개발되어 왔습니다.
12.6.2 레드햇 크라이오스탯 (Cryostat)
크라이오스탯(Cryostat)은 컨테이너화된 자바/JVM 애플리케이션을 위한 JFR 도구입니다. 원래 레드햇에서 개발되었으며, 오픈시프트 하이브리드 클라우드 플랫폼에서 기본 지원됩니다. 하지만 모든 쿠버네티스 배포판에서 작동하는 상위 오픈 소스 프로젝트로도 제공됩니다.
핵심 가치
크라이오스탯은 쿠버네티스 클러스터에서 JFR 기록 파일을 다루는 복잡한 작업을 단순화하도록 설계되었습니다. 사용자가 원격으로 JFR 데이터를 시작·중지하고, 데이터를 가져오거나 분석할 수 있습니다.
배포 요구사항
크라이오스탯을 성공적으로 배포하려면 다음이 필요합니다.
- 인증서 관리자(cert-manager)
- 연산자 수명 주기 관리자(operator lifecycle manager, OLM)
- 연산자 허브(OperatorHub)
제공 기능
크라이오스탯은 다음과 같은 기능을 제공합니다.
- 애플리케이션 토폴로지 뷰
- 그라파나 뷰
- 자동화된 규칙
- 알림 기능
- 스마트 트리거
관측 가능한 서비스 자체도 관측 가능해야 한다
크라이오스탯은 샘플 애플리케이션과 크라이오스탯 자체의 Pod를 포함하여 여러 Pod로 구성된 쿠버네티스 배포를 보여줍니다. 이 사례는 관측 가능한 서비스 자체도 관측 가능해야 한다는 원칙을 잘 보여줍니다.
독점 도구 비교
독점 도구 영역에서는 데이터독과 뉴 렐릭도 JFR을 기반으로 한 실행 프로파일러를 제공합니다.
12.6.3 JFR과 오픈텔레메트리 프로파일링
10.2절에서 관측성의 세 개의 핵심 요소(지표·추적·로그)를 다루었으며, 여기에 프로파일링을 네 번째 요소로 추가할 가능성을 논의했습니다. 이 개념은 현재 오픈텔레메트리 작업 그룹에서 활발히 연구되고 있으며, 자바를 포함한 여러 언어에서 다양한 프로파일러가 오픈텔레메트리의 새로운 프로파일링 신호 소스로 활용될 가능성이 있습니다.
제약 사항
JFR은 오픈텔레메트리 프로파일링 신호를 제공하는 중요한 데이터 소스가 될 수 있지만, 몇 가지 제약이 있습니다.
- 오픈텔레메트리의 타임 윈도우 제한 때문에 실질적인 오픈텔레메트리 프로파일링 구현은 JFR 이벤트 스트리밍을 기반으로 해야 함 → 자바 17 이상에서만 작동
- 2024년 8월 현재, JFR 프로파일링 샘플과 추적 ID를 연결하는 간단한 방법이 없음 → 자바뿐 아니라 오픈텔레메트리의 적용 범위 내에 있는 다른 언어에서도 진행 중
opentelemetry-java-instrumentation
opentelemetry-java-instrumentation 프로젝트는 JFR을 기반으로 한 오픈텔레메트리 자바 가상 머신 지표 구현을 제공합니다. 이 프로젝트는 표준 자바 가상 머신 지표 세트를 정의합니다.
종합적으로 JFR은 오픈텔레메트리 생태계의 중요한 데이터 소스이자 기여 요소로 간주되어야 하며, 오픈 계측(open instrumentation)으로의 전환에서 중요한 역할을 합니다. 하지만 JFR은 단독으로 완전한 실행 프로파일링 솔루션이 아닙니다.
12.6.4 프로파일러 선택하기
프로파일러를 선택할 때 고려해야 할 측면입니다.
- GUI에서 대화형으로 작업할 도구가 필요한가?
- 이 도구가 프로덕션 프로파일링에 적합한가, 아니면 개발/CI에 더 적합한가?
- 프로파일러는 설정에 얼마나 많은 시간과 노력을 투자할 수 있나?
- 프로파일링 솔루션의 오버헤드에 대해 허용 가능한 한계가 있나?
오픈 소스 프로파일러 요약
| 도구 | 특징 |
|---|---|
| 비주얼 가상 머신 | GUI 배포 쉬움. 직접적인 자바 관리 확장 프로그램 연결과 스냅샷 필요 |
| JDK 미션 제어 | 더 정교한 GUI. JFR 사용 가능하지만 덤프 파일 기반으로만 작동하며 스트리밍 이벤트 미지원 |
| JFR | JVM에 내장된 저 오버헤드 헤드리스 프로파일링 엔진. 다양한 도구와 통합 가능 |
| perf | 리눅스 하드웨어 이벤트 분석. 자바에 특화되어 있지 않으며 추가 브릿지 도구(perf-map-agent) 필요 |
| 비동기 프로파일러 | perf_events 기반 저 오버헤드. 세이프포인트 편향 없음. JFR보다 통합 가능한 도구가 적을 수 있음 |
상용 도구
JProfiler와 YourKit 같은 상용 프로파일러가 있습니다. 한때 무료 도구보다 훨씬 우수했지만, 최근 몇 년 동안 그 격차는 줄어들었습니다. 그럼에도 일부 팀은 여전히 이 도구에서 가치를 찾고 있습니다.
12.7 메모리 프로파일링
실행 프로파일링은 매우 중요하지만, 그것만으로는 충분하지 않을 때가 많습니다. 많은 애플리케이션은 어느 정도의 메모리 분석도 필요로 합니다. 여기서는 할당 프로파일링과 힙 덤프 분석 두 가지 주요 유형을 다룹니다.
12.7.1 할당 프로파일링
4장에서 다룬 것처럼, 애플리케이션의 할당 동작을 이해하는 것은 성능 분석의 핵심 요소 중 하나입니다. 이를 통해 메모리 사용량을 깊이 분석하는 할당 프로파일링(allocation profiling)이 필요하며, 이와 관련된 다양한 접근 방식이 존재합니다.
jmap과 방문자 패턴
jmap 같은 도구는 방문자 패턴(visitor pattern)을 사용합니다. 비주얼 가상 머신의 메모리 프로파일링 화면은 각 객체 유형별 메모리 사용량을 히스토그램으로 시각화합니다.
방문자 패턴
방문자 패턴은 고전적인 디자인 패턴으로 특정 연산(이 경우, 메모리 분석)을 별도의 클래스로 분리하여, 복합 개체의 하위 구성 요소를 탐색할 수 있도록 합니다.
두 가지 스냅샷 옵션
| 옵션 | 명령어 | 특성 |
|---|---|---|
| 빠른 스냅샷 | jmap -histo | 활성 객체와 가비지를 모두 포함 |
| 정확한 스냅샷 | jmap -histo:live | 더 정확하지만, 생성하기 전에 STW 가비지 컬렉션이 필요 |
히스토그램이 보여주는 것
단순한 히스토그램이라도 메모리 사용량에 대한 여러 정보를 제공할 수 있습니다.
- 대부분의 애플리케이션에서 문자열이 가장 일반적인 데이터 유형. 자바 문자열은 내부적으로
byte[](자바 8에서는char[])에 대한 참조가 포함되어 있으며, 이 구현 방식은 JEP 254에 따라 변경. 문자열 개수 또는 그 이상으로byte[]객체가 존재할 것으로 예상 HashMap항목이나Object[]같은 다른 일반 객체들이 자주 등장- 비즈니스 애플리케이션은 도메인 객체가 가장 일반적인 객체 목록에 나타나는 경우가 많음. “관찰된 도메인 객체의 양이 내 애플리케이션에 적합한지”를 간단히 점검 가능
JDK 미션 제어의 가비지 컬렉션 프로파일링
힙 사용률에서 가비지 컬렉션 프로파일링으로 약간 이동해보면, JDK 미션 제어 도구를 활용할 수 있습니다. 기존 JDK 미션 제어 서비스 지원 에이전트가 제공하지 않는 몇 가지 값들을 수집할 수 있습니다(다만, 대부분의 카운터는 서비스 지원 에이전트 카운터와 중복됩니다).
JDK 미션 제어의 큰 장점은 JFR을 통해 필요한 데이터를 수집하는 데 드는 비용이 서비스 지원 에이전트보다 훨씬 낮다는 것입니다. 또한 JDK 미션 제어의 인터페이스는 성능 분석 엔지니어가 데이터를 더 직관적이고 유연하게 볼 수 있도록 해 줍니다.
스레드-로컬 할당 이벤트
객체 할당을 분석하는 또 다른 방법은 스레드-로컬 할당 기능을 살펴보는 것입니다. JFR은 객체가 할당될 때 다음과 같은 두 가지 경우를 이벤트로 기록합니다.
- 스레드-로컬 할당 기능 내부 →
jdk.ObjectAllocationInNewTLAB - 스레드-로컬 할당 기능 외부(‘느린 경로’, slow path) →
jdk.ObjectAllocationOutsideTLAB
이를 통해 JFR은 메모리가 얼마나 빠르게 할당되는지를 계산할 수 있습니다. JDK 미션 제어/JFR의 할당 뷰는 스레드-로컬 할당 기능 할당 뷰를 표시할 수 있습니다.
메모리 누수 프로파일러: jdk.OldObjectSample
JFR은 메모리 누수 프로파일러(memory leak profiler)로 작동할 수 있는 이벤트도 제공합니다. JFR은 jdk.OldObjectSample 이벤트를 사용하여 객체 할당을 샘플링하고, 객체의 수명 주기를 추적합니다. 이를 통해 시간이 지나면서 메모리 누수 가능성이 있는 지점을 조사하기 위한 시작점을 제안할 수 있습니다.
12.7.2 힙 덤프
또 다른 메모리 프로파일링 기법은 힙 덤프 분석(heap dump analysis)입니다. 할당 프로파일링과 달리, 이것은 오프라인 프로세스로, 전체 힙의 스냅샷을 생성하여 파일에 저장하는 방식입니다.
힙 덤프 생성
jmap 명령으로 수행할 수 있습니다.
jmap -dump:live,format=b,file=heap.bin <pid>이 덤프 파일은 별도의 도구로 검사 또는 분석되어 활성 객체 집합이나 객체의 수와 유형, 객체 그래프의 형태와 구조 같은 중요한 정보를 확인할 수 있습니다. 이러한 도구는 배치 모드로 작동하거나 대화형으로 사용할 수 있습니다.
대화형 도구의 강점
힙 덤프를 대화형 도구로 로드하면, 성능 엔지니어는 힙 스냅샷을 탐색하며 분석할 수 있습니다. 이를 통해 활성 객체뿐만 아니라 아직 수집되지 않은 객체도 확인할 수 있습니다.
힙 덤프의 단점
| 단점 | 내용 |
|---|---|
| 파일 크기 | 일반적으로 덤프된 메모리 크기의 3~4배에 달할 수 있음. 멀티 기가바이트 힙이라면 상당한 크기 |
| 네트워크 전송 | 프로덕션 환경에서는 네트워크를 통해 파일을 가져와야 하는데, 컨테이너화된 애플리케이션에서는 쉽지 않음 |
| 워크스테이션 리소스 | 덤프를 처리할 충분한 리소스(특히 메모리)를 갖춘 워크스테이션에 로드해야 함. 전체 로드가 안 되면 디스크에서 페이징하며 작업해야 함 |
| STW 정지 | 힙 덤프를 생성하는 과정에서도 활성 객체와 가비지가 함께 나타나거나, STW 방식으로 힙을 탐색하고 덤프하는 동안 프로세스를 중단해야 함 |
클라우드 환경의 위험
클라우드 기반 시스템에서는 이러한 STW 정지 때문에 자바 가상 머신 프로세스가 다운된 것처럼 보일 수 있으며, 이는 Pod가 종료되는 결과를 초래할 수 있습니다.
이런 어려움에도, 특정 상황(분리하기 어려운 메모리 누수)이 있을 때는 힙 덤프가 유용할 수 있습니다. 다만 힙 덤프는 제한 사항을 인지하고, 프로덕션 Pod가 의도치 않게 성능 저하되거나 종료되지 않도록 주의해야 합니다.
이클립스 메모리 분석(MAT)
비주얼 가상 머신은 힙 덤프 파일을 탐색할 수 있지만 실무에서는 프로덕션 힙 덤프가 다소 다루기 어렵습니다. 대안으로 이클립스 메모리 분석(Eclipse memory analyzer, MAT)을 사용할 수 있습니다. MAT는 힙 덤프를 분석하는 독립 실행형 도구입니다.
MAT의 강점
이클립스 메모리 분석의 강점은 객체 그래프를 탐색하고, 힙 구조를 기반으로 보고서를 생성하는 기능에서 나옵니다.
- 잠재적인 메모리 누수 검색
- 메모리를 가장 많이 소비하거나 지배하는 객체 분석
- 기타 메모리 관련 작업 수행
기본적으로 제공되는 보고서 기능도 매우 유용하지만, 이클립스 메모리 분석을 올바르게 활용하려면 시간을 들여 고급 기능과 사용법을 익히는 것이 중요합니다.
hprof 도구 체인의 마이그레이션
이전 자료에서는 hprof 힙 프로파일링 네이티브 에이전트가 사용되었으며, 이는 자바 가상 머신 도구 인터페이스 기술의 참조 구현으로 설계되었으나, 프로덕션 환경에서 사용하는 도구가 아님을 문서에서도 명확히 했습니다.
그럼에도 불구하고 많은 개발자들이 hprof를 실제 성능 분석 도구로 사용하기 시작했습니다. 이러한 상황을 반영하여, hprof는 자바 9에서 공식적으로 제거되었으나, jmap 같은 도구에서는 여전히 hprof 형식으로 힙 덤프를 생성할 수 있는 기능이 유지되었습니다. 현재 사용 중인 도구 체인이 hprof를 사용하는 경우, 이클립스 메모리 분석 같은 도구로 마이그레이션하는 것이 좋습니다.
12.8 요약
프로파일링에 대한 주제는 많은 개발자들이 제대로 이해하기 힘든 주제입니다. 실행 프로파일링과 메모리 프로파일링은 모두 중요한 기법이지만, 성능 엔지니어는 자신이 무엇을 하고 왜 하는지 명확히 알아야 합니다. 도구를 아무 생각 없이 사용하면 엉뚱한 결과를 얻거나 분석 시간을 낭비하게 될 수 있습니다.
현대 애플리케이션의 프로파일링에는 도구가 필요하며, 상용이나 오픈 소스 옵션을 포함한 다양한 선택지가 있습니다. 다음 장에서는 프로파일링을 넘어 동시성과 이를 자바 애플리케이션에서 효율적으로 사용하는 방법에 대해 논의할 것입니다.
비교 / 트레이드오프
샘플링 프로파일러 vs AGCT 기반 vs JFR
| 항목 | 샘플링 (VisualVM·JMC GUI) | AGCT (async-profiler·perf) | JFR |
|---|---|---|---|
| 세이프포인트 편향 | 강함 (루프 펼치기에서 0 샘플) | 호출 스택은 없음, 마지막 프레임은 있음 | 이벤트 기반이라 무관 |
| 오버헤드 | 보통 (자바 8 이전 특히 큼) | 매우 낮음 | 지속 ~1% / 프로파일링 ~3% |
| 컨테이너 적합도 | 떨어짐 | timer_create 엔진으로 가능 | 기본 적합 (디스크 I/O 주의) |
| 프로덕션 상시 운영 | 비권장 | 가능 | always-on 설계 |
JFR 지속 프로파일 vs 프로파일링 프로파일
| 항목 | 지속 (default.jfc) | 프로파일링 (profile.jfc) |
|---|---|---|
| 오버헤드 | ~1% | ~3% |
| 메모리 할당 세부 정보 | 부족 | 포함 |
| 사용 시점 | 항상 켜두는 베이스라인 | 누수·할당 조사 기간 한정 |
| 커스터마이징 | XML 구성 파일 / 맞춤 프로파일 | 〃 |
JFR 링 버퍼: maxage vs maxsize
| 항목 | maxage | maxsize |
|---|---|---|
| 보장 대상 | 되돌아보기 시간 창 | 메모리 상한 |
| 위험 | JVM 활동 폭증 시 메모리 초과 → OOM-killer | 시간 창이 얼마인지 사전에 확정 불가 |
| 적합한 경우 | 메모리 여유 있는 베어메탈 | 컨테이너 메모리 한계 직전 |
할당 프로파일링 vs 힙 덤프
| 항목 | 할당 프로파일링 | 힙 덤프 |
|---|---|---|
| 동작 방식 | 온라인 (이벤트·샘플) | 오프라인 (전체 스냅샷) |
| JFR 이벤트 | jdk.ObjectAllocationInNew/OutsideTLAB, jdk.OldObjectSample | 해당 없음 |
| 파일 크기 | 작음 | 덤프 메모리의 3~4배 |
| STW 정지 | 없음~짧음 | 길어질 수 있음 → Pod 종료 위험 |
| 보는 것 | 할당 속도·메모리 누수 시작점 | 객체 그래프·도미네이터·누수 의심자 |
플레임 그래프 vs 플레임 차트
| 항목 | 플레임 그래프 | 플레임 차트 |
|---|---|---|
| x축 | 알파벳 순 (분포) | 시간 순 |
| 적합한 환경 | 다중 스레드 (자바 기본) | 단일 스레드 (자바스크립트 등) |
| 답하는 질문 | ”전체 시간이 어디 쌓이는가" | "이 시점에 무엇이 실행 중이었나” |
내 생각
프로파일러는 거짓말하니까 교차 검증이 디폴트
같은 코드를 GUI 샘플링 프로파일러로 보면 한 메서드가 80%인데 async-profiler로 보면 보이지도 않는 경우가 실제로 일어납니다. 카운트 루프 펼치기 사례가 그 증거입니다. 한 도구의 숫자를 진실로 받아들이는 순간 엉뚱한 곳을 최적화하게 됩니다.
CPU 100%는 GC 100%일 수 있다
실행 프로파일러를 켜기 전 GC 로그부터 확인하라는 규칙은 단순한 절차가 아닙니다. GC가 CPU를 다 먹고 있으면 실행 프로파일러는 영원히 본질을 못 찾습니다. -XX:+PrintGCApplicationStoppedTime은 프로파일링 옵션이 아니라 진단의 1단계입니다.
JFR의 진짜 가치는 “끄지 않아도 된다”는 점
1% 오버헤드로 항상 켜둘 수 있다는 사실은 운영에서 게임 체인저입니다. 장애가 터진 뒤 “그때 프로파일러 켰어야 했는데”가 아니라, 링 버퍼에서 사후 덤프를 추출하는 ‘go back in time’ 분석이 가능합니다. 단, maxage 대신 maxsize를 써야 컨테이너가 OOM으로 죽지 않습니다.
개발용 도구와 운영용 도구를 섞지 말 것
JMC는 데스크톱 분석 무대에서 강력하지만 클라우드 네이티브에 박는 도구가 아닙니다. 운영은 JFR 헤드리스 엔진 + Cryostat/스트리밍 API + 덤프 추출 파이프라인으로 갈라야 합니다. 둘을 섞으면 보안·성능 양쪽에서 문제를 만납니다.
힙 덤프는 최후의 수단
힙 덤프는 STW가 길어 클라우드에서 Pod를 죽일 수 있고, 멀티 기가바이트 파일을 네트워크로 빼내는 것 자체가 인시던트입니다. 분리하기 어려운 누수일 때만 꺼내고, 평소에는 jdk.OldObjectSample 같은 JFR 이벤트로 시작점을 좁히는 편이 안전합니다.
상용 vs 오픈소스의 격차는 사라졌다
한때 JProfiler·YourKit이 오픈소스를 압도하던 시기는 끝났습니다. async-profiler + JFR + MAT 조합이면 대부분의 분석은 끝납니다. 상용 도구는 GUI 편의성·통합 보고서가 강점이지 분석 능력에서 결정적 우위는 더 이상 없습니다.
더 알아볼 것
-
-XX:StartFlightRecording:disk=true,maxage=4h,settings=profile로 링 버퍼 띄우고jcmd JFR.dump로 사후 분석 실습 -
jfr print --json출력 + Gunnar Morling JFR-Analytics로 SQL 쿼리 시도 -
RecordingFileAPI + Predicate 매퍼로 커스텀 JFR 분석기 작성 - 자바 17+
RecordingStream으로 실시간 이벤트 콜백 실험 -
jdk.ObjectAllocationInNewTLABvsOutsideTLAB비율로 TLAB 튜닝 효과 측정 -
jdk.OldObjectSample로 메모리 누수 의심 객체 추적 - 이클립스 메모리 분석(MAT) Leak Suspects · Dominator Tree 보고서 해석법
- Cryostat 쿠버네티스 배포 (cert-manager + OLM + OperatorHub)
- OpenTelemetry 프로파일링 시그널 작업 그룹 진행 상황 추적
- async-profiler
timer_create엔진과perf_events엔진 비교 측정 - JEP 312 — Thread-Local Handshakes 원문 읽기
- 컨테이너에서
perf_events차단(seccomp 프로파일) 검증
관련 개념
- Ch04 가비지 컬렉션 이해하기 — 실행 프로파일링 전에 GC부터 의심
- Ch05 고급 가비지 컬렉션 — ZGC·셰넌도어와 thread-local handshakes
- Ch06 자바 가상 머신에서 코드 실행 — JIT·루프 펼치기·세이프포인트 (편향의 원천)
- Ch07 하드웨어와 운영 시스템 — 명령어 재정렬과 스키드
- Ch08 클라우드 스택의 구성 요소 — 컨테이너에서 perf·디스크 I/O 제약
- Ch10 관측성 소개 — 프로파일링이 네 번째 신호로 합류하는 흐름
- Ch11 자바에서 관측성 구현 — JFR이 OpenTelemetry 데이터 소스가 되는 지점
출처
- 벤 에번스 외, 『자바 최적화』 2판, 12장 프로파일링