JFR 수집과 검증
JFR 기록·덤프·사용자 정의 이벤트를 실행하고 RecordingFile로 게시판 동작을 분석합니다.
Java Flight Recorder는 JVM과 애플리케이션 이벤트를 적은 부하로 기록하는 JDK 도구입니다.
CPU 표본, 할당, GC, 락, 스레드, 소켓, 예외를 같은 시간선에서 비교할 수 있습니다.
기록을 켰다는 사실보다 어떤 설정·기간·작업 부하에서 수집했고 어떤 가설을 확인할지 정하는 것이 중요합니다.
Recording의 상태 전이
기록을 직접 시작하고 중단·닫는 정상 경로는 NEW, RUNNING, STOPPED, CLOSED 상태를 따릅니다.
닫힌 기록에 start()를 호출하면 API 계약상 IllegalStateException을 던집니다.
import jdk.jfr.Recording;
public final class ClosedJfrRecordingFailure {
public static void main(String[] args) {
Recording recording = new Recording();
recording.close();
recording.start();
}
}원칙은 기록 소유자가 start·stop·덤프·close 순서를 책임지고 try-with-resources로 close를 보장하는 것입니다.
지속 관찰은 JFR 구성이나 외부 관리 명령으로 운영하며 요청마다 Recording을 만들지 않습니다.
명령줄 JFR 기록
애플리케이션 시작 시점부터 작업 부하를 수집하려면 JVM 옵션을 사용합니다.
java -XX:StartFlightRecording=filename=board.jfr,settings=profile,duration=30s,dumponexit=true Application
jfr summary board.jfr
jfr print --events jdk.CPULoad,jdk.GarbageCollection,jdk.JavaMonitorEnter board.jfrjfr summary는 이벤트 타입별 개수와 크기를 요약하고, jfr print는 선택한 이벤트를 출력하는 명령입니다. 스레드·스택 등 제공되는 정보는 이벤트 유형과 기록 설정에 따라 다릅니다. 위 명령으로 실제 애플리케이션 기록을 수집한 결과를 제시하는 것은 아닙니다.
기록 파일에는 환경·경로·인수 같은 민감 정보가 들어갈 수 있으므로 접근 권한과 보존 기간을 제한합니다.
프로그래밍 방식 JFR 기록
API 기록은 통합 확인 프로그램에서 특정 연산 전후를 감쌀 때 유용합니다.
enable()로 이벤트 임계값·스택 추적을 조정하고 stop() 뒤 파일에 덤프합니다.
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import jdk.jfr.FlightRecorder;
import jdk.jfr.Recording;
public final class ProgrammaticJfrRecording {
private static long workload() {
long checksum = 0;
for (int value = 0; value < 200_000; value++) {
checksum += (long) value * value;
}
return checksum;
}
public static void main(String[] args) throws Exception {
System.out.println("available=" + FlightRecorder.isAvailable());
Path file = Files.createTempFile("board-recording", ".jfr");
try (Recording recording = new Recording()) {
recording.enable("jdk.CPULoad").withPeriod(Duration.ofMillis(20));
recording.enable("jdk.ExecutionSample").withPeriod(Duration.ofMillis(10));
recording.start();
long checksum = workload();
recording.stop();
recording.dump(file);
System.out.println("checksum=" + checksum);
}
System.out.println("file=" + file);
System.out.println("bytes=" + Files.size(file));
}
}stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다
Recording이 NEW→RUNNING→STOPPED→CLOSED로 전환하고 dump는 STOPPED를 유지한 채 파일을 남깁니다. 수집 중지·파일 작성·내부 자원 해제의 다른 상태를 보여 줄 수 있습니다.
ProgrammaticJfrRecording이 직접 시작하는 경로의 메서드 순서를 읽습니다.
JFR 직접 시작의 정상 경로 · 수집과 보관의 구분
stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다
stop은 수집을 멈추고 데이터를 보관합니다. 이 예제는 보관 데이터를 dump한 뒤 try 블록 종료에서 close하며, 저장한 파일은 close 뒤에도 남습니다.
new Recording()
- 그 뒤 상태
- NEW
- 데이터와 자원
- 기록 객체 생성
start()
- 그 뒤 상태
- RUNNING
- 데이터와 자원
- 활성 이벤트 수집
stop()
- 그 뒤 상태
- STOPPED
- 데이터와 자원
- 수집 중단 · 데이터 보관
dump(file)
- 그 뒤 상태
- STOPPED
- 데이터와 자원
- 보관 데이터를 파일에 기록
try 블록 종료의 close()
- 그 뒤 상태
- CLOSED
- 데이터와 자원
- 기록 자원과 내부 데이터 해제
원문의 직접 시작 경로입니다. 예약 시작의 DELAYED 상태도 있으며, API는 RUNNING 중 dump도 허용합니다.
setDestination을 설정하지 않은 원문 경로입니다. 설정한 경우 stop이 파일에 저장하고 자동으로 닫는 정책은 이 흐름과 다릅니다.
이 예의 조건
- dump는 기록 상태를 바꾸지 않고 파일을 작성한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | setDestination을 설정하지 않은 직접 시작 원 코드의 정상 흐름 호출 순서다. NEW→RUNNING→STOPPED로 수집을 멈춘다. dump 후에도 STOPPED이며 파일이 작성된다. close가 내부 기록 데이터를 해제해 CLOSED가 되지만 작성한 파일은 남는다. destination 설정/DELAYED/RUNNING 중 dump의 별도 정책은 원 지원 주석에 있다. |
| 구분 | 내용 |
|---|---|
| 설명 | setDestination을 설정하지 않은 직접 시작 원 코드의 정상 흐름 호출 순서다. NEW→RUNNING→STOPPED로 수집을 멈춘다. dump 후에도 STOPPED이며 파일이 작성된다. close가 내부 기록 데이터를 해제해 CLOSED가 되지만 작성한 파일은 남는다. destination 설정/DELAYED/RUNNING 중 dump의 별도 정책은 원 지원 주석에 있다. |
| 전체 읽기 정보 | stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다 stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다 ProgrammaticJfrRecording이 직접 시작하는 경로의 메서드 순서를 읽습니다. JFR 직접 시작의 정상 경로 · 수집과 보관의 구분 stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다 stop은 수집을 멈추고 데이터를 보관합니다. 이 예제는 보관 데이터를 dump한 뒤 try 블록 종료에서 close하며, 저장한 파일은 close 뒤에도 남습니다. new Recording() 그 뒤 상태 NEW 데이터와 자원 기록 객체 생성 start() 그 뒤 상태 RUNNING 데이터와 자원 활성 이벤트 수집 stop() 그 뒤 상태 STOPPED 데이터와 자원 수집 중단 · 데이터 보관 dump(file) 그 뒤 상태 STOPPED 데이터와 자원 보관 데이터를 파일에 기록 try 블록 종료의 close() 그 뒤 상태 CLOSED 데이터와 자원 기록 자원과 내부 데이터 해제 원문의 직접 시작 경로입니다. 예약 시작의 DELAYED 상태도 있으며, API는 RUNNING 중 dump도 허용합니다. setDestination을 설정하지 않은 원문 경로입니다. 설정한 경우 stop이 파일에 저장하고 자동으로 닫는 정책은 이 흐름과 다릅니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 기록 생성 Recording을 NEW로 만듭니다. |
|
|
| 수집 시작 start 뒤 workload를 실행합니다. |
|
|
| 수집 정지 stop이 데이터를 보관합니다. |
|
|
| 파일 작성 dump(file)을 호출합니다. |
|
|
| 자원 종료 try 블록 종료에서 close합니다. |
|
|
stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다
Recording이 NEW→RUNNING→STOPPED→CLOSED로 전환하고 dump는 STOPPED를 유지한 채 파일을 남깁니다. 수집 중지·파일 작성·내부 자원 해제의 다른 상태를 보여 줄 수 있습니다.
ProgrammaticJfrRecording이 직접 시작하는 경로의 메서드 순서를 읽습니다.
JFR 직접 시작의 정상 경로 · 수집과 보관의 구분
stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다
stop은 수집을 멈추고 데이터를 보관합니다. 이 예제는 보관 데이터를 dump한 뒤 try 블록 종료에서 close하며, 저장한 파일은 close 뒤에도 남습니다.
new Recording()
- 그 뒤 상태
- NEW
- 데이터와 자원
- 기록 객체 생성
start()
- 그 뒤 상태
- RUNNING
- 데이터와 자원
- 활성 이벤트 수집
stop()
- 그 뒤 상태
- STOPPED
- 데이터와 자원
- 수집 중단 · 데이터 보관
dump(file)
- 그 뒤 상태
- STOPPED
- 데이터와 자원
- 보관 데이터를 파일에 기록
try 블록 종료의 close()
- 그 뒤 상태
- CLOSED
- 데이터와 자원
- 기록 자원과 내부 데이터 해제
원문의 직접 시작 경로입니다. 예약 시작의 DELAYED 상태도 있으며, API는 RUNNING 중 dump도 허용합니다.
setDestination을 설정하지 않은 원문 경로입니다. 설정한 경우 stop이 파일에 저장하고 자동으로 닫는 정책은 이 흐름과 다릅니다.
이 예의 조건
- dump는 기록 상태를 바꾸지 않고 파일을 작성한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | setDestination을 설정하지 않은 직접 시작 원 코드의 정상 흐름 호출 순서다. NEW→RUNNING→STOPPED로 수집을 멈춘다. dump 후에도 STOPPED이며 파일이 작성된다. close가 내부 기록 데이터를 해제해 CLOSED가 되지만 작성한 파일은 남는다. destination 설정/DELAYED/RUNNING 중 dump의 별도 정책은 원 지원 주석에 있다. |
| 구분 | 내용 |
|---|---|
| 설명 | setDestination을 설정하지 않은 직접 시작 원 코드의 정상 흐름 호출 순서다. NEW→RUNNING→STOPPED로 수집을 멈춘다. dump 후에도 STOPPED이며 파일이 작성된다. close가 내부 기록 데이터를 해제해 CLOSED가 되지만 작성한 파일은 남는다. destination 설정/DELAYED/RUNNING 중 dump의 별도 정책은 원 지원 주석에 있다. |
| 전체 읽기 정보 | stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다 stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다 ProgrammaticJfrRecording이 직접 시작하는 경로의 메서드 순서를 읽습니다. JFR 직접 시작의 정상 경로 · 수집과 보관의 구분 stop 뒤에도 dump할 데이터는 남고 close가 자원을 해제한다 stop은 수집을 멈추고 데이터를 보관합니다. 이 예제는 보관 데이터를 dump한 뒤 try 블록 종료에서 close하며, 저장한 파일은 close 뒤에도 남습니다. new Recording() 그 뒤 상태 NEW 데이터와 자원 기록 객체 생성 start() 그 뒤 상태 RUNNING 데이터와 자원 활성 이벤트 수집 stop() 그 뒤 상태 STOPPED 데이터와 자원 수집 중단 · 데이터 보관 dump(file) 그 뒤 상태 STOPPED 데이터와 자원 보관 데이터를 파일에 기록 try 블록 종료의 close() 그 뒤 상태 CLOSED 데이터와 자원 기록 자원과 내부 데이터 해제 원문의 직접 시작 경로입니다. 예약 시작의 DELAYED 상태도 있으며, API는 RUNNING 중 dump도 허용합니다. setDestination을 설정하지 않은 원문 경로입니다. 설정한 경우 stop이 파일에 저장하고 자동으로 닫는 정책은 이 흐름과 다릅니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 기록 생성 Recording을 NEW로 만듭니다. |
|
|
| 수집 시작 start 뒤 workload를 실행합니다. |
|
|
| 수집 정지 stop이 데이터를 보관합니다. |
|
|
| 파일 작성 dump(file)을 호출합니다. |
|
|
| 자원 종료 try 블록 종료에서 close합니다. |
|
|
아주 짧은 작업 부하에서는 표본 수집 이벤트가 0개일 수 있습니다. 원문은 available=false여도 중단하지 않고 Recording 생성을 시도하며, JFR을 사용할 수 없는 환경의 대체 경로는 구현하지 않았습니다.
기록 파일 생성과 체크섬을 기능 확인 기준으로 삼고, 표본 개수는 작업 부하의 지속 시간과 수집 주기를 함께 고려해 해석합니다.
예를 들어 정상 파일을 읽었는데 실행 표본이 0개라면, 파일 저장 실패와 표본 부족은 서로 다른 가설입니다. 활성 설정과 10 ms 주기, 작업 부하의 지속 시간을 먼저 비교합니다. 파일 크기가 양수라는 사실도 특정 이벤트나 원하는 필드가 저장되었다는 검사까지 대신하지는 않습니다. Recording API는 시작·중단·덤프·닫기를 각각 다른 계약으로 규정합니다.
사용자 정의 이벤트로 도메인 연산을 시간선에 포함
게시글의 저장 시간, 사용자·게시글 식별자, 결과를 사용자 정의 JFR 이벤트로 기록하면 JVM 이벤트와 같은 시간축에서 볼 수 있습니다.
비밀번호나 본문 같은 민감 데이터는 필드로 넣지 않습니다.
import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
import jdk.jfr.StackTrace;
public final class CustomBoardOperationEvent {
@Name("com.example.BoardOperation")
@Label("Board operation")
@Category({"Application", "Board"})
@StackTrace(false)
private static final class BoardOperation extends Event {
@Label("Operation")
String operation;
@Label("User id")
long userId;
@Label("Success")
boolean success;
}
static void save(long userId) {
BoardOperation event = new BoardOperation();
event.operation = "save-summary";
event.userId = userId;
event.begin();
try {
long checksum = 0;
for (int value = 0; value < 10_000; value++) {
checksum += value;
}
event.success = checksum > 0;
} finally {
event.commit();
}
}
public static void main(String[] args) {
save(7);
System.out.println("event-committed");
}
}이 예제의 event-committed 출력은 commit() 호출 뒤까지 진행했다는 뜻입니다. 실행 중인 기록이 없으면 파일에 이벤트가 저장됐다는 증거가 되지 않습니다. begin() 뒤 end() 없이 commit()하면 그 시점에 지속 시간 측정이 끝납니다.
event.isEnabled()와 shouldCommit()을 사용해 비용이 큰 필드 계산을 피할 수 있습니다.
사용자 정의 이벤트 스키마는 대시보드 소비자와 함께 버전 관리합니다.
RecordingFile 이벤트 확인
다음 독립 실행형 예제는 사용자 정의 이벤트 하나를 기록하고 파일을 읽어 이벤트 개수와 필드를 확인합니다. 이 원문을 JDK 25.0.4.1에서 한 번 실행해 user=7, score=95, count=1을 확인했고 생성된 JFR 파일을 보존했습니다.
import java.nio.file.Files;
import java.nio.file.Path;
import jdk.jfr.Event;
import jdk.jfr.Name;
import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordingFile;
public final class JfrCustomEventRoundTrip {
@Name("com.example.BoardScore")
private static final class BoardScore extends Event {
long userId;
int score;
}
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("board-score", ".jfr");
try (Recording recording = new Recording()) {
recording.enable("com.example.BoardScore");
recording.start();
BoardScore event = new BoardScore();
event.userId = 7;
event.score = 95;
event.commit();
recording.stop();
recording.dump(file);
}
int count = 0;
try (RecordingFile events = new RecordingFile(file)) {
while (events.hasMoreEvents()) {
RecordedEvent event = events.readEvent();
if (event.getEventType().getName().equals("com.example.BoardScore")) {
count++;
System.out.println("user=" + event.getLong("userId"));
System.out.println("score=" + event.getInt("score"));
}
}
}
System.out.println("count=" + count);
}
}운영 수집 선택 기준
- 짧은 장애 분석은
jcmd <pid> JFR.start name=incident settings=profile duration=... filename=...로 시작합니다. - 장기간 저부하로 기록할 때는 디스크 크기와 최대 보존 기간을 제한한 연속 설정을 사용합니다.
- CPU 가설에는
ExecutionSample·NativeMethodSample, 할당에는ObjectAllocationSample·TLAB, 락에는JavaMonitorEnter이벤트를 확인합니다. - 이벤트가 0건이라는 결과는 문제가 없다는 뜻이 아닙니다. 이벤트 비활성 상태·임계값·표본 수집 주기·작업 부하 기간을 확인합니다.
- 기록 시간 범위를 애플리케이션 로그의 요청 ID·배포 이벤트와 맞춥니다.
JFR을 켜면 프로파일러가 더 필요 없나요?
JFR은 폭넓은 저부하 근거 수집에 강하지만 특정 네이티브 병목 지점, 할당 보유, 줄 단위 디버깅에는 다른 프로파일러·힙 분석기가 필요할 수 있습니다.
JFR로 가설을 좁힌 뒤 목적에 맞는 도구를 추가합니다.
연습 문제
게시글 제목과 조회 수를 가진 이벤트를 기록 파일에 저장하고 다시 읽어 한 건인지 확인하세요.
이벤트 이름은 com.example.PostPublished, 제목은 JFR introduction, 조회 수는 45입니다. 먼저 이벤트를 활성화한 기록의 시작·커밋·중단·덤프·닫기 순서를 작성합니다. 기본 확인은 해당 이벤트 타입 한 건이며, 추가 확인에서는 읽은 title과 viewCount도 입력과 같은지 검사합니다. 타입만 맞고 제목이 다른 한 건은 추가 기준을 통과할 수 있는지 설명합니다.
해설 보기
import java.nio.file.Files;
import java.nio.file.Path;
import jdk.jfr.Event;
import jdk.jfr.Name;
import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordingFile;
public final class JfrPostPublishedEventSolution {
@Name("com.example.PostPublished")
private static final class PostPublished extends Event {
String title;
int viewCount;
}
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("post-published", ".jfr");
try (Recording recording = new Recording()) {
recording.enable("com.example.PostPublished");
recording.start();
PostPublished event = new PostPublished();
event.title = "JFR introduction";
event.viewCount = 45;
event.commit();
recording.stop();
recording.dump(file);
}
long count;
try (var stream = RecordingFile.readAllEvents(file).stream()) {
count =
stream.filter(
event ->
event.getEventType()
.getName()
.equals("com.example.PostPublished"))
.count();
}
System.out.println("count=" + count);
}
}종료 기준은 이벤트 개수가 1인 것입니다. 이 답안은 이벤트 타입과 건수만 확인하며 title·viewCount의 읽기 값은 검사하지 않습니다. 이 연습 문제를 별도로 실행해 관측한 결과는 아닙니다.
임시 기록의 삭제와 운영 파일의 보존 정책은 별도로 적용합니다.
추가 기준에서는 해당 타입의 한 이벤트에서 getString("title")과 getInt("viewCount")를 읽어 제목과 45를 비교합니다. 기본 답안의 건수 확인은 유지하면서 이 검사를 덧붙이면, 같은 타입의 다른 데이터가 저장된 경우도 찾을 수 있습니다. 임시 파일이 있다는 이유만으로 이벤트 필드의 정상 값을 선언하지 않습니다.