Skip to content

JVM参数来源、JDK7/8配置与诊断命令

JVM参数不是一张“背下来就能调优”的清单。生产中真正要回答的是:参数从哪里注入、目标JDK是否支持、最终值是多少、影响哪个内存区域或运行机制、怎样观察效果、错误配置如何回退。

本页负责“启动参数和低风险诊断入口”;OOM、Heap Dump、线程栈、MAT和Arthas等深入故障分析继续放在JVM常用排查工具OOM生产级排查手册

学习目标

学完后应能:

  • 区分标准参数、-X参数、-XX参数、系统属性和应用参数。
  • 解释-XX:+Flag-XX:-Flag-XX:Flag=value语法。
  • 找出参数来自启动脚本、环境变量、容器Entrypoint、systemd还是命令行。
  • 区分参数“配置值”“JVM最终值”和“当前运行状态”。
  • 使用PrintCommandLineFlagsPrintFlagsFinaljcmd VM.flags核实参数。
  • 正确设置Heap、年轻代、线程栈、PermGen/Metaspace、Direct Memory和Code Cache相关参数。
  • 说明JDK 7/8与JDK 9+ GC日志配置差异。
  • 理解JDK 8容器感知与具体8u更新/发行版有关,不能机械使用宿主机资源。
  • 选择jpsjcmdjinfojstatjstackjmap并评估风险。
  • 理解Attach机制、用户权限、PID命名空间和极简JRE镜像为什么会让命令失败。
  • 为Java服务配置可回溯的GC日志、OOM Dump、错误日志和版本信息。
  • 从启动失败、GC频繁、容器OOMKilled等商业场景建立证据链。

一、一条Java启动命令怎样被解释

bash
java -Xms512m -Xmx512m -Dspring.profiles.active=prod \
  -jar order-service.jar \
  --server.port=8080

可分为:

mermaid
flowchart TD
    A["java启动器"] --> B["JVM选项:-Xms、-Xmx"]
    A --> C["系统属性:-Dname=value"]
    A --> D["入口:-jar或主类"]
    D --> E["应用参数:--server.port=8080"]

最容易犯的错误是把应用参数放在-jar前后弄混:

  • JVM选项和-D通常放在-jar或主类之前,由Java启动器/JVM处理。
  • JAR名或主类之后的内容传给应用main(String[] args)
  • Spring Boot的--server.port属于应用参数,不是JVM Heap参数。

下面通常不会设置JVM最大堆,而只是把字符串传给应用:

bash
java -jar app.jar -Xmx2g

除非应用自己解析-Xmx2g,JVM不会把它当启动选项。

二、参数语法分类

2.1 标准选项

规范性和跨实现稳定性相对更高,例如:

bash
-classpath
-cp
-Dname=value
-verbose:class
-version

2.2 -X扩展选项

由JVM实现提供,长期常用但不保证所有虚拟机一致:

bash
-Xms512m
-Xmx2g
-Xss1m
-Xmn512m
-Xloggc:logs/gc.log

2.3 -XX高级选项

布尔型:

bash
-XX:+UseG1GC
-XX:-UseBiasedLocking

+是开启,-是关闭。

数值/字符串型:

bash
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:HeapDumpPath=/data/dump

不同JDK版本可能新增、废弃、移除或改变默认值。启动时报Unrecognized VM option时,先核对目标java -version,不能直接删参数上线。

2.4 系统属性

bash
-Dfile.encoding=UTF-8
-Duser.timezone=Asia/Shanghai
-Dspring.profiles.active=prod

Java代码通过:

java
System.getProperty("spring.profiles.active");

读取。系统属性不是操作系统环境变量:

java
System.getenv("SPRING_PROFILES_ACTIVE");

读取的是另一套来源。

三、最终参数可能从哪里来

生产进程的最终命令可能经过多层拼接:

mermaid
flowchart TD
    A["基础镜像/主机JDK默认值"] --> B["JVM Ergonomics自动选择"]
    C["启动脚本JAVA_OPTS"] --> F["最终java命令"]
    D["环境变量/平台注入"] --> F
    E["systemd、Docker、K8s配置"] --> F
    B --> F
    F --> G["JVM解析并校验参数"]
    G --> H["运行时最终Flags"]

常见来源:

  • Shell/批处理脚本中的JAVA_OPTSJAVA_TOOL_OPTIONS等。
  • systemd的ExecStartEnvironment
  • Dockerfile的ENTRYPOINTCMD
  • Compose的commandentrypoint和environment。
  • Kubernetes的container command/args、ConfigMap、Secret和环境变量。
  • Jenkins或发布平台模板。
  • 应用服务器自己的JVM配置文件。
  • JVM基于机器资源和收集器执行的Ergonomics。

JAVA_OPTS只是很多脚本约定的普通变量,Java启动器不会自动读取任意名为JAVA_OPTS的变量;必须由脚本显式展开。JAVA_TOOL_OPTIONS等则可能由JVM/启动器机制读取,但支持、输出和安全边界需要按目标JDK验证。

3.1 容器中最常见的“我明明没配”

Docker inspect:

bash
docker inspect order-api --format 'Path={{.Path}} Args={{json .Args}} Env={{json .Config.Env}}'

Kubernetes:

bash
kubectl get pod order-api-xxx -o yaml
kubectl exec order-api-xxx -- sh -c 'tr "\0" " " < /proc/1/cmdline'

Linux宿主机:

bash
tr '\0' ' ' < /proc/<pid>/cmdline
systemctl cat order-api

注意命令行和环境变量可能包含密码、Token或密钥,保存和分享前必须脱敏。

四、配置值、最终值和运行状态不是一回事

text
配置值:启动脚本中写了-Xmx2g
最终值:JVM解析后MaxHeapSize是多少
运行状态:此刻Heap用了多少、GC发生几次

分别使用:

bash
# JVM启动时打印被选择/修改的重要命令行Flag
java -XX:+PrintCommandLineFlags -version

# 打印大量最终Flag
java -XX:+PrintFlagsFinal -version

# 运行进程最终Flag
jcmd <pid> VM.flags

# 当前Heap/GC状态
jstat -gcutil <pid> 1000 10

PrintFlagsFinal输出中的=:=等标记可帮助观察默认值与被修改值,但具体来源还要结合命令行、环境、Ergonomics和版本;不能只凭一个符号认定是谁修改。

五、Heap参数完整关系

5.1 Xms与Xmx

bash
-Xms2g
-Xmx2g
  • Xms是初始Java堆相关目标。
  • Xmx是最大Java堆上限。

“默认Xms固定为物理内存1/64、Xmx固定为1/4”只适合作为某些历史HotSpot环境的粗略记忆,不是跨JDK、容器和发行版保证。正确方式是查看最终Flags和实际Heap。

将Xms和Xmx设为相同值可减少运行期扩堆变化,常用于延迟敏感服务;但它会让进程更早提交/保留较大内存,增加高密度部署压力。是否相同要结合启动速度、容器limit和容量规划,不是绝对规范。

5.2 Xmn、NewSize、MaxNewSize与NewRatio

传统分代收集器中常见:

bash
-Xmn512m
-XX:NewSize=512m
-XX:MaxNewSize=512m
-XX:NewRatio=2
  • Xmn通常同时影响年轻代初始和最大大小。
  • NewRatio=2常表示老年代与年轻代约2:1,但实际布局受对齐、收集器和自适应策略影响。
  • 同时固定Xmn和配置NewRatio可能产生覆盖或失去自适应空间。
  • G1按暂停目标动态选择年轻Region数量,不应机械复制传统固定Xmn调法。

5.3 SurvivorRatio

bash
-XX:SurvivorRatio=8

传统年轻代布局中常表示:

text
Eden : From : To ≈ 8 : 1 : 1

它不是“Eden与两个Survivor总和为8:1”,也不保证运行时始终严格固定;Adaptive Size Policy和收集器策略可能调整。

5.4 MaxTenuringThreshold

bash
-XX:MaxTenuringThreshold=15

它是晋升年龄上限相关参数,不代表每个对象一定经历15次Young GC。Survivor不足、动态年龄阈值、大对象和收集器策略都可能提前晋升。

详见对象分配、TLAB、晋升与GC

六、JDK7 PermGen与JDK8 Metaspace

JDK7常见参数

bash
-XX:PermSize=128m
-XX:MaxPermSize=256m

JDK8常见参数

bash
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

关键区别:

  • JDK 7 HotSpot大量类元数据由永久代实现承载,受MaxPermSize约束。
  • JDK 8移除永久代,类元数据主要使用本地内存Metaspace。
  • MetaspaceSize更接近触发元空间GC/调整的初始阈值概念,不等同“预先固定分配这么大”。
  • 未配置MaxMetaspaceSize不等于没有风险,ClassLoader泄漏可继续消耗本地/容器内存。
  • JDK 8继续使用PermSize通常会收到参数被忽略或不支持提示,必须按实际版本处理。

动态代理、脚本、热部署和插件系统应同时监控已加载类数、ClassLoader数量和Metaspace,而不是只调大上限。

七、线程栈、直接内存和Code Cache

7.1 线程栈Xss

bash
-Xss1m

每个Java线程通常需要本地线程栈,线程数很大时总量近似受:

text
线程栈预算 ≈ 线程数 × 单线程栈大小

影响。Xss过大降低可创建线程数;过小会增加StackOverflowError风险。无限创建线程的问题不能只靠减小Xss解决,应限制线程池和阻塞并发。

7.2 Direct Memory

bash
-XX:MaxDirectMemorySize=512m

Direct Memory不计入Java Heap使用量,但计入进程RSS和容器总内存。NIO、Netty、压缩和本地库可能大量使用。未显式设置时的默认关联目标随JDK实现和版本而异,应查看Flags、框架指标和NMT。

7.3 Code Cache

JIT编译后的机器码存放在Code Cache。JDK 7/8参数名称和分段行为随版本演进,常见检查:

bash
jcmd <pid> Compiler.codecache
jcmd <pid> VM.flags

Code Cache耗尽可能导致编译被禁用、性能下降和警告。不能只看Heap判断JVM内存。

八、容器总内存怎样预算

text
容器内存limit
  > Java Heap
  + Metaspace/类元数据
  + Direct Memory
  + 线程栈
  + Code Cache
  + GC/JIT/JVM本地结构
  + JNI/Agent/共享库
  + mmap和可能的子进程
  + 安全余量

如果容器limit为1GiB、-Xmx900m、200个线程且-Xss1m,仅Heap和理论线程栈就可能超过1GiB,还没计算其他区域。最终可能是cgroup OOMKilled/Exit 137,而不是Java抛Java heap space

8.1 JDK8容器感知必须看更新版

早期JDK 8可能按宿主机CPU和内存估算Heap、GC线程和ForkJoin并行度;后续Oracle/OpenJDK更新或不同发行版回移了容器支持。不能只说“JDK8支持/不支持容器”,必须记录:

bash
java -version
java -XX:+PrintFlagsFinal -version

并在目标容器中验证最大Heap、availableProcessors()和实际Flags。若版本不可靠,优先升级受支持更新版,而不是堆叠来源不明的实验参数。

九、垃圾收集器参数必须成组理解

JDK 7/8常见:

bash
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC

不能同时随意启用多个互斥收集器。CMS、ParNew、Parallel、G1的支持组合和版本边界见HotSpot垃圾收集器完整原理

常见目标参数:

bash
-XX:MaxGCPauseMillis=200
-XX:GCTimeRatio=99
-XX:+UseAdaptiveSizePolicy
  • MaxGCPauseMillis是软目标,不是接口SLA保证。
  • 参数名是GCTimeRatio,不是GCTimeRadio
  • 自适应策略可能调整代大小和晋升策略;手工固定过多参数会限制它。

十、JDK7/8 GC日志完整配置

基础:

bash
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/data/logs/gc.log

按需增加:

bash
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCApplicationConcurrentTime

JDK 7/8不同更新版和收集器支持细节不同,启动前在目标JDK验证。

10.1 JDK7/8日志轮转

部分JDK 7/8 HotSpot版本支持:

bash
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=50M

历史轮转实现、文件命名和重启覆盖行为有版本差异。生产必须实际执行重启、轮转、日志采集和磁盘占满演练,不能只看到参数启动成功。

10.2 为什么时间戳要同时考虑

  • DateStamps便于与业务日志、监控按真实时间对齐。
  • TimeStamps/uptime便于判断JVM启动后多久发生事件,不受部分系统时钟调整影响。

故障分析需要统一时区,并保存发布、流量、容器事件和GC时间线。

十一、JDK9+统一日志不能复制给JDK8

bash
-Xlog:gc*,safepoint:file=/data/logs/gc-%t-%p.log:time,uptime,level,tags:filecount=10,filesize=50M

JDK 9+使用统一日志框架,选择器、装饰器和轮转语法与JDK 7/8不同。JDK 8执行-Xlog:gc*会失败;JDK 17/21也不应继续把所有历史PrintGC*参数当作默认模板。

迁移JDK时要做参数兼容清单:

  1. 哪些参数仍支持。
  2. 哪些被废弃或移除。
  3. 哪些默认值改变。
  4. 日志采集规则是否识别新文件名和格式。
  5. 告警解析是否仍能识别Pause、Full、OOM等事件。

十二、OOM和致命错误现场参数

bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
-XX:ErrorFile=/data/error/hs_err_pid%p.log

12.1 HeapDumpPath是目录还是文件

目标JDK对路径行为要实测。无论使用目录还是带PID/时间的文件方案,都必须确保:

  • 路径存在且运行用户可写。
  • 挂载为持久存储,容器重建后仍能取回。
  • 磁盘空间大于可能的Dump体积并留安全余量。
  • 多实例不会互相覆盖。
  • 文件包含敏感业务数据,访问、传输和销毁受控。

12.2 OnOutOfMemoryError与ExitOnOutOfMemoryError

不同JDK版本支持不同。执行外部脚本可能阻塞、重复触发或带来命令注入、权限风险;自动退出会影响可用性但可能避免假存活。必须按错误类型、编排自愈、Dump完成顺序和恢复策略设计,不能盲目复制。

12.3 容器OOMKilled不会保证生成Heap Dump

内核直接杀进程时,JVM可能没有机会抛OutOfMemoryError和执行Dump逻辑。因此还必须依靠容器事件、历史RSS、Heap/Non-Heap指标和上次日志。

十三、先学会确认PID

13.1 jps

bash
jps -lvm

它利用JVM进程信息发现本机Java进程,但可能受以下因素影响:

  • 目标JVM禁用或无法写PerfData。
  • 当前用户无权限。
  • 容器/PID命名空间不同。
  • 极简运行镜像没有JDK工具。
  • 多个Java进程显示的主类/JAR信息不完整。

所以jps没有输出不等于没有Java进程。Linux还可结合:

bash
ps -ef
pgrep -af java
cat /proc/<pid>/cmdline

Docker/Kubernetes先定位容器,再明确在宿主机还是容器PID命名空间执行。

十四、Attach机制为什么会失败

jcmdjstackjmapjinfo等很多操作需要附加到目标JVM。典型链路包含同用户权限校验、目标进程通信文件/Socket、信号和目标JVM Attach Listener。

mermaid
flowchart TD
    A["诊断工具拿到PID"] --> B["检查OS用户与权限"]
    B --> C["请求目标JVM启动/连接Attach Listener"]
    C --> D["通过本地通信通道发送命令"]
    D --> E["目标JVM在安全点/VM线程等执行诊断"]
    E --> F["返回结果"]

失败常见原因:

  • 使用不同操作系统用户。
  • /tmp、PerfData或通信文件被清理/权限异常。
  • 容器内外PID不同。
  • 目标JVM长时间无响应或正处于严重GC。
  • JRE/精简镜像没有工具。
  • 安全加固禁用Attach或缺少能力。
  • 工具JDK与目标JVM版本差异太大。

不要为方便把生产容器长期改成privileged。应预先设计诊断镜像、同版本工具、权限和审计流程。

十五、jcmd:优先的综合入口

发现能力:

bash
jcmd -l
jcmd <pid> help

低风险信息类命令:

bash
jcmd <pid> VM.version
jcmd <pid> VM.command_line
jcmd <pid> VM.flags
jcmd <pid> VM.system_properties
jcmd <pid> VM.uptime
jcmd <pid> GC.heap_info
jcmd <pid> VM.classloader_stats

高成本命令如Heap Dump、类直方图live统计、某些JFR/GC操作需要单独评估。jcmd help显示的命令取决于目标JDK,不要把JDK 21输出当作JDK 7/8必然能力。

十六、jinfo:看参数与有限动态修改

bash
jinfo -flags <pid>
jinfo -sysprops <pid>
jinfo -flag MaxHeapSize <pid>

部分可管理Flag可能支持动态修改:

bash
jinfo -flag +PrintGCDetails <pid>

但不是所有参数都能运行时修改。Heap上限、收集器等通常不能靠jinfo任意切换;动态修改也必须记录、评估影响并同步部署配置,否则重启后丢失。

十七、jstat:看趋势,不把采样列当绝对真相

bash
jstat -gcutil <pid> 1000 10
jstat -gc <pid> 1000 10
jstat -class <pid> 1000 10
jstat -compiler <pid> 1000 10

参数1000 10表示每1000毫秒采样一次,共10次。

-gcutil常见关注:

  • E、S0、S1、O等代使用百分比。
  • YGC/YGCT:Young GC次数和累计时间。
  • FGC/FGCT:Full GC次数和累计时间。
  • GCT:累计GC时间。

不同收集器、JDK版本的列含义和可用性不同。单次快照只能说明当时状态;连续采样才能看Eden增长、Old基线、GC后回落和频率。

十八、jstack与Thread.print

bash
jstack -l <pid> > thread-1.txt
jcmd <pid> Thread.print -l > thread-2.txt

排查CPU、锁、死循环、线程池阻塞时,建议间隔数秒抓取3份以上线程栈。单份栈里的WAITING可能是正常线程池等待。

风险通常低于Heap Dump,但线程非常多或JVM严重卡顿时仍有开销。输出可能包含SQL、URL、业务参数等敏感信息。

十九、jmap:能力强但风险更高

bash
jmap -heap <pid>
jmap -histo <pid>
jmap -histo:live <pid>
jmap -dump:format=b,file=heap.hprof <pid>

注意:

  • histo:live可能触发Full GC以统计存活对象,不能当无成本查询。
  • Heap Dump可能导致停顿、巨大磁盘写入和额外I/O压力。
  • 堆很大且磁盘不足时可能把宿主机也拖垮。
  • 某些JDK版本更推荐使用对应jcmd命令,但风险不会因命令名字改变而消失。

执行前检查副本、摘流、磁盘、暂停预算和数据安全。

二十、命令风险矩阵

操作一般风险使用前检查
VM.version/uptime/command_line权限、敏感参数
VM.flags/system_properties密码和Token脱敏
jstat连续采样采样频率不要过高
线程栈低到中线程数量、输出敏感信息
普通Histogram对象数量、目标版本实现
histo:live中到高可能Full GC
Heap DumpSTW、磁盘、I/O、敏感数据
强制GC长暂停和业务抖动
运行时修改Flag取决于参数是否可写、回滚和重启一致性

“命令能执行”不等于“线上现在应该执行”。

二十一、JDK7/8可运行参数观察Demo

java
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.List;

public class JvmFlagsDemo {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
        List<String> inputArguments = bean.getInputArguments();

        System.out.println("pid-name=" + bean.getName());
        System.out.println("vm=" + bean.getVmName());
        System.out.println("version=" + bean.getVmVersion());
        System.out.println("inputArguments=" + inputArguments);
        System.out.println("maxMemoryMiB="
                + runtime.maxMemory() / 1024L / 1024L);
        System.out.println("totalMemoryMiB="
                + runtime.totalMemory() / 1024L / 1024L);
        System.out.println("processors="
                + runtime.availableProcessors());
        System.out.println("profile="
                + System.getProperty("app.profile"));
    }
}

编译运行:

bash
javac JvmFlagsDemo.java
java -Xms64m -Xmx128m -Dapp.profile=demo JvmFlagsDemo app-arg

PowerShell调用某些本地程序时可能对包含点号、等号的-D参数产生意外解析。若出现Java把.profile=demo当作主类的现象,显式加引号:

powershell
java -Xms64m -Xmx128m "-Dapp.profile=demo" JvmFlagsDemo app-arg

生产启动脚本应在目标Shell中原样打印并验证最终参数数组,不要只在另一个Shell中测试字符串看起来正确。

应该观察:

  • inputArguments包含JVM参数和-D,通常不包含app-arg
  • maxMemoryMiB接近但不应武断要求打印恰好128;Heap对齐和实现会影响可见值。
  • totalMemoryMiB反映当前已提交Heap相关容量,不等于RSS。
  • processors用于观察JVM感知CPU,不等于容器一定获得无限CPU。
  • 系统属性app.profile=demo能被读取。

二十二、生产模板必须按版本拆分

下面是“验收项目”,不是复制即上线模板。

22.1 JDK8服务应至少明确

text
Heap预算:Xms/Xmx
收集器:Parallel/CMS/G1及目标原因
JDK8完整更新版和发行版
GC日志:PrintGCDetails、时间戳、Xloggc、轮转验证
OOM:HeapDumpOnOutOfMemoryError、Dump持久路径
Fatal Error:ErrorFile
容器:总内存预算、CPU识别、线程数

示意:

bash
java -Xms2g -Xmx2g \
  -XX:+UseG1GC \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+PrintGCTimeStamps \
  -Xloggc:/data/logs/gc.log \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/data/dump \
  -XX:ErrorFile=/data/error/hs_err_pid%p.log \
  -jar app.jar

必须在目标8u版本验证日志轮转、路径、容器资源和收集器,不代表所有服务都应该2GiB或G1。

22.2 JDK17/21服务示意

bash
java -Xms2g -Xmx2g \
  -XX:+UseG1GC \
  -Xlog:gc*,safepoint:file=/data/logs/gc-%t-%p.log:time,uptime,level,tags:filecount=10,filesize=50M \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/data/dump \
  -XX:ErrorFile=/data/error/hs_err_pid%p.log \
  -jar app.jar

迁移时不能只替换Java镜像Tag,还要重新验证启动参数、GC日志采集、TLS、字符集、反射访问、依赖和性能基线。

二十三、商业场景:容器反复OOMKilled

23.1 现象

容器limit为1GiB,启动参数-Xmx900m,日志没有Java heap space,容器Exit 137并重启。

23.2 证据链

mermaid
flowchart TD
    A["容器重启"] --> B["inspect/describe确认OOMKilled与137"]
    B --> C["读取实际Path、Args、Env"]
    C --> D["jcmd/监控确认Heap、线程、Direct、Metaspace"]
    D --> E["核算容器总内存预算"]
    E --> F{"Heap泄漏还是总预算不足"}
    F -- "Heap基线上升" --> G["Dump/MAT查持有链"]
    F -- "Heap正常但RSS超限" --> H["线程栈、Direct、Native、Agent"]

不能因为Exit 137就直接说Java Heap泄漏,也不能只把limit加倍。修复可能涉及降低Heap占比、限制线程/Direct Memory、修复泄漏、升级容器感知JDK或调整合理容量。

二十四、商业场景:改了参数却没生效

常见原因:

  1. 参数写在-jar之后,成为应用参数。
  2. Docker只restart旧容器,没有按新环境重建。
  3. systemd ExecStart覆盖Shell变量。
  4. Compose多文件合并后值被后文件覆盖。
  5. 发布平台追加了另一组参数,后值覆盖前值。
  6. 观察的是另一个实例/PID。
  7. 参数在目标JDK被忽略、废弃或启动失败后回滚到旧版本。

验证闭环:

bash
docker inspect <container>
jcmd <pid> VM.command_line
jcmd <pid> VM.flags

同时核对发布记录、镜像Digest和实例启动时间。

二十五、启动失败排查

text
Unrecognized VM option
→ 目标JDK不支持、拼写错误、参数已移除

Improperly specified VM option
→ 值、单位或语法错误

Could not reserve enough space for object heap
→ 地址空间/内存限制/Heap配置不满足

Could not create the Java Virtual Machine
→ 向前找第一条具体参数错误

标准流程:

  1. 保存完整启动命令和stderr。
  2. 执行同一Java二进制的-version
  3. 在隔离环境最小化参数,逐组恢复,不在生产盲删。
  4. 查版本迁移清单和PrintFlagsFinal
  5. 修复部署模板并补启动验收,避免只手工改当前机器。

二十六、常见误区

误区正确理解
Xms默认永远是物理内存1/64默认受JDK、Ergonomics和容器环境影响
Xmx等于容器limit最充分利用内存Heap外区域会让进程被OOM Kill
参数写在命令里就一定生效要看最终命令和运行时Flags
JAVA_OPTS会被Java自动读取它通常只是启动脚本约定,脚本必须展开
MaxGCPauseMillis是暂停保证只是GC软目标
JDK8都以同样方式识别容器必须看8u更新和发行版
jps没有进程就是Java没运行可能是权限、PerfData或PID命名空间问题
jmap histo:live是无风险查询可能触发Full GC
Heap Dump只占磁盘不影响服务可能STW并产生巨大I/O
动态jinfo改完就永久生效重启可能丢失,必须同步部署配置
JDK8 GC日志参数可以用于JDK21JDK9+使用统一日志体系
OOMKilled一定有Heap Dump内核强杀时JVM可能来不及Dump

二十七、面试标准回答

JVM参数最终值怎么确认

先从发布配置、启动脚本、systemd或容器inspect还原原始命令,再用jcmd pid VM.command_line看目标进程命令、jcmd pid VM.flags看最终Flags;离线可用PrintCommandLineFlagsPrintFlagsFinal。还要区分启动配置、JVM最终值和jstat当前状态,不能只看某个脚本文件。

Xmx为什么不能等于容器内存

容器limit约束整个进程cgroup,不只Java Heap。还包含Metaspace、Direct Memory、线程栈、Code Cache、GC/JIT结构、Agent和本地库。Xmx接近limit会没有峰值余量,可能在Heap未满时被cgroup OOM Kill,因此要建立总内存预算并监控Heap与RSS。

JDK7/8和JDK9+ GC日志区别

JDK7/8常用PrintGCDetails、Date/TimeStamps和-Xloggc,轮转使用历史UseGCLogFileRotation等参数并需按更新版验证;JDK9+使用统一日志-Xlog:gc*及filecount/filesize。迁移JDK时日志参数、文件名和采集解析都要重验。

jcmd、jstat、jstack、jmap分别做什么

jcmd是综合入口,可查版本、命令、Flags、Heap、类加载和线程;jstat适合低成本连续观察GC、类加载和编译趋势;jstack/Thread.print用于线程状态、锁和CPU栈;jmap可看直方图和Dump,但live统计与Heap Dump可能触发GC、停顿和大I/O,生产执行前必须评估风险。

为什么诊断工具attach不上

常见原因是OS用户不一致、PID命名空间不同、临时通信文件权限异常、目标JVM严重卡顿、精简镜像无工具、Attach被禁用或工具版本不匹配。应先确认目标真实PID、用户、JDK版本和执行位置,不应直接把容器改成privileged。

二十八、关联知识点

二十九、学习验收

  • [ ] 能把Java启动命令拆成JVM参数、系统属性、入口和应用参数。
  • [ ] 能从脚本、systemd、Docker/K8s和运行时Flags还原最终配置。
  • [ ] 能解释Xms/Xmx、Xmn/NewRatio、SurvivorRatio和晋升阈值边界。
  • [ ] 能区分JDK7 PermGen和JDK8 Metaspace参数。
  • [ ] 能计算Heap、Direct、线程栈、Metaspace和容器limit总预算。
  • [ ] 能分别写出JDK8和JDK17 GC日志方案并解释不能混用的原因。
  • [ ] 能说明Attach失败的权限、PID命名空间和工具版本原因。
  • [ ] 能根据风险选择jcmd、jstat、线程栈、Histogram或Heap Dump。
  • [ ] 能解释为什么配置修改后必须用VM.command_line和VM.flags验证。
  • [ ] 能把参数变更纳入压测、金丝雀、监控和回滚闭环。

本章小结

JVM参数管理的核心是“来源可追踪、版本可兼容、最终值可验证、运行效果可观察、变更可回滚”。命令工具的核心是“先选低风险证据,再按问题升级采集强度”。会背-Xmxjmap不等于会生产排障;能从启动平台一路证明到运行时Flags,并知道每条诊断命令的成本,才是完整能力。