JVM 은 실행 환경을 보고 스스로 정한다. HotSpot 의 기본 동작은 다음과 같다.
| 항목 | 기본값 |
|---|---|
최대 힙 (-Xmx) |
물리 메모리의 1/4 |
초기 힙 (-Xms) |
물리 메모리의 1/64. 최소 8MB |
메모리가 작은 장비(대략 256MB 미만)에서는 비율이 달라지고, 클라이언트 클래스 머신으로 판정되면 상한이 더 낮게 잡힌다. 짐작하지 말고 실제 값을 확인한다.
java -XX:+PrintFlagsFinal -version | grep -E 'MaxHeapSize|InitialHeapSize|MaxRAMPercentage'
java -XshowSettings:vm -version
출력은 바이트 단위다. 2147483648 이면 2GiB 다.
JDK 10 이상(8u191 이상 백포트 포함)은 cgroup 의 메모리 한도를 인식한다. 즉 컨테이너에 준 메모리의 1/4 이 기본 최대 힙이 된다. 호스트 전체 메모리 기준이 아니다.
컨테이너에서는 절대값 대신 비율로 주는 편이 관리하기 쉽다.
java -XX:MaxRAMPercentage=75 -jar app.jar
-XX:+UseContainerSupport 는 기본으로 켜져 있다. 힙 밖에서 쓰는 메모리(메타스페이스, 스레드 스택, 다이렉트 버퍼, GC 구조체)가 상당하므로 컨테이너 한도를 힙과 같게 잡으면 OOMKilled 를 만난다. 75% 안팎을 출발점으로 삼고 실측해 조정한다.
애플리케이션마다 설정 위치가 다르다. 짐작하지 말고 실제 실행 명령을 본다.
ps -ef | grep [j]ava
tr '\0' ' ' < /proc/<PID>/cmdline; echo
| 대상 | 설정 위치 |
|---|---|
| systemd 서비스 | 유닛 파일의 Environment= 또는 드롭인 |
| Tomcat | bin/setenv.sh 의 CATALINA_OPTS |
| Hadoop 계열 | hadoop-env.sh · HADOOP_HEAPSIZE · 서비스별 _OPTS |
| Cloudera Manager | 서비스별 Java Heap Size 설정 항목 |
| 일반 jar | 실행 스크립트의 java 인자 |
JAVA_TOOL_OPTIONS 환경 변수는 거의 모든 JVM 에 전역으로 먹히므로 급할 때 쓸 수 있지만, 의도치 않은 프로세스에도 적용되므로 상시 설정으로는 권하지 않는다.
실행 중 프로세스의 힙 상태를 보는 가장 간단한 방법은 jcmd 다.
jcmd -l
jcmd <PID> GC.heap_info
jcmd <PID> VM.flags
jcmd <PID> VM.native_memory summary # NativeMemoryTracking 을 켜 둔 경우
시계열로 보려면 jstat 을 쓴다.
jstat -gc <PID> 1000 10
jstat -gcutil <PID> 1000
jstat -gcutil 의 O 열이 old 영역 사용률이다. Full GC 뒤에도 이 값이 내려오지 않고 계속 올라가면 누수를 의심한다.
jmap -heap 은 최근 JDK 에서 제거됐거나 동작하지 않는다. jcmd GC.heap_info 로 대체한다. 힙 덤프는 여전히 jmap 또는 jcmd 로 받는다.
jcmd <PID> GC.heap_dump /var/tmp/heap.hprof
덤프는 힙 크기만큼의 파일이 생기고 그동안 애플리케이션이 멈춘다. 디스크 여유를 먼저 확인한다.
장애가 난 뒤에 원인을 찾으려면 그때 남은 것이 있어야 한다.
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=10,filesize=50M
-Xlog:gc* 는 JDK 9 이상의 통합 로깅 문법이다. 8 에서는 -XX:+PrintGCDetails -Xloggc:... 를 쓴다.