본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
38장 : 선택 심화 — JVM 진단

JFR 수집과 검증

닫힌 Recording 재시작 실패를 재현하고 명령줄 기록, 프로그래밍 방식 덤프, 사용자 정의 StudyOperation 이벤트, RecordingFile 분석을 실행합니다.

Java Flight Recorder는 JVM과 애플리케이션 이벤트를 적은 부하로 기록하는 JDK 도구입니다. CPU 표본, 할당, GC, 락, 스레드, 소켓, 예외를 같은 시간선에서 비교할 수 있습니다. 기록을 켰다는 사실보다 어떤 설정·기간·작업 부하에서 수집했고 어떤 가설을 확인할지 정하는 것이 중요합니다.

Recording의 상태 전이

RecordingNEW, RUNNING, STOPPED, CLOSED의 생명 주기를 가집니다. close한 뒤 다시 start하면 실제 IllegalStateException이 발생합니다.

lab/ClosedJfrRecordingFailure.java
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-resourcesclose를 보장하는 것입니다. 지속 관찰은 JFR 구성이나 외부 관리 명령으로 운영하며 요청마다 Recording을 만들지 않습니다.

명령줄 JFR 기록

애플리케이션 시작 시점부터 작업 부하를 수집하려면 JVM 옵션을 사용합니다.

java -XX:StartFlightRecording=filename=study.jfr,settings=profile,duration=30s,dumponexit=true Application
jfr summary study.jfr
jfr print --events jdk.CPULoad,jdk.GarbageCollection,jdk.JavaMonitorEnter study.jfr

관찰 결과 jfr summary는 이벤트 타입별 개수와 크기를 보여 주고 jfr print는 선택한 이벤트의 시각·스레드·스택·필드를 출력합니다. 기록 파일에는 환경·경로·인수 같은 민감 정보가 들어갈 수 있으므로 접근 권한과 보존 기간을 제한합니다.

프로그래밍 방식 JFR 기록

API 기록은 통합 확인 프로그램에서 특정 연산 전후를 감쌀 때 유용합니다. enable()로 이벤트 임계값·스택 추적을 조정하고 stop() 뒤 파일에 덤프합니다.

src/ProgrammaticJfrRecording.java
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("study-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));
    }
}

아주 짧은 작업 부하에서는 표본 수집 이벤트가 0개일 수 있습니다. 기록 파일 생성과 체크섬을 기능 확인 기준으로 삼고, 표본 개수는 작업 부하의 지속 시간과 수집 주기를 함께 고려해 해석합니다.

사용자 정의 이벤트로 도메인 연산을 시간선에 포함

학습 기록의 저장 시간, 사용자·메모 식별자, 결과를 사용자 정의 JFR 이벤트로 기록하면 JVM 이벤트와 같은 시간축에서 볼 수 있습니다. 비밀번호나 본문 같은 민감 데이터는 필드로 넣지 않습니다.

src/CustomStudyOperationEvent.java
import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
import jdk.jfr.StackTrace;

public final class CustomStudyOperationEvent {
    @Name("com.example.StudyOperation")
    @Label("Study operation")
    @Category({"Application", "Study"})
    @StackTrace(false)
    private static final class StudyOperation extends Event {
        @Label("Operation")
        String operation;

        @Label("User id")
        long userId;

        @Label("Success")
        boolean success;
    }

    static void save(long userId) {
        StudyOperation event = new StudyOperation();
        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.isEnabled()shouldCommit()을 사용해 비용이 큰 필드 계산을 피할 수 있습니다. 사용자 정의 이벤트 스키마는 대시보드 소비자와 함께 버전 관리합니다.

RecordingFile 이벤트 확인

다음 독립 실행형 예제는 사용자 정의 이벤트 하나를 기록하고 파일을 읽어 이벤트 개수와 필드를 확인합니다.

src/JfrCustomEventRoundTrip.java
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.StudyScore")
    private static final class StudyScore extends Event {
        long userId;
        int score;
    }

    public static void main(String[] args) throws Exception {
        Path file = Files.createTempFile("study-score", ".jfr");
        try (Recording recording = new Recording()) {
            recording.enable("com.example.StudyScore");
            recording.start();
            StudyScore event = new StudyScore();
            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.StudyScore")) {
                    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로 가설을 좁힌 뒤 목적에 맞는 도구를 추가합니다.

연습 문제

주제와 분을 가진 이벤트를 기록 파일에 저장하고 다시 읽어 한 건인지 확인하세요.

해설 보기
exercise/JfrCompletionEventSolution.java
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 JfrCompletionEventSolution {
    @Name("com.example.StudyCompleted")
    private static final class StudyCompleted extends Event {
        String topic;
        int minutes;
    }

    public static void main(String[] args) throws Exception {
        Path file = Files.createTempFile("completed", ".jfr");
        try (Recording recording = new Recording()) {
            recording.enable("com.example.StudyCompleted");
            recording.start();
            StudyCompleted event = new StudyCompleted();
            event.topic = "jfr";
            event.minutes = 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.StudyCompleted"))
                            .count();
        }
        System.out.println("count=" + count);
    }
}

종료 기준은 이벤트 개수가 1인 것입니다. 임시 기록의 삭제와 운영 파일의 보존 정책은 별도로 적용합니다.