async-profiler — 낮은 오버헤드의 프로파일링

async-profiler는 perf_events와 AsyncGetCallTrace를 이용한 샘플링 프로파일러로, 세이프포인트 편향 없이 1~2%대의 낮은 오버헤드로 CPU·할당·락을 분석합니다. 인스트루먼테이션과의 차이, 플레임그래프 읽는 법, 기본 사용법을 정리합니다.

· 7 min read · PALDYN Team

지난 글까지 JIT·GC·기동 최적화를 두루 살펴봤습니다. 그런데 이 모든 튜닝의 대전제는 “어디가 느린지 정확히 안다” 입니다. 추측으로 최적화하는 것은 시간 낭비이자 위험이죠 — 전체 시간의 2%를 쓰는 코드를 아무리 빠르게 만들어도 체감 차이는 없습니다. 실제로 CPU 시간이 어디서 소모되는지 들여다보는 도구가 프로파일러이고, 자바 진영에서 사실상 표준이 된 것이 async-profiler 입니다.

두 가지 프로파일링 방식

프로파일러는 크게 두 방식으로 나뉩니다. 이 차이를 이해하는 것이 중요합니다.

인스트루먼테이션 vs 샘플링

인스트루먼테이션(instrumentation) 은 메서드 진입·종료에 측정 코드를 삽입합니다. 호출 횟수를 정확히 세지만, 오버헤드가 크고 무엇보다 JIT의 인라이닝을 방해합니다. 측정 행위가 측정 대상의 동작을 바꿔 버리는 관찰자 효과가 생기는 것이죠. 핫한 작은 메서드일수록 왜곡이 심합니다.

샘플링(sampling) 은 주기적으로(예: 10ms마다) 스레드들의 콜 스택을 스냅샷으로 찍습니다. 어떤 스택이 자주 보이면 거기서 시간을 많이 쓴다고 추론합니다. async-profiler가 이 방식인데, 단순한 샘플링을 넘어 두 가지 영리한 기술을 씁니다.

세이프포인트 편향을 피하는 법

기존 자바 샘플링 프로파일러(jstack 기반 등)는 스택을 안전하게 읽기 위해 모든 스레드를 세이프포인트(safepoint) 에 멈춰 세웠습니다. 문제는 스레드가 아무 곳에서나 세이프포인트에 가지 않고 특정 지점(루프 백엣지, 메서드 종료 등)에서만 멈춘다는 것입니다. 그래서 표본이 세이프포인트 근처로 쏠리는 세이프포인트 편향(safepoint bias) 이 생겨, 정작 핫한 코드가 안 보일 수 있었습니다.

async-profiler는 OS의 perf_events로 신호를 받아, HotSpot의 내부 API인 AsyncGetCallTrace세이프포인트 없이 현재 스택을 읽습니다. 덕분에 편향이 없고, 자바 프레임뿐 아니라 JNI·네이티브·커널 프레임까지 한 화면에 잡아냅니다. 오버헤드는 보통 1~2% 수준입니다.

사용법 — 붙이고 떼기

가장 흔한 사용법은 실행 중인 프로세스에 잠깐 붙여 CPU 프로파일을 뜨는 것입니다. PID만 알면 됩니다.

# 30초간 CPU 프로파일을 떠서 플레임그래프 HTML로 저장
./asprof -d 30 -f profile.html <pid>

# 무엇을 측정할지 이벤트로 지정
./asprof -e alloc -d 30 -f alloc.html <pid>   # 할당 핫스팟
./asprof -e lock  -d 30 -f lock.html  <pid>   # 락 경합

-e 옵션으로 측정 대상을 바꿉니다. cpu(기본)는 CPU 시간, alloc은 어디서 객체를 많이 할당하는지, lock은 어디서 락 경합이 일어나는지를 보여 줍니다. CPU만 보면 GC가 바쁜 이유를 놓치기 쉬우므로, alloc 프로파일도 함께 보는 습관이 좋습니다.

JDK에 내장된 JFR(Java Flight Recorder)과도 연동되며, 결과를 플레임그래프로 시각화하는 것이 핵심입니다.

플레임그래프 읽기

async-profiler의 결과는 보통 플레임그래프(flame graph) 로 나옵니다. 처음 보면 막막하지만 규칙은 두 가지뿐입니다.

플레임그래프 읽는 법

  • y축은 콜 스택의 깊이입니다. 아래가 호출자, 위가 피호출자입니다. 맨 아래는 보통 main이나 스레드 진입점입니다.
  • x축은 표본의 비율, 즉 그 프레임이 차지한 시간 점유율입니다. 호출 순서(시간순)가 아닙니다 — 이 점을 자주 오해합니다. 너비가 넓을수록 그 함수가 (자신과 하위 호출 포함) CPU를 많이 썼다는 뜻입니다.

따라서 우리가 찾는 것은 넓고 평평한 꼭대기 프레임입니다. 위로 더 올라가지 않으면서(즉 다른 함수를 호출하지 않고 자기 자신이 CPU를 쓰는) 너비가 넓은 프레임이 진짜 핫스팟이고, 최적화 1순위입니다. 반대로 폭이 좁은 프레임은 아무리 깊어도 전체 성능에 영향이 적습니다.

정리

async-profiler는 perf_eventsAsyncGetCallTrace를 이용해 세이프포인트 편향 없이 낮은 오버헤드로 CPU·할당·락을 분석하는 샘플링 프로파일러입니다. 플레임그래프에서 넓고 평평한 꼭대기를 찾아 핫스팟을 짚고, 그곳에 최적화를 집중하면 됩니다. 이번 글로 JVM 성능 — JIT부터 GC, 기동, 프로파일링까지 — 의 핵심 흐름을 한 바퀴 돌았습니다. 측정으로 시작해 측정으로 검증하는 태도가 모든 튜닝의 처음과 끝입니다.


지난 글: 클래스 데이터 공유(CDS) — JVM 기동을 빠르게

다음 글: 자바 기동 시간 최적화 — 빠르게 시작하는 JVM


읽어주셔서 감사합니다. 😊