Java의 플랫폼 독립성은 JVM의 강력한 설계에서 비롯됩니다. 이 문서에서는 클래스 로딩, 메모리 관리, GC 메커니즘 등 핵심 원리를 코드 예제와 함께 살펴봅니다.
클래스 로딩 메커니즘: 바이트코드에서 Class 객체까지
클래스 로딩 세 단계
- 로딩: 바이트코드를 찾아 Class 객체 생성
- 링킹: 검증 → 준비 → 해석
- 초기화: 정적 코드 블록 실행 및 변수 초기화
사용자 정의 클래스 로더 실습
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
// 1. 바이트코드 파일 읽기
String filePath = "classes/" + className.replace('.', '/') + ".class";
byte[] byteCodes = Files.readAllBytes(Paths.get(filePath));
// 2. 바이트 배열을 Class 객체로 변환
return defineClass(className, byteCodes, 0, byteCodes.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
CustomClassLoader loader = new CustomClassLoader();
Class<?> loadedClass = loader.loadClass("com.example.DynamicClass");
// 3. 리플렉션으로 인스턴스 생성 및 메서드 호출
Object instance = loadedClass.getDeclaredConstructor().newInstance();
loadedClass.getMethod("execute").invoke(instance);
}
}
메모리 모델: 데이터 저장 영역 다섯 가지
JVM 메모리 구조
| 영역 | 설명 | 스레드 공유 |
|---|---|---|
| 힙(Heap) | 객체 인스턴스 저장 | O |
| 스택(Stack) | 메서드 호출과 지역 변수 | X |
| 메소드 영역 | 클래스 정보, 상수 풀 | O |
| 네이티브 메소드 스택 | 네이티브 메서드 호출 | X |
| 프로그램 카운터 | 현재 명령어 주소 | X |
힙 메모리 분석
힙 메모리
- 대상별 구조
- 신생대(Young Generation): 새로운 객체는 Eden 영역에 우선 할당되며, Minor GC 후 생존 객체는 Survivor 영역으로 이동합니다(나이 증가). 기본적으로 15번 이상 GC를 겪으면 Old Generation으로 승격됩니다.
- 노년대(Old Generation): 장기적으로 살아남은 객체를 저장하는 영역입니다. Full GC(표시-정리 알고리즘)가 발생합니다.
- 메모리 설정 파라미터
-Xms512m // 초기 힙 크기
-Xmx2048m // 최대 힙 크기
-XX:NewRatio=2 // 노년대:신생대 비율 2:1
-XX:SurvivorRatio=8 // Eden:Survivor 비율 8:1
비힙 메모리
| 영역 | 저장 내용 | 예외 시나리오 | JDK 버전 변화 |
|---|---|---|---|
| 메소드 영역 | 클래스 메타정보, 상수 풀 | OutOfMemoryError: Metaspace | JDK 8+에서 PermGen 대체 |
| 가상 머신 스택 | 스택 프레임(지역 변수 테이블, 연산자 스택) | StackOverflowError | 변경 없음 |
| 프로그램 카운터 | 현재 스레드의 명령어 주소 | 유일한 OOM 없는 영역 | 변경 없음 |
스택 프레임 구조
public class StackFrameExample {
public static void main(String[] args) {
int x = 10; // 지역 변수 테이블
int y = 20;
int total = add(x, y); // 메서드 호출 → 새로운 스택 프레임
// 연산자 스택 작업 예시
int z = x + y * 2; // 연산자 스택 계산
}
static int add(int a, int b) {
return a + b; // 반환 값은 호출자 스택 프레임에 전달
}
}
클래스 로딩 메커니즘과 부모 위임 모델
- 클래스 로딩 과정
- 검증 단계: 바이트코드의 유효성을 확인(예: 매직 넘버 CAFEBABE).
- 준비 단계: 정적 변수에 메모리 할당 및 초기값 설정(예: int 타입 변수는 0).
- 부모 위임 모델
// ClassLoader.loadClass() 핵심 로직
protected Class<?> loadClass(String className, boolean resolve) {
synchronized (getClassLoadingLock(className)) {
Class<?> cls = findLoadedClass(className); // 이미 로딩된 클래스 검사
if (cls == null) {
if (getParent() != null) {
cls = getParent().loadClass(className, false); // 부모 로더 위임
} else {
cls = findBootstrapClassOrNull(className); // 부트스트랩 클래스 로더
}
if (cls == null) {
cls = findClass(className); // 직접 로딩
}
}
return cls;
}
}
가비지 컬렉션 메커니즘과 알고리즘
- GC 트리거 조건
| GC 종류 | 트리거 조건 | 적용 범위 |
|---|---|---|
| Minor GC | Eden 영역 부족 | 신생대만 |
| Full GC | 노년대 또는 메소드 영역 부족, System.gc() 호출 | 전체 힙 + 메소드 영역 |
- 알고리즘 비교
| 알고리즘 | 동작 방식 | 사용 시나리오 | 단점 |
|---|---|---|---|
| 복사 알고리즘 | 생존 객체를 Survivor 영역으로 복사 | 신생대(Eden) | 50% 메모리 낭비 |
| 마크-스윕 알고리즘 | 마킹 후 직접 제거 | 노년대(CMS) | 메모리 파편화 |
| 마크-컴팩트 알고리즘 | 마킹 후 메모리 정리 | 노년대(G1) | 시간 소모 큼 |
실행 엔진: 바이트코드에서 기계어로
JIT 컴파일 최적화 예제
public class JITOptimizationDemo {
// 핫 코드 감지 임계치: -XX:CompileThreshold=10000
public static void main(String[] args) {
long startTime = System.nanoTime();
// 핫 코드 (반복 횟수가 CompileThreshold를 초과하면 JIT 컴파일이 트리거됨)
for (int i = 0; i < 15000; i++) {
compute(i);
}
System.out.println("소요 시간: " + (System.nanoTime() - startTime) + "ns");
}
static int compute(int num) {
// 복잡한 계산 (JIT 내부에서 최적화 가능)
return num * num + 2 * num + 1;
}
}
분할 컴파일 전략
| 레벨 | 컴파일러 | 최적화 수준 | 시작 속도 |
|---|---|---|---|
| 0 | 해석 실행 | 없음 | 가장 빠름 |
| 1 | C1 간단 컴파일 | 기본 | 빠름 |
| 2 | C1 부분 최적화 | 중간 | 중간 |
| 3 | C1 완전 최적화 | 전체 | 느림 |
| 4 | C2 심층 최적화 | 적극적 | 가장 느림 |
실무 적용 및 성능 최적화
공통 성능 문제 해결
- 메모리 누수 감지
jmap -dump:format=b,file=heap_dump.bin <pid>
jhat heap_dump.bin # 힙 덤프 분석
- GC 최적화 파라미터
-XX:+UseG1GC
-Xms4g -Xmx4g
-XX:MaxGCPauseMillis=200
- JIT 진단
-XX:+PrintCompilation # 컴파일 로그 출력
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining # 내부 최적화 결정 보기
- CPU 사용률 급증
top -Hp <pid> # CPU 사용량이 높은 스레드 확인
jstack <pid> | grep <nid> # 스레드 ID 변환 및 스택 정보 확인
JVM 모니터링 도구 매트릭스
| 도구 | 역할 | 주요 명령 |
|---|---|---|
| jstat | GC 통계 | jstat -gc 1000 |
| jstack | 스레드 스냅샷 | jstack -l |
| jvisualvm | 그래픽 모니터링 | 내장 프로파일러 |
| Arthas | 온라인 진단 | watch demo.MathGame primeFactors |