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));
}
}ProgrammaticJfrRecording이 직접 시작하는 경로의 메서드 순서를 읽습니다.
| 원문의 동작 | 그 뒤 상태 | 데이터와 자원 |
|---|---|---|
| new Recording() | NEW | 기록 객체 생성 |
| start() | RUNNING | 활성 이벤트 수집 |
| stop() | STOPPED | 수집 중단 · 데이터 보관 |
| dump(file) | STOPPED | 보관 데이터를 파일에 기록 |
| try 블록 종료의 close() | CLOSED | 기록 자원과 내부 데이터 해제 |
- new Recording()
- 그 뒤 상태: NEW데이터와 자원: 기록 객체 생성
- start()
- 그 뒤 상태: RUNNING데이터와 자원: 활성 이벤트 수집
- stop()
- 그 뒤 상태: STOPPED데이터와 자원: 수집 중단 · 데이터 보관
- dump(file)
- 그 뒤 상태: STOPPED데이터와 자원: 보관 데이터를 파일에 기록
- try 블록 종료의 close()
- 그 뒤 상태: CLOSED데이터와 자원: 기록 자원과 내부 데이터 해제
원문의 직접 시작 경로입니다. 예약 시작의 DELAYED 상태도 있으며, API는 RUNNING 중 dump도 허용합니다.
아주 짧은 작업 부하에서는 표본 수집 이벤트가 0개일 수 있습니다. 원문은 available=false여도 중단하지 않고 Recording 생성을 시도하며, JFR을 사용할 수 없는 환경의 대체 경로는 구현하지 않았습니다.
기록 파일 생성과 체크섬을 기능 확인 기준으로 삼고, 표본 개수는 작업 부하의 지속 시간과 수집 주기를 함께 고려해 해석합니다.
사용자 정의 이벤트로 도메인 연산을 시간선에 포함
게시글의 저장 시간, 사용자·게시글 식별자, 결과를 사용자 정의 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로 가설을 좁힌 뒤 목적에 맞는 도구를 추가합니다.
연습 문제
게시글 제목과 조회 수를 가진 이벤트를 기록 파일에 저장하고 다시 읽어 한 건인지 확인하세요.
해설 보기
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의 읽기 값은 검사하지 않습니다. 이 연습문제를 별도로 실행해 관측한 결과는 아닙니다.
임시 기록의 삭제와 운영 파일의 보존 정책은 별도로 적용합니다.