1. JVM이 서버의 전체 메모리를 다 사용하지 않는 이유
서버에 4GB의 물리적 메모리가 있다고 해도 JVM이 이를 전부 사용하지 않는 이유는 운영 체제(OS)와 JVM의 메모리 관리 방식 때문이다.
이를 세부적으로 살펴보면 다음과 같다.
(1) OS가 사용할 메모리를 남겨둬야 한다
- JVM이 실행되는 서버는 단순히 JVM만 사용하는 것이 아니라 OS 자체가 동작하고 있다.
- 네트워크, 디스크 I/O, 캐시 관리 등을 위해 일정 메모리가 필요하다.
- 일반적인 리눅스 서버의 경우, 최소한 500MB~1GB 정도의 메모리를 OS가 자체적으로 사용한다.
- 만약 JVM이 4GB를 전부 차지하면 OS가 원활하게 동작하지 못하고, 전체적인 시스템 성능이 저하될 수 있다.
(2) JVM의 Heap 외에도 많은 메모리가 필요
JVM이 사용하는 메모리는 Heap 메모리뿐만 아니라 여러 추가적인 영역이 있다.
메모리 영역 설명
Heap Memory | -Xms, -Xmx 옵션으로 설정하는 객체 저장 공간. |
Stack Memory | 각 쓰레드마다 생성되는 메서드 호출 스택을 저장하는 공간. |
Metaspace | 클래스 메타데이터 저장 (-XX:MaxMetaspaceSize로 크기 조절 가능). |
Code Cache | JIT(Just-In-Time) 컴파일된 코드 저장 공간. |
Direct Memory | ByteBuffer.allocateDirect() 같은 네이티브 메모리를 사용하는 공간. (-XX:MaxDirectMemorySize로 설정 가능) |
즉, JVM이 2GB Heap만 설정되어 있어도 스택, 메타스페이스, Code Cache, Direct Memory 등을 포함하면 실제로 JVM이 차지하는 메모리는 3GB 이상이 될 수도 있다.
(3) 가비지 컬렉션(GC)과 메모리 최적화
- JVM은 모든 사용 가능한 힙 메모리를 항상 다 쓰려고 하지 않고, 객체가 더 필요할 때만 확장한다.
- 즉, -Xmx4g 로 설정하더라도 실제로 4GB까지 확장될 때까지는 사용하지 않는다.
- GC가 주기적으로 동작하면서 메모리를 회수하므로, 실제 사용량은 설정한 최대값보다 작을 수 있다.
2. JVM 메모리 구조와 실행 흐름
JVM이 실행될 때 메모리를 어떻게 사용하는지 실행 흐름을 따라가 보자.
(1) JVM 실행 흐름
1️⃣ JVM이 시작되면 OS에서 필요한 메모리를 할당받음
- -Xms 값만큼 Heap을 먼저 할당.
- Stack, Metaspace, Code Cache 등의 기본 영역도 함께 할당.
2️⃣ 클래스 로딩 (Class Loader & Metaspace)
- .class 파일을 로딩하고, 클래스의 메타데이터를 Metaspace에 저장.
- 메서드 영역(클래스, 정적 변수 등)도 필요할 경우 추가적으로 할당.
3️⃣ 애플리케이션 코드 실행 (Heap & Stack 사용)
- 객체를 생성할 때 Heap에서 메모리 할당.
- 메서드가 실행될 때마다 Stack에 호출 프레임을 push/pop.
4️⃣ JIT 컴파일 & Code Cache 활용
- Hotspot JIT 컴파일러가 자주 실행되는 코드들을 네이티브 코드로 변환하여 Code Cache에 저장.
- 이를 통해 실행 속도를 향상.
5️⃣ 가비지 컬렉션(GC) 실행
- 필요 없는 객체를 정리하여 힙 메모리를 확보.
- GC 방식에 따라 Minor GC, Major GC, Full GC가 실행됨.
6️⃣ JVM 종료 시 모든 메모리 해제
- JVM이 종료되면 OS가 메모리를 반환받음.
(2) JVM 메모리 관리 심화 개념
JVM은 성능 최적화를 위해 여러 영역을 따로 관리하며, 각각의 역할이 있다.
- Eden Space → 새롭게 생성된 객체가 먼저 저장되는 공간.
- Survivor Space (S0, S1) → 살아남은 객체가 이동되는 공간.
- Old (Tenured) Generation → 장기간 사용되는 객체가 저장되는 공간.
- Metaspace → 클래스 메타데이터와 네이티브 메모리 저장.
- Code Cache → JIT 컴파일된 네이티브 코드 저장.
특히 GC가 발생할 때 이 메모리 구조에 따라 객체가 이동하며 정리된다.
3. JVM 메모리 최적화 및 적절한 설정
✅ (1) 적절한 Heap 크기 설정 (-Xms, -Xmx)
- 전체 서버 메모리의 50~70% 정도를 JVM Heap으로 설정하는 것이 일반적.
- 예: 4GB 서버 → -Xmx2g ~ -Xmx3g 정도가 적절.
✅ (2) GC 방식에 따라 메모리 관리
- Java 8 → -XX:+UseG1GC (대부분의 애플리케이션에 적합)
- Java 11 이상 → -XX:+UseZGC or -XX:+UseShenandoahGC (낮은 레이턴시 필요 시)
✅ (3) Metaspace 크기 조정
-XX:MaxMetaspaceSize=512m
- 기본적으로 무제한이지만, 너무 커지면 OOM 발생할 수 있음.
✅ (4) Stack 크기 설정
-Xss1m
- 기본값은 1MB 정도지만, 많은 재귀 호출이 있다면 늘릴 필요가 있음.
✅ (5) Direct Memory 관리
-XX:MaxDirectMemorySize=512m
- Direct Buffer 사용량이 많다면 충분한 공간 할당 필요.
4. 결론: JVM 메모리 설정의 핵심 요약
💡 JVM이 4GB 전체 메모리를 사용하지 않는 이유
- OS 및 기타 프로세스 사용을 고려해야 함.
- JVM Heap 외에도 Metaspace, Stack, Direct Memory, Code Cache 등이 추가적인 메모리를 차지함.
- GC가 Heap을 효과적으로 관리하면서 필요 이상으로 커지지 않도록 조절됨.
💡 적절한 JVM 설정 방법
- Heap 크기는 서버 전체 메모리의 50~70%가 적당 (-Xmx2g ~ -Xmx3g).
- GC 방식에 따라 메모리 관리 (-XX:+UseG1GC 추천).
- Metaspace, Stack, DirectMemory 등의 크기도 조정 가능.
💡 서버 메모리별 최적 설정 예시
- 4GB 서버 → Heap 2GB (-Xmx2g)
- 8GB 서버 → Heap 4GB (-Xmx4g)
- 16GB 서버 → Heap 8GB (-Xmx8g)
💡 마무리하며…
JVM 메모리 설정은 단순한 -Xmx 값 조정이 아니라, OS 메모리 사용량, GC 로그 분석, 애플리케이션의 실제 메모리 패턴을 고려하여 최적의 설정을 찾아야 한다.
이 글이 JVM 메모리 최적화에 대한 이해를 돕는 데 도움이 되었기를 바랍니다.
'운영' 카테고리의 다른 글
[504의 교훈] APISIX 전환으로 겪은 장애, 그리고 우리가 준비하지 못한 것들 (0) | 2025.05.11 |
---|---|
[운영] 로그는 적을수록 좋다? ELK 로그 최적화 전략과 운영 팁 (0) | 2025.03.12 |
[운영] JVM 메모리 설정, 왜 중요할까? (0) | 2025.03.12 |
[운영] 비동기 처리와 Thread Pool을 활용한 API 성능 최적화 (0) | 2025.01.30 |
캐시(Cache) 전략 (0) | 2025.01.11 |