4. 가상머신 성능 모니터링과 문제 해결 도구

2026. 1. 10. 01:14Life Style/jvm 밑바닥까지 파헤치기

4.1 들어가며

- 가상머신 장애 처리 및 분석 도구를 적절히 이용하면, 데이터를 분석하여 문제를 식별하고 해결하는 일이 쉬워진다.

- 어떤 도구들이 있는지 알아보자.

4.2 기본적인 문제 해결 도구

4.2.1 jps(Java Virtual Machine Process Status Tool): 가상머신 프로세스 상태 도구

1️⃣ jps가 하는 일

  • 실행 중인 Java 프로세스의 PID 확인
  • 각 JVM이 어떤 클래스/애플리케이션으로 실행됐는지 확인

👉 JVM 전용 ps 명령어라고 생각하면 됨

2️⃣ 기본 사용법

옵션 의미 설명 출력 예시
-q Quiet PID만 출력 (프로세스 이름 제외) 12345
-m Main arguments main 메서드로 전달된 인자 출력 12345 SpringBootApplication --spring.profiles=prod
-l Long listing 메인 클래스의 전체 패키지명 출력 12345 com.example.demo.SpringBootApplication
-v JVM arguments JVM 실행 옵션 출력 (-Xms, -Xmx, GC 옵션 등) 12345 SpringBootApplication -Xms512m -Xmx1024m

4.2.2 jstat(Java Virtual Machine Statistics Monitoring Tool): 가상 머신 통계 정보 모니터링 도구

1️⃣ jstat이 뭐 하는 도구냐면

  • GC(가비지 컬렉션) 상태
  • GC 횟수, GC 소요 시간

실시간에 가깝게 숫자로 확인할 수 있다.

👉 한마디로 “JVM이 지금 어떻게 살아 움직이고 있는지 수치로 보는 도구”

2️⃣ 기본 사용법

jstat -옵션 PID [interval(ms)] [count]

예시: jstat -gc 12345 1000

의미:

  • 12345 → JVM PID
  • 1000 → 1초마다 출력

4.2.3 jinfo: 자바 설정 정보 도구

1️⃣ jinfo가 뭐 하는 도구냐면

  • JVM이 어떤 옵션으로 실행됐는지 확인
  • Heap, GC, JIT 관련 플래그 값 조회
  • 일부 옵션은 실행 중 동적 변경 가능

👉 한마디로, “이 JVM이 어떤 설정으로 살아가고 있는지 보는 도구”

2️⃣ 기본 사용법

jinfo [options] PID

4.2.4 jmap: 자바 메모리 매핑 도구

1️⃣ jmap이 뭐 하는 도구냐면

  • Heap 메모리 구조와 사용량 확인
  • 객체 분포(Class별 인스턴스 수/용량) 조회
  • Heap Dump 생성 → 메모리 누수 분석의 핵심 자료

👉 한마디로, “JVM 메모리를 CT 촬영하듯 들여다보는 도구”(힙 스냅숏을 파일로 덤프해주는 자바용 메모리 맵 명령어)

2️⃣ 기본 사용법

jmap [옵션] PID

4.2.5 jhat: 가상 머신 힙 덤프 스냅숏 분석 도구

1️⃣ jhat이 뭐 하는 도구냐면

jhat은 jmap으로 찍어둔 힙 덤프 파일을 사람이 보기 쉽게 분석해주는 도구

도구 비유 역할
jmap 📸 카메라 집 안을 사진 찍어서 파일로 저장
jhat 🔍 사진 분석기 그 사진을 확대·분석해서 구조를 보여줌

👉 jhat은 “사진을 찍는 도구”가 아니라 “찍어둔 사진을 분석하는 도구”

2️⃣ 실제 사용 흐름

① 힙 덤프 생성 : jmap -dump:format=b,file=heap.hprof 5404

② jhat으로 분석 : jhat heap.hprof

출력: Listening on http://localhost:7000

③ 브라우저에서 접속: http://localhost:7000

➡️ 힙 구조, 객체 관계를 웹 화면으로 확인

3️⃣ jhat으로 볼 수 있는 것들

  • 객체 개수 / 메모리 사용량

  • 클래스별 메모리 점유율

  • 객체 참조 관계

  • Root 객체(GC Root)

    그런데… 실무에서는 잘 안 씀 ⚠️

이유

  • 분석 속도 느림
  • 메모리 많이 먹음
  • 기능 제한적
  • Java 9부터 deprecated

요즘 대안

  • Eclipse MAT (가장 많이 씀)
  • VisualVM
  • YourKit

4.2.6 jstack: 자바 스택 추적 도구

1️⃣ jstack이 뭐 하는 도구냐면

실행 중인 자바 프로그램에서 “지금 모든 스레드가 뭘 하고 있는지”를 한 번에 찍어주는 도구

2️⃣ 기본 사용법

jstack [옵션] PID
옵션 이름 무엇을 보여주나 언제 쓰나
-l Long listing 락(lock) 정보 + 모니터 정보 포함한 상세 스레드 덤프 데드락, 락 경합 분석
-e Extended 스레드의 네이티브 프레임 정보 포함 JNI(Java Native Interface: Java 코드가 C / C++ 같은 네이티브 코드와 통신하기 위한 표준 인터페이스) / 네이티브 코드 문제(Java 코드가 아니라 C/C++ 영역에서 발생하는 문제) CPU 100% 원인 추적

4.3 GUI 도구

4.3.1 JHSDB: 서비스 에이전트 기반 디버깅 도구

1️⃣ JHSDB가 뭐 하는 도구냐면

JHSDB는 JVM 내부를 “디버거처럼” 열어보는 도구

2️⃣ 예제 코드

/**
 * JHSDB 분석용 테스트 코드
 *
 * 목적:
 *  - static 객체
 *  - instance 객체
 *  - local 객체
 * 가 JVM 메모리에 어떻게 존재하는지 확인
 */
public class JHSDBTestCase {

    // 1️⃣ static 필드 (클래스에 귀속)
    // 클래스 로딩 시 1회 생성
    static ObjectHolder staticObj = new ObjectHolder();

    // 2️⃣ instance 필드 (객체에 귀속)
    // JHSDBTestCase 인스턴스 생성 시 생성
    ObjectHolder instanceObj = new ObjectHolder();

    public static void main(String[] args) {
        // 테스트용 인스턴스 생성
        JHSDBTestCase test = new JHSDBTestCase();

        // 메서드 호출
        test.foo();
    }

    void foo() {
        // 3️⃣ local 변수 (메서드에 귀속)
        // 스택 프레임에 참조 존재
        ObjectHolder localObj = new ObjectHolder();

        System.out.println("done"); // 브레이크 포인트용
    }

    /**
    * 단순 객체 (내용 중요하지 않음)
    */
    private static class ObjectHolder {}
}
구분 변수 참조 위치 실제 객체 위치
static staticObj Metaspace(Class) Heap
instance instanceObj Heap(Object) Heap
local localObj Stack(Frame) Heap

4.3.2 JConsole: 자바 모니터링 및 관리 콘솔

1️⃣ 왜 이런 도구가 필요할까?

JMX에 기반한 GUI 모니터링 및 관리 도구

2️⃣ 예제

1. 메모리 모니터링 예제

64KB 크기의 객체를 반복적으로 생성하여 리스트에 담음으로써,

메모리 사용량이 계단식으로 증가하다가 Garbage Collection(GC)이 발생하는 과정을 보여줌

import java.util.ArrayList;
import java.util.List;

/**
 * VM 매개변수: -Xms100m -Xmx100m -XX:+UseSerialGC
 */
public class MemoryMonitoringTest {

    static class OOMObject {
        // 64KB 용량의 객체
        public byte[] placeholder = new byte[64 * 1024];
    }

    public static void fillHeap(int num) throws InterruptedException {
        List<OOMObject> list = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            // 약 64KB의 데이터를 리스트에 추가
            Thread.sleep(50); // 모니터링을 위해 지연 시간 추가
            list.add(new OOMObject());
        }
        // 이 지점에서 명시적으로 GC 호출 시도 (책의 설명 참고)
        System.gc();
    }

    public static void main(String[] args) throws Exception {
        fillHeap(1000);
    }
}

주요 내용 요약

  • 현상: fillHeap 메서드가 실행되는 동안 Eden 영역과 Old 영역의 메모리 사용량이 계속 우상향합니다.
  • 분석: list 변수가 메서드 내에서 살아있는 지역 변수이기 때문에, System.gc()를 호출해도 리스트 내의 OOMObject들은 참조가 유지되어 메모리에서 해제되지 않습니다.
  • 결론: JConsole의 차트를 통해 메모리가 해제되지 않는 구간(메모리 누수 위험 등)을 시각적으로 확인하는 법을 설명합니다.

2. 스레드 모니터링 예제

2.1 대기 상태에 빠진 스레드 시연코드

package org.fenixsoft.jvm.chapter4;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * @author zzm
 */
public class ThreadMonitoringTest {
    /**
     * 무한 루프를 도는 스레드 생성: 아무 작업도 안 하지만 계속 루프를 돌기 때문에 CPU 코어 하나를 100% 점유
     * Threads 탭: testBusyThread가 RUNNABLE
     * CPU 사용률 급격히 상승
     * 스택 트레이스 java.lang.Thread.run(): ThreadMonitoringTest.java:17
     * ➡️ “CPU를 잡아먹는 스레드”를 관찰하는 목적
    */
    public static void createBusyThread() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true)  // 17번째 줄
                    ;
            }
        }, "testBusyThread");
        thread.start();
    }

    /**
     * 락을 대기하는 스레드 생성(실행 흐름)
     * 1. synchronized(lock) 진입 → 락 획득
     * 2. lock.wait() 호출
     * 3. 락을 반납
     * 4. WAITING 상태로 전환
     * 5. notify()가 오기 전까지 영원히 대기
     * Threads 탭: testLockThread → WAITING
     * 스택 트레이스 java.lang.Object.wait(Native Method): ThreadMonitoringTest.java:38
     */
    public static void createLockThread(final Object lock) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "testLockThread");
        thread.start();
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // 콘솔(터미널)에서 '엔터' 키를 치면 다음 줄 실행
        br.readLine();
        createBusyThread();

        // 콘솔(터미널)에서 '엔터' 키를 치면 다음 줄 실행
        br.readLine();
        Object obj = new Object();
        createLockThread(obj);
    }
}

1️⃣ 전체 실행 흐름

프로그램 실행 ↓ 엔터 입력 (1번)

→ 무한 루프 스레드 생성 (CPU 100% 사용) ↓ 엔터 입력 (2번)

→ 락 대기 스레드 생성 (WAITING 상태)

2.2 교착 상태 코드 예

package org.fenixsoft.jvm.chapter4;

/**
 * @author zzm
 */
public class DeadLockMonitoringTest {
    static class SynAddRunnalbe implements Runnable {
        int a, b;
        public SynAddRunnalbe(int a, int b) {
            this.a = a;
            this.b = b;
        }

        @Override
        public void run() {
            synchronized (Integer.valueOf(a)) {
                synchronized (Integer.valueOf(b)) {
                    System.out.println(a + b);
                }
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(new SynAddRunnalbe(1, 2)).start();  // 1 + 2를 수행하는 스레드
            new Thread(new SynAddRunnalbe(2, 1)).start();  // 2 + 1을 수행하는 스레드
        }
    }
}

1️⃣ 전체 구조 한눈에 보기

Thread A: lock(1) → lock(2)

Thread B: lock(2) → lock(1)

👉 락 획득 순서가 서로 반대 → 교착 상태 발생 가능

2️⃣ 교착 상태 4가지 조건(이 코드에서 전부 만족)

조건 설명 코드에서
상호 배제 락은 한 스레드만 synchronized
점유 대기 락을 잡은 채 다른 락 대기 중첩 synchronized
비선점 락 강제 회수 불가 intrinsic lock
순환 대기 락을 원형으로 대기 1 → 2 → 1

3️⃣ Threads 탭

  • 여러 스레드가 BLOCKED
  • CPU 사용량 거의 없음
  • 프로그램이 멈춘 것처럼 보임

4.3.3 VisualVM: 다용도 문제 대응 도구(설치 및 예제)

4.3.4 JMC: 지속 가능한 온라인 모니터링 도구(설치 및 예제)

4.4 핫스팟 가상 머신 플러그인과 도구

  • HotSpot JVM: 실행 중에 ‘자주 실행되는 코드(Hot Spot)’를 찾아 인터프리터 → JIT 컴파일 → 최적화까지 자동으로 수행하는 JVM 구현체

4.4.1 HSDIS(HotSpot Disassembler)

  • 오라클이 권장하는 핫스팟 가상머신 JIT 컴파일 코드용 디스어셈블리 플러그인
  • JIT 컴파일러: 실행 중인 프로그램에서 ‘자주 실행되는 코드’를 골라 인터프리터용 바이트코드를 CPU가 직접 실행할 수 있는 기계어로 컴파일하는 엔진
  • 디셈블러(Disassembler): 컴퓨터가 실행하는 기계어를, 사람이 읽을 수 있게 되돌려 보여주는 도구
  • 컴파일러(Compiler): 사람이 읽는 코드를, 컴퓨터가 바로 실행할 수 있는 코드로 바꿔주는 변환기

4.4.2 JITWatch

  • 핫스팟 JIT 컴파일러용 로그 분석 및 시각화 도구, HSDIS와 자주 함께 쓰임
  • JIT 로그를 사람이 읽을 수 있게 번역해 주는 도구