JVM 공부를 제대로 하고 싶어서 하나하나 해보기로 했다. 저우즈밍의 저서인 JVM 밑바닥까지 파헤치기로 학습의 중심을 잡고 공식 문서와 기타 실습을 통해 내용을 보완해가며 작성할거다. Java 버전은 나온지 너무 오래되지 않은 LTS 버전인 17 로 선정했다.
먼저 Java Runtime Data Area는 이전에 이미 작성했던 내용이지만 새롭게 정리하는 차원에서 다시 써보자. Oracle에서 작성한 공식 JVM 명세(Java SE 17 JVM Specification)를 기반으로 학습했다.
JVM의 런타임 데이터 영역은 JVM이 실행되는 동안 메모리를 어떻게 할당하고 관리하는지를 정의한다.
이 영역은 크게 JVM 프로세스와 생애주기를 함께하는 shared 영역과, 유저 스레드와 생애주기를 함께하는 per-thread (thread-private) 영역으로 나뉜다.
⸻
📌 Per-thread 영역
⸻
non-contiguous 메모리 → 말그대로 메모리를 연속적이지 않은 위치에 할당. 메모리 낭비를 줄일 수 있지만, 제어가 어려워 CPU 오버헤드가 증가하고 실행 속도가 느려질 수 있음.
⸻
⸻
🧠 Shared 영역
'거의'?
JVM 명세 상에는 'all class instances and arrays'가 저장되는 영역이라 표현하고 있어 현재(JDK 17)까지는 '모든'이 맞는 것 같지만, Java의 발전에 따라 JIT 컴파일의 탈출분석 기술로 스택 할당과 스칼라 치환 최적화 방식이 달라지면서 예외가 생길 수 있음.
→ 탈출 분석 (Escape Analysis) : 새로 만들 객체가 사용되는 범위를 분석하여 Java Heap에 할당 여부를 결정하는 기술. (나중에 자세히 다뤄보자)
그렇다면 통상적으로 알려진 Gen 영역들은?
현대 GC 구현체들의 일반적인 설계방식으로 HotSpot을 비롯한 대중적인 JVM 구현체들이 이러한 설계를 차용해서 일반론으로 굳어진 것.
⸻
클래스 메타데이터와 static 변수는 다르다
클래스 메타데이터가 클래스의 '정의'와 '형태'에 대한 정적인 데이터라면, static 변수는 인스턴스가 아닌 클래스 수준에서 관리되는 변수로 실제 데이터를 저장하는 공간이라는 점에서 구분된다.
⸻
JVM Spec에서는 'per-class or per-interface run-time representation of the constant_pool table in a class file' 이라 표현하고 있는데 이해하려면 Java .class 파일의 상수 풀(constant_pool)이 뭔지 알아야한다.
Java 클래스 파일은 바이너리 데이터이며 constant_pool은 클래스 파일 내에 저장된 테이블이다. constant_pool은 클래스 파일이 메모리에 올라오기 전에 디스크 상에 정적으로 존재하는 symbolic reference이며, 런타임 상수 풀은 JVM를 통해 클래스가 로드되면 constant_pool이 실제 레퍼런스로서 메모리에 로드된 것(runtime representation
)이다.
비유하자면 .class 파일의 constant pool은 요리책에 적힌 재료 리스트 이고, 런타임 상수 풀은 요리사가 실제로 재료를 꺼내서 준비한 상태 라고 볼 수 있는 것.
⸻
⚠️ Direct Memory
• JVM 런타임 메모리에 속하지 않는 네이티브 메모리 영역으로, JVM 메모리와 무관해보이지만 OOM의 원인이 될 수 있다.
• Java NIO는 malloc을 통해 Heap이 아닌 메모리에 직접 할당할 수 있는 네이티브 함수를 이용하는 DirectByteBuffer를 사용한다.
• Direct Memory는 GC가 직접 관리하는 대상이 아니기에 계속 할당되고 회수는 되지 않는 상태가 반복되어 OutOfMemoryError: Direct buffer memory
를 발생시킬 수 있다.
• 제한 설정: -XX:MaxDirectMemorySize=256m
• 모니터링: jcmd VM.native_memory summary
DirectByteBuffer
JDK 1.4에 도입된 Java NIO는 채널과 버퍼 기반의 I/O 메서드를 제공.
DirectByteBuffer는 NIO를 활용하여 Java 힙과 네이티브 힙이 데이터를 복사해 주고받는 과정을 생략(Zero-Copy)하고 커널 영역에서 직접 통신하도록 하여 I/O 처리 속도를 높이기 위해 사용. (Netty, Kafka, Spark 등에서 사용됨)
GC 관리 대상이 아니면 어떻게?
버퍼에서 할당한 네이티브 영역의 메모리는 GC의 관리 대상이 아니지만 DirectByteBuffer 객체 자체는 Java Heap 내에 위치한다. 따라서, Direct Memory는 DirectByteBuffer 객체 안에 cleaner라는 내부 객체를 통해 간접적으로 해제할 수 있다. 이에 따라 GC가 해당 DirectByteBuffer를 해제할 때 함께 해제되지만, GC는 Heap 메모리가 부족할 때 돌기 때문에 네이티브 메모리가 고갈될 때까지 동작하지 않을 수 있다.
운영 환경 Tip
크기 제한 →
-XX:MaxDirectMemorySize=256m
와 같이 파라미터를 통해 Direct Memory 제한 설정.
모니터링 →jcmd <pid> VM.native_memory summary
명령이나-XX:NativeMemoryTracking=summary
파라미터를 설정하여 모니터링 가능.
🔎 앞으로 더 다뤄볼 주제들
• 🔬 Escape Analysis와 스택 할당, 스칼라 치환 최적화
• 🔍 HotSpot JVM의 GC 설계 (G1GC, ZGC 등)와 Heap 분할 구조
• 📦 JVM 메모리 관련 튜닝 옵션 정리
• 🧵 스레드별 Stack 사이즈 설정과 최적화