許多開發者習慣在 catalina.sh 中直接修改 JAVA_OPTS 加入堆記憶體參數,卻不知這會連帶影響 shutdown.sh、version.sh 等所有 Tomcat 衍生進程。本文深度剖析 CATALINA_OPTS 與 JAVA_OPTS 的作用域差異,重現經典 OOM 故障現場,並提供生產環境驗證過的標準化配置模板。
# catalina.sh 開頭直接加入
JAVA_OPTS="$JAVA_OPTS -server -Xms1G -Xmx1G -XX:+UseG1GC"
shutdown.sh 內部調用 catalina.sh stopcatalina.sh stop 執行 org.apache.catalina.startup.Bootstrap stopOutOfMemoryError: Native memory allocation failedkill -9 強制終止
| 變數 | 作用範圍 | 適用場景 | 繼承關係 |
|---|---|---|---|
JAVA_OPTS | **所有**啟動 Tomcat 的 Java 進程 | JVM 通用參數: - 記憶體模型選擇 ( -client/-server)- GC 日誌路徑 - JMX 監控埠 - 系統屬性 ( -D) | 被 CATALINA_OPTS 繼承 |
CATALINA_OPTS | **僅** catalina.sh run/start 啟動的主進程 | 應用層級參數: - 堆大小 ( -Xms/-Xmx)- GC 演算法 ( -XX:+UseG1GC)- 應用專用 -D 參數 | 疊加在 JAVA_OPTS 之上 |
# catalina.sh 內部邏輯簡化
if [ "$1" = "start" ] || [ "$1" = "run" ]; then
eval exec "\"$_RUNJAVA\"" "\"$JAVA_OPTS\"" "\"$CATALINA_OPTS\"" \
-classpath "\"$CLASSPATH\"" \
org.apache.catalina.startup.Bootstrap "$@" start
elif [ "$1" = "stop" ]; then
eval exec "\"$_RUNJAVA\"" "\"$JAVA_OPTS\"" \
-classpath "\"$CLASSPATH\"" \
org.apache.catalina.startup.Bootstrap "$@" stop
# 注意:stop 時**不包含 CATALINA_OPTS**
fi
bin/setenv.sh(推薦,不侵入原檔)#!/bin/bash
# ==========================================
# Tomcat 生產環境 JVM 標準配置
# 檔案位置: $CATALINA_BASE/bin/setenv.sh
# ==========================================
# --- JAVA_OPTS: 通用 JVM 參數 ---
export JAVA_OPTS="-server \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Asia/Taipei \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=$CATALINA_BASE/logs/heapdump.hprof \
-XX:ErrorFile=$CATALINA_BASE/logs/hs_err_pid%p.log \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9090 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=127.0.0.1"
# --- CATALINA_OPTS: 主進程專用參數 ---
export CATALINA_OPTS="-Xms2g -Xmx2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+ParallelRefProcEnabled \
-XX:+UnlockExperimentalVMOptions \
-XX:G1HeapRegionSize=16m \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:G1MixedGCLiveThresholdPercent=85 \
-XX:+DisableExplicitGC \
-Xlog:gc*:$CATALINA_BASE/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100m"
# --- 記憶體計算建議 ---
# 容器環境: -XX:MaxRAMPercentage=75.0
# 實體機: 保留 2-4G 給 OS/PageCache/Metaspace
| 參數 | 建議值 | 說明 |
|---|---|---|
-Xms/-Xmx | 物理記憶體 50-70% | 設定相等避免動態擴縮抖動 |
-XX:+UseG1GC | 啟用 | JDK 9+ 預設,大堆低延遲首選 |
-XX:MaxGCPauseMillis | 100-200ms | 目標停頓時間,G1 會自動調整 |
-XX:G1HeapRegionSize | 16-32MB | 堆區塊大小,2G 堆建議 16MB |
-Xlog:gc* | 統一日誌格式 | JDK 9+ 統一 GC 日誌,支援輪轉 |
# Dockerfile 最佳實踐
FROM tomcat:9-jre11
# 1. 複製配置
COPY setenv.sh /usr/local/tomcat/bin/
# 2. 容器記憶體感知 (JDK 10+)
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"
# 3. 啟動命令
CMD ["catalina.sh", "run"]
⚠️ 避坑:Docker
--memory=2g限制容器記憶體,但 JVM 預設看宿主機記憶體。必須加上-XX:MaxRAMPercentage或顯式-Xmx,否則 JVM 會嘗試分配宿主機級別的堆導致 OOM Kill。
# 1. 檢查啟動參數
ps -ef | grep tomcat | grep -v grep
# 應看到 -Xms2g -Xmx2g -XX:+UseG1GC 在 catalina 進程中
# 2. 確認關機進程無堆參數
./shutdown.sh
ps -ef | grep Bootstrap
# stop 進程僅含 JAVA_OPTS,無 -Xmx
# 3. GC 日誌輪轉驗證
ls -lh $CATALINA_BASE/logs/gc.log*
# 4. JMX 連線測試
jconsole localhost:9090
| 誤區 | 後果 | 正確做法 |
|---|---|---|
| 在 JAVA_OPTS 設 -Xmx | 關機/工具進程 OOM | 移至 CATALINA_OPTS |
| 未設 HeapDumpOnOOM | OOM 現場無法分析 | 加入 JAVA_OPTS |
| 容器未設 MaxRAMPercentage | 容器被 OOM Kill | 設 70-80% |
| 生產用 ParallelGC | 大堆停頓不可控 | 改 G1GC/ZGC |
| 直接改 catalina.sh | 升級覆蓋、難維護 | 用 setenv.sh |
JVM 參數調優不是玄學,而是「作用域隔離 + 容量規劃 + 可觀測性」三位一體。把 CATALINA_OPTS 留給主進程,把 JAVA_OPTS 留給平台能力,把 GC 日誌留給事後複盤。這樣配置的 Tomcat,重啟不再驚心動魄,OOM 也不再無從下手。