티스토리 뷰
1. 올바른 자바/코틀린(Java/Kotlin) 애플리케이션 JVM 메모리 설정 가이드
[ 최소/최대 힙의 크기 ]
개발한 애플리케이션을 배포할 때면 여러 가지 설정 및 옵션들을 제공해주어야 하는데, 대표적인 값이 바로 메모리 할당에 대한 부분이다. 자바에서 모든 객체는 힙 영역에 할당되며, 핵심은 힙 메모리의 크기이다. 힙 메모리의 크기를 너무 작게 잡으면 객체를 힙 메모리에 할당할 공간이 부족하여 OutOfMemoryError로 인해 애플리케이션이 비정상적으로 종료되거나 잦은 GC 발생으로 인해 애플리케이션 오버헤드가 증가하여 성능에 문제를 줄 수 있다. 반대로 힙 메모리를 지나치게 크게 잡으면 애프리케이션이 시작할 때 힙 초기화 시간이 길어져 시작 속도가 저하되거나 GC를 수행하는 시간이 길어져 응답 지연 등의 문제가 생길 수 있다. 따라서 다음과 같은 옵션으로 최소 혹은 최대 힙의 크기를 설정할 수 있다.
- Xms <size in MB>m: 힙의 초기 크기 지정, 기본값은 “물리적 메모리의 1/64”
- Xmx <size in MB>m: 힙의 최대 크기 지정, 기본값은 “물리적 메모리의 1/4”
많은 곳들에서 흔히 -Xms와 -Xmx를 동일하게 설정하곤 하는데, 이는 안티 패턴으로 알려져 있다. JVM은 동적으로 힙 영역을 조절하는 데 뛰어난데, 두 값을 동일하게 설정하면 자바 프로세스가 정확히 해당 힙의 크기로 실행되며 실행 중에 크기를 조정하지 못하기 때문이다. 대부분의 경우에 이를 인위적으로 제한하는 것은 득보다 실이 많다고 한다. 따라서 특이 사항이 없으면 대부분의 경우에 Xmx를 설정하고 Xms를 설정하지 않는 것이 모범 사례라고 한다. (기본기가 탄탄한 자바 개발자 2판의 7장 참고)
[ 컨테이너의 메모리 크기 설정하기 ]
오늘날 대부분의 애플리케이션은 컨테이너 환경에서 배포된다. 따라서 힙의 크기와 함께 컨테이너 혹은 파드의 메모리 역시 중요하며, 최대 힙의 크기보다 컨테이너의 메모리를 높게 설정하는 것이 핵심 포인트이다.
JVM 애플리케이션은 메모리 영역을 크게 힙 영역(Heap Area)과 힙이 아닌 영역(Non-Heap Area)으로 구분한다. 힙이 아닌 영역으로 잡히는 메모리가 존재하기 때문에 컨테이너 또는 파드의 메모리 크기를 할당할 때 이러한 부분을 고려해야 한다.
- 스택(Stack): 각 스레드의 호출 스택에 사용되는 메모리
- 메서드 영역(Method Area): 클래스 메타데이터가 저장되는 메모리, 메타데이터 영역(metadata)이라고도 함
- 네이티브 메서드 스택(Native Method Stack): 네이티브 메서드(자바가 아닌 언어로 작성된 메서드)를 호출하기 위한 영역
- 기타 등등
힙이 아닌 영역의 대부분은 네이티브 메모리 영역에 해당하며, 특히나 자바 11부터는 G1 GC와 같은 알고리즘이 더욱 개선되어 네이티브 영역에 더 많이 의존하게 되었다. 이러한 네이티브 영역은 주로 다음과 같은 용도로 사용된다고 한다.
- 스레드 스택: 각 스레드마다 고정된 크기의 스택 메모리를 할당함
- Direct ByteBuffer: 네트워크 I/O나 파일 I/O를 위해 할당함
- GC 메타데이터: G1 GC의 경우 메타데이터를 네이티브 메모리에서 관리함
- JVM Code Cache: JIT 컴파일된 코드를 저장함
- JNI 사용 메모리: 네이티브 라이브러리가 JVM과 상호작용할 때 사용하는 메모리
따라서 일반적으로는 최대 힙 메모리(-Xmx)를 설정한 후, 해당 값의 약 30~50% 정도의 여유를 두어 파드 또는 컨테이너의 메모리를 설정할 필요가 있다. 만약 Netty와 같은 프레임워크를 사용하는 경우라면 Direct ByteBuffer를 더 많이 사용하므로 추가적인 여유가 필요할 수 있다. 총 시스템 메모리에서 JVM 힙 크기와 네이티브 영역 외에도 운영체제 및 기타 프로세스가 사용할 메모리도 존재하므로, 대게 여유롭게 50% 정도를 할당하는 것이 좋다. 예를 들어 -Xmx4G 설정 시에 컨테이너의 메모리는 최소 6GB 이상을 할당하는 것이다. 물론 이와 별개로 실제 환경에서의 측정 및 튜닝 역시 필요하다.
이러한 부분은 OOM이 발생한 상황에서도 도움이 될 수 있다. -XX:+HeapDumpOnOutOfMemoryError 옵션은 JVM으로 하여금 자바 힙 영역이 할당한 경우에 힙 덤프를 뜨게 해주는 옵션이다. 만약 OOM이 JVM이 아닌 파드나 컨테이너에서 발생했다면 당연하게도 힙 덤프가 남지 않는 부분 역시 인지할 필요가 있다.
[ 최소한의 메모리와 CPU 산정 ]
JVM은 매우 동적인 플랫폼이어서, 중요한 매개변수 중 일부는 JVM이 실행되는 환경을 바탕으로 JVM이 시작될 때 자동 결정된다. 대표적인 동적 특성들로는 다음과 같은 것들이 있다.
- JVM 내재적 특성, 특정한 CPU 기능을 활용할 수 있는 JIT 기술
- 내부 스레드 풀 크기 설정(예: 공통 풀)
- 가비지 컬렉션에 사용되는 스레드 수
따라서 잘못된 서버 혹은 컨테이너의 용량을 산정하면 가비지 컬렉션 또는 공통 스레드 풀과 관련된 문제가 발생할 수 있는데, 가장 중요한 부분은 GC에 관한 부분이다. 자바 17을 포함해서, 명시적으로 가비지 컬렉션 알고리즘이 지정되지 않은 경우에는 몇 가지 동적 검사를 수행해서 자동으로 가비지 컬렉션 알고리즘을 결정한다. 이때 JVM은 먼저 현재 실행되는 환경이 서버급 스펙인지 확인한다. 여기서 서버급 스펙이란 물리적 CPU가 2개 이상이면서 메모리가 2GB 이상인지 여부이다. 그리고 이를 바탕으로 다음과 같이 GC 알고리즘을 산정한다.
- 서버급 스펙인 경우 G1 GC 선택
- 서버급 스펙이 아닌 경우 Serial GC 선택
즉, CPU가 2개, 메모리가 2GB 미만인 것으로 보이는 환경에서 자바 애플리케이션을 실행하는 경우, 특정 GC 알고리즘을 명시적으로 선택하지 않는다면 Serial GC가 사용된다.
따라서 테스트 용도로 배포하는 것이라면 1개의 CPU와 메모리 1GB로 충분하며, 그 이하로 설정하면 애플리케이션 구동 자체에 메모리가 부족할 가능성이 매우 높다. 하지만 실제 서비스 운영이 필요하다면 적어도 2개의 CPU와 2GB의 메모리를 가질 필요가 있다.
[ 자바의 컨테이너 환경 고려하기 ]
이전 포스팅에서 살펴보았듯, 컨테이너 환경에 배포하는 상황이라면 컨테이너에서 JVM의 동작에 유의해야 한다.
JVM은 GC 쓰레드 수 및 기본 메모리 제한과 같은 런타임 기본값을 설정하기 위해 운영 체제를 쿼리(cgroup)한다. 하지만 자바는 기본적으로 컨테이너 환경을 제대로 이해하지 못하기 때문에, 컨테이너가 아닌 호스트 머신에 대한 정보를 제공하여 문제가 생겼던 것이다. 따라서 자바 10 이전의 버전을 사용중이면서 컨테이너 환경에 배포를 해야 한다면, -XX:+UseContainerSupport를 반드시 설정해주어야 한다. 참고로 해당 기능은 자바 8u191부터 자바8에 백포트되었다. 자세한 내용은 해당 포스팅을 참고하도록 하자.
이러한 부분에 의해 애플리케이션을 컨테이너에서 실행중이라면 항상 자바 11로 업그레이드하는 것을 권장한다.
참고로 -X:로 시작하는 옵션은 표준이 아니며, 핫스폿이나 Eclipse OpenJ9와 같은 JVM 구현체에서는 이식성이 없을 수 있다. 또한 -XX:로 시작하는 옵션 역시 확장된 옵션으로 일반적인 사용을 권장하지 않는다. 많은 성능 관련 스위치는 확장된 스위치이다. 예를 들어 다음과 같이 성능 최적화를 위해 사용될 수 있는 옵션들이 있는데, 이러한 부분은 엄밀한 검토와 테스트가 필요하다.
- -XX:CompileThreshold=20000: JIT 컴파일을 위해 메서드가 20000번 호출되도록 함
- Xmn <size in MB>m: 힙 내의 Young Generation의 크기를 지정함
- -XX: -DisableExplicitGC: System.gc() 호출이 어떠한 효과도 가지지 않도록 막음
- XX:MaxGCPauseMillis=50: G1 가비지 컬렉터에 대해 한 번의 컬렉션 동안 최대 50ms 이내로 일시 중지하도록 지시함
- -XX:GCPauseIntervalMillis=200: G1 가비지 컬렉터에게 컬렉션 간격을 최소한 200ms으로 하도록 지시함
[ 요약 ]
다음의 내용은 일반적으로 통용되는 자바/코틀린(Java/Kotlin) 애플리케이션 JVM 메모리 설정 가이드일 뿐이며, 최종적으로는 테스트를 통해 확인하여 최적화를 해야 한다.
- 스프링 부트 애플리케이션을 위한 컨테이너 또는 머신의 최소 메모리는 1GB임
- 자바 11 이전 버전을 사용중이라면 UseContainerSupport 활성화하기
- 최소 힙의 크기는 미지정, 최대 힙의 크기만 지정하기
- 파드 또는 컨테이너의 크기는 최대 힙 크기의 1.5배로 지정하기
참고 자료
- 기본기가 탄탄한 자바 개발자 (제2판)
- https://docs.oracle.com/javase/specs/