JVM 가상 주소 공간(Virtual Address Space) 구조 설명
가상 주소 공간은 크게 JVM이 직접 관리하는 영역 과 OS 프로세스 레지스터 수준에서 관리되는 영역 으로 나뉩니다.
JVM 런타임 데이터 영역 (JVM Managed)
자바 프로그램이 실행되면서 동적으로 사용하는 메모리 공간입니다.
Stack (스택): 각 스레드마다 개별적으로 할당됩니다. 메서드 호출 시 생성되는 ‘스택 프레임’을 저장하며, 지역 변수, 매개변수, 리턴 값 등이 잠시 머물다 가는 공간입니다.JVM Heap (JVM 힙): JVM이 관리하는 가장 큰 메모리 영역입니다. new 키워드로 생성된 모든 객체(인스턴스)와 배열이 여기에 저장되며, 가비지 컬렉터(GC)의 주요 관리 대상입니다.Metaspace (메타스페이스): Java 8부터 도입된 영역으로, 과거의 PermGen을 대체합니다. 클래스의 메타데이터(클래스 구조, 메서드 정보 등)를 저장하며, Native Memory를 사용해 크기가 유연하게 조절됩니다.Code Cache (코드 캐시): JIT(Just-In-Time) 컴파일러가 컴파일한 네이티브 기계어 코드를 저장하는 공간입니다. 바이트 코드 중 자주 실행되는 코드를 최적화 해서 기계어로 블락으로 컴파일 하고 캐싱하여 실행 속도를 높입니다.
네이티브 및 프로세스 영역 (Native / OS Level)
자바 코드 외에 시스템 운영이나 외부 라이브러리 실행을 위한 공간입니다.
Native Library: 자바가 아닌 C/C++ 등으로 작성된 네이티브 라이브러리가 로드되는 공간입니다. 예를들어 (JNI 등을 통해 호출)Heap (네이티브 힙): JVM 힙과는 별개로, C 수준의 라이브러리나 JVM 자체의 구동을 위해 OS로부터 직접 할당받는 메모리 영역입니다.Data (+BSS): 프로그램의 전역 변수와 정적(static) 변수가 저장되는 영역입니다. 초기화된 변수는 Data에, 초기화되지 않은 변수는 BSS 영역에 위치합니다.Code (Text): 실행할 프로그램의 제어 명령(기계어 코드) 자체가 기입되는 영역입니다. CPU가 이 영역의 명령어를 하나씩 읽어 처리를 수행하며, 읽기 전용입니다.
JVM 힙(Heap) 메모리 동적 관리 과정
JVM은 시스템 자원을 효율적으로 사용하기 위해 처음부터 모든 메모리를 점유하지 않고, 상황에 따라 늘리거나 줄이는 유연한 전략을 사용합니다.
1단계: 메모리 예약 (Initial Reservation)
전체 가상 주소 공간(노란색) 중 JVM이 사용할 영역(파란색)을 찜해두는 단계입니다.
JVM이 시작될 때 -Xms(초기 힙 크기)만큼의 메모리를 OS로부터 실제로 할당(Commit)받고, -Xmx(최대 힙 크기)까지의 나머지 공간은 다른 프로세스가 쓰지 못하도록 ‘예약(Reserve)’만 해둡니다.
2단계: 객체 할당 (Object Allocation)
그림 설명: 비어있던 힙 영역에 Obj들이 하나씩 들어차기 시작합니다.
상세 내용: 자바 코드에서 new 키워드로 객체를 생성하면, JVM은 활성화된 힙 영역에 차곡차곡 데이터를 쌓습니다.
3단계: 힙 영역 확장 (Heap Expansion)
그림 설명: 할당된 파란색 영역이 꽉 차자, 예약해두었던 노란색 영역을 파란색(사용 영역)으로 바꿉니다.
상세 내용: 현재 할당된 메모리가 부족해지면 JVM은 OS에 추가 메모리를 요청합니다. 이때 미리 예약해둔 가상 주소 범위 내에서 실제 메모리 점유율을 높여갑니다.
4단계: 가비지 컬렉션 - 회수 (GC: Mark & Sweep)
그림 설명: 회수 화살표와 함께 사용하지 않는 Obj들이 사라지는 단계입니다.
상세 내용: Garbage Collector(GC)가 작동하는 시점입니다. 더 이상 참조되지 않는 ‘쓰레기 객체’들을 찾아내어 메모리에서 삭제합니다. 그림에서는 중간중간 비어있는 공간이 생기는 모습을 볼 수 있습니다.
5단계: 메모리 반납 (Memory Release / Uncommit)
그림 설명: GC 이후 사용량이 줄어들자, 오른쪽 끝의 파란색 영역이 다시 노란색으로 변하며 반납이라고 표시됩니다.
상세 내용: GC가 끝난 후에도 메모리에 여유가 많이 남는다면, JVM은 OS의 효율적인 자원 배분을 위해 사용 중이던 메모리를 다시 ‘예약 상태’로 돌려줍니다. (이를 통해 다른 프로세스가 해당 메모리를 쓸 수 있게 됩니다.)
GC 선택과 튜닝 중요성
자바는 메모리 관리를 JVM이 알아서 해주지만, 어떻게 비우느냐에 따라 애플리케이션의 성격이 완전히 달라집니다.
보통 중 소규모 시스템에서는 자바 5부터 도입된 에르고노믹스(Ergonomics) 기능 덕분에 JVM은 실행 환경 (OS, CPU 코어 수, 메모리 크기 등)을 감지하여 스스로 최적의 GC와 힙 설정을 선택합니다. 하지만 대규모 시스템에서는 에르고노믹스 도 만능은 아니기 때문에,
JVM이 설정한 자동값이 서비스의 예측 불가능한 트래픽 패턴과 맞지 않을 때, 혹은 너무 보수적으로 메모리를 잡을 때 성능 저하가 발생합니다.
에르고노믹스(Ergonomics) 란?
에르고노믹스(Ergonomics) 는 자바 5부터 도입된 기능으로 JVM 이 주어진 환경 속에서 GC 와 메모리 관련 설정을 스스로 최적화 하도록 합니다.
아래 두 가지 목표를 맞춰서 JVM 이 목표 중심 설정 통해 동적으로 조정 하도록 합니다.
Maximum Pause-Time goal (-XX:MaxGCPauseMillis=nnn)Throughput goal (-XX:GCTimeRatio=nnn)
이 설정된 두가지 값으로 인해 JVM 이 내부적으로 힙 영역의 크기를 늘리거나 줄이며 그 목표를 맞추려 노력 하도록 합니다.
참고로 위 두 가지 목표가 모두 충족되는 동안 Minimum Footprint 를 추구하며 동작 되도록 설계 되어 있습니다.
왜 튜닝이 필요한가?
앞써 이야기 한대로 에르고노믹스 는 만능이 아닙니다. JVM 이 설정한 자동값이 서비스의 예측 불가능한 트래픽 패턴과 맞지 않을 때, 혹은 너무 보수적으로 메모리를 잡을 때 성능 저하가 발생합니다.
JVM은 에르고노믹스 를 통해 다음 세 가지 목표를 달성하려고 노력합니다. 하지만 이들은 서로 트레이드오프 (Trade-off) 관계에 있기 때문에 서비스의 성격에 맞춰 우선순위를 정하는 것이 튜닝의 핵심입니다.
Maximum Pause-Time goal (-XX:MaxGCPauseMillis=nnn)
GC로 인해 애플리케이션이 멈추는 시간(Stop-the-world) 을 특정 밀리초(ms) 이하로 제한하는 것이 목표입니다. 작동 방식은 JVM 이 이 시간을 맞추기 위해 Young 영역 의 크기를 줄이거나 GC를 더 자주 수행합니다.
Throughput goal (-XX:GCTimeRatio=nnn)
전체 애플리케이션 실행 시간 대비 GC에 소비되는 시간의 비율을 최소화하는 목표입니다. 즉 “얼마나 빨리 끝내느냐” 보다는 “얼마나 많은 일을 처리하느냐”에 집중합니다. 처리량을 높이기 위해 힙 메모리를 크게 잡고 GC 횟수를 줄입니다. 한 번 GC가 돌 때 시간은 길어질 수 있지만, 전체적인 작업 효율은 올라갑니다.
Minimum Footprint
애플리케이션이 돌아가는 데 필요한 최소한의 물리적 메모리(Heap)만 사용하는 것을 목표로 합니다. 앞선 두 목표 (Pause-time, Throughput)가 달성되면, JVM은 힙 크기를 서서히 줄여서 운영체제에 메모리를 반환하려고 시도합니다.
Copyright 201- syh8088. 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.