2026. 1. 10. 01:14ㆍLife 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] PID4.2.4 jmap: 자바 메모리 매핑 도구
1️⃣ jmap이 뭐 하는 도구냐면
- Heap 메모리 구조와 사용량 확인
- 객체 분포(Class별 인스턴스 수/용량) 조회
- Heap Dump 생성 → 메모리 누수 분석의 핵심 자료
👉 한마디로, “JVM 메모리를 CT 촬영하듯 들여다보는 도구”(힙 스냅숏을 파일로 덤프해주는 자바용 메모리 맵 명령어)
2️⃣ 기본 사용법
jmap [옵션] PID4.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 로그를 사람이 읽을 수 있게 번역해 주는 도구
'Life Style > jvm 밑바닥까지 파헤치기' 카테고리의 다른 글
| 6. 클래스 파일 구조 (0) | 2026.01.31 |
|---|---|
| 5. 최적화 사례 분석 및 실전 (0) | 2026.01.17 |
| 1. 자바 기술 시스템 소개 (0) | 2025.12.06 |