Pro Tips — JVM 튜닝, GC 분석, Tomcat Native
이 챕터에서는 Tomcat 운영 전문가가 실제로 쓰는 JVM 힙 튜닝 기준, GC 로그 분석 방법, 그리고 성능 향상을 위한 Tomcat Native(APR) 설치까지 실무 노하우를 다룹니다.
JVM 힙 메모리 튜닝
기본 원칙
권장 힙 크기 = 가용 메모리 × 0.5 ~ 0.7
예시:
- 4GB RAM → Xms1g -Xmx2g
- 8GB RAM → Xms2g -Xmx4g
- 16GB RAM → Xms4g -Xmx8g
- 32GB RAM → Xms8g -Xmx16g
setenv.sh — 메모리 설정
# $CATALINA_HOME/bin/setenv.sh
# === 힙 메모리 ===
export CATALINA_OPTS="$CATALINA_OPTS -Xms2g -Xmx4g"
# === GC 알고리즘 선택 ===
# Java 17+: G1GC (기본값, 대부분의 경우 적합)
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"
# G1GC 튜닝
export CATALINA_OPTS="$CATALINA_OPTS -XX:MaxGCPauseMillis=200" # 목표 최대 Stop-the-World 시간
export CATALINA_OPTS="$CATALINA_OPTS -XX:G1HeapRegionSize=16m" # 힙 영역 크기 (1~32MB)
export CATALINA_OPTS="$CATALINA_OPTS -XX:InitiatingHeapOccupancyPercent=45" # Old GC 트리거 임계값
# === GC 로그 ===
export CATALINA_OPTS="$CATALINA_OPTS \
-Xlog:gc*:file=$CATALINA_HOME/logs/gc.log:time,uptime,pid:filecount=5,filesize=20m"
# === OOM 시 힙 덤프 자동 생성 ===
export CATALINA_OPTS="$CATALINA_OPTS \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=$CATALINA_HOME/logs/heapdump.hprof"
# === OOM 발생 시 자동 재시작 스크립트 ===
export CATALINA_OPTS="$CATALINA_OPTS \
-XX:OnOutOfMemoryError='systemctl restart tomcat'"
# === 서버 모드 ===
export CATALINA_OPTS="$CATALINA_OPTS -server"
# === 타임존 / 인코딩 ===
export JAVA_OPTS="$JAVA_OPTS -Duser.timezone=Asia/Seoul -Dfile.encoding=UTF-8"
GC 알고리즘 선택 가이드
| GC | Java 버전 | 특징 | 권장 상황 |
|---|---|---|---|
| G1GC | Java 9+ (기본) | 균형잡힌 처리량·지연시간 | 대부분의 서버 애플리케이션 |
| ZGC | Java 15+ (안정) | 초저지연 (< 1ms) | 응답 지연에 민감한 서비스 |
| Shenandoah | Java 12+ (OpenJDK) | 초저지연, 높은 처리량 | 대용량 힙 (> 8GB) |
| SerialGC | 모든 버전 | 단일 스레드 | 개발/테스트 환경 |
| ParallelGC | 모든 버전 | 처리량 최우선 | 배치 처리 |
ZGC 설정 예시 (응답 시간 민감 서비스)
# ZGC — 매우 낮은 Stop-the-World (< 1ms)
export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseZGC"
export CATALINA_OPTS="$CATALINA_OPTS -XX:SoftMaxHeapSize=6g" # 힙 상한 소프트 제한
export CATALINA_OPTS="$CATALINA_OPTS -Xmx8g"
GC 로그 분석
GC 로그 형식 이해
# Java 17+ GC 로그 예시 (G1GC)
[0.123s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 256M->100M(512M) 8.234ms
[0.456s][info][gc] GC(1) Pause Young (Concurrent Start) (G1 Humongous Allocation) 400M->200M(512M) 15.678ms
[1.234s][info][gc] GC(2) Pause Full (G1 Compaction Pause) 500M->300M(512M) 1250.123ms
분석 포인트
Pause Young (Normal) → 정상 Young GC, < 50ms 권장
Pause Young (Concurrent Start) → 동시 마킹 시작 (Old GC 징조)
Pause Full → Full GC! 원인 파악 필요 (힙 크기 부족 or 메모리 누수)
GC 로그 분석 도구
# 1. GCEasy.io (온라인) — GC 로그 파일 업로드
# https://gceasy.io
# 2. GCViewer (오프픈소스)
java -jar gcviewer-1.36.jar /opt/tomcat/latest/logs/gc.log
# 3. 직접 분석 — Full GC 횟수 확인
grep "Pause Full" /opt/tomcat/latest/logs/gc.log | wc -l
# 4. 평균 GC 시간
grep "Pause Young" /opt/tomcat/latest/logs/gc.log | \
awk -F'ms' '{print $(NF-1)}' | \
awk '{sum+=$1; cnt++} END {print "Avg:", sum/cnt "ms"}'
Tomcat Native (APR) — 네이티브 성능 향상
Tomcat Native 라이브러리는 OpenSSL과 APR(Apache Portable Runtime)을 사용하여 네이티브 레벨에서 SSL 처리와 파일 I/O를 수행합니다.
성능 개선 효과
| 항목 | 기본 Java NIO | Tomcat Native (APR) |
|---|---|---|
| SSL Handshake | Java JSSE | OpenSSL (빠름) |
| 정적 파일 서빙 | Java File API | sendfile 시스템 콜 |
| 연결 처리 | Java NIO | APR 네이티브 I/O |
| 권장 환경 | 일반 웹앱 | 고트래픽 파일 서빙 + SSL |
현대적 관점: Tomcat을 Nginx 뒤에서 백엔드로 사용한다면 SSL은 Nginx가 처리하므로 Tomcat Native의 실질적 이점은 제한적입니다. 직접 SSL을 처리하거나 대용량 파일 서빙이 필요한 경우에 유용합니다.
Ubuntu에서 Tomcat Native 설치
# 의존성 설치
sudo apt install -y libapr1-dev libssl-dev gcc make
# Tomcat Native 소스 다운로드
TOMCAT_VER=10.1.20
cd /tmp
wget https://dlcdn.apache.org/tomcat/tomcat-connectors/native/2.0.7/source/tomcat-native-2.0.7-src.tar.gz
tar -xzf tomcat-native-2.0.7-src.tar.gz
cd tomcat-native-2.0.7-src/native
# 컴파일 및 설치
./configure \
--with-apr=/usr/bin/apr-1-config \
--with-java-home=$JAVA_HOME \
--with-ssl=yes \
--prefix=/opt/tomcat/latest
make && sudo make install
# 설치 확인
ls /opt/tomcat/latest/lib/libtcnative*
setenv.sh에 네이티브 라이브러리 경로 추가
# setenv.sh
export LD_LIBRARY_PATH="/opt/tomcat/latest/lib:$LD_LIBRARY_PATH"
export CATALINA_OPTS="$CATALINA_OPTS \
-Djava.library.path=/opt/tomcat/latest/lib"
설치 확인
# catalina.out에서 APR 로드 메시지 확인
grep "apr" /opt/tomcat/latest/logs/catalina.out
# INFO [...] AprLifecycleListener.init Loaded Apache Tomcat Native library [2.0.7] ...
# INFO [...] AprLifecycleListener.init APR capabilities: IPv6 [true], sendfile [true], ...
커넥션 풀 튜닝 — 데이터베이스 연결
<!-- context.xml 또는 conf/Catalina/localhost/앱.xml -->
<Resource name="jdbc/myDB"
auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://db-host:3306/mydb?useSSL=false&serverTimezone=Asia/Seoul&rewriteBatchedStatements=true"
username="appuser"
password="AppP@ssw0rd"
<!-- 풀 크기 -->
initialSize="10"
maxTotal="100"
maxIdle="30"
minIdle="10"
<!-- 타임아웃 -->
maxWaitMillis="30000"
removeAbandoned="true"
removeAbandonedTimeout="60"
logAbandoned="true"
<!-- 연결 유효성 검사 -->
validationQuery="SELECT 1"
testOnBorrow="true"
testWhileIdle="true"
timeBetweenEvictionRunsMillis="30000"
minEvictableIdleTimeMillis="60000"/>
자주 발생하는 문제와 해결책
문제 1: OutOfMemoryError: Java heap space
# 증상
java.lang.OutOfMemoryError: Java heap space
# 원인 진단
# 1. 힙 덤프 분석 (Eclipse MAT)
jmap -dump:format=b,file=/tmp/heapdump.hprof <PID>
# 또는 -XX:+HeapDumpOnOutOfMemoryError 자동 생성된 파일 분석
# 2. 힙 사용량 실시간 확인
jstat -gcutil <PID> 1000 10
# S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
# 0.00 12.34 75.23 85.67 ...
# 해결
# setenv.sh에서 -Xmx 증가
export CATALINA_OPTS="$CATALINA_OPTS -Xms4g -Xmx8g"
문제 2: PermGen / Metaspace OOM
# Java 8+는 PermGen → Metaspace
# 증상
java.lang.OutOfMemoryError: Metaspace
# 해결: Metaspace 최대 크기 설정
export CATALINA_OPTS="$CATALINA_OPTS -XX:MaxMetaspaceSize=512m"
# 클래스 언로딩 활성화
export CATALINA_OPTS="$CATALINA_OPTS -XX:+ClassUnloadingWithConcurrentMark"
문제 3: 스레드 고갈 (Thread Starvation)
# 증상: HTTP 503, catalina.out에 다음 메시지
# WARNING: All available threads (200) are currently busy
# 스레드 덤프 생성
kill -3 <TOMCAT_PID>
# catalina.out에 전체 스레드 상태 출력
# 또는 jstack 사용
jstack <PID> > /tmp/thread-dump.txt
# 분석: BLOCKED 스레드가 많다면 Lock 경합 또는 DB 연결 풀 고갈
grep "BLOCKED" /tmp/thread-dump.txt | wc -l
# 해결: maxThreads 증가 또는 DB 풀 확대
문제 4: 앱 재배포 후 클래스 로더 메모리 누수
# 증상: 재배포 반복 시 Metaspace 점진적 증가
# 원인: 정적 필드에 클래스로더 참조, ThreadLocal 미정리
# 확인: 재배포 경고 메시지
grep "The web application \[" /opt/tomcat/latest/logs/catalina.out
# WARNING: The web application [...] appears to have started a thread named [xxx]
# but has failed to stop it. This is very likely to create a memory leak.
# 해결
# 1. ThreadLocal.remove() 호출 보장 (finally 블록)
# 2. 애플리케이션 종료 시 스레드 풀 shutdown
# 3. Listener를 이용한 리소스 정리
운영 체크리스트
#!/bin/bash
# Tomcat 헬스체크 스크립트
echo "=== Tomcat 상태 확인 ==="
# 1. 프로세스 확인
if ! pgrep -f "catalina" > /dev/null; then
echo "❌ Tomcat 프로세스 없음"
exit 1
fi
# 2. HTTP 응답 확인
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/)
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ HTTP 응답 이상: $HTTP_CODE"
exit 1
fi
# 3. 힙 사용량 확인
PID=$(pgrep -f catalina)
HEAP_USED=$(jstat -gcutil $PID | tail -1 | awk '{print $4}')
echo "✅ Old Gen 사용률: ${HEAP_USED}%"
if (( $(echo "$HEAP_USED > 85" | bc -l) )); then
echo "⚠️ 힙 사용률 경고: ${HEAP_USED}%"
fi
# 4. Full GC 횟수 확인 (지난 1시간)
FGC_COUNT=$(jstat -gcutil $PID | tail -1 | awk '{print $6}')
echo "✅ Full GC 횟수: $FGC_COUNT"
echo "✅ 모든 체크 통과"
Summary
| 항목 | 권장 설정 |
|---|---|
| 힙 크기 | -Xms = -Xmx = 가용 RAM × 0.5~0.7 |
| GC 알고리즘 | Java 17+: G1GC (기본), 저지연 필요 시 ZGC |
| GC 로그 | -Xlog:gc*:file=gc.log:filecount=5,filesize=20m |
| OOM 대응 | -XX:+HeapDumpOnOutOfMemoryError |
| Tomcat Native | 직접 SSL 처리 또는 고트래픽 파일 서빙 시 |
| DB 풀 크기 | maxTotal = maxThreads × 0.5~0.8 |