2、JVM参数基础:JVM内存模型简介、堆内存参数(-Xms, -Xmx)、栈内存参数(-Xss)、元空间参数(-XX:MetaspaceSize)

聊JVM参数,绕不开内存模型。很多人一上来就调-Xmx,结果堆是调大了,GC却频繁得要命。我见过不少线上事故,都是因为对内存模型理解不透,参数拍脑袋乱设。

今天咱们就把这块彻底捋清楚。我会结合自己踩过的坑,把堆、栈、元空间这三个核心区域讲透。

2.1 JVM内存模型:一张图看懂

先看整体结构。JVM把内存分成了几个区域,每个区域各司其职。我习惯用这张图来跟团队新人讲解:

JVM运行时数据区 方法区(元空间) 类信息、常量、静态变量、JIT编译代码 -XX:MetaspaceSize / -XX:MaxMetaspaceSize 堆(Heap) 对象实例、数组 -Xms / -Xmx 新生代 Eden + Survivor 老年代 长期存活对象 Java虚拟机栈(Stack) 每个线程一个栈,栈中存栈帧 局部变量表、操作数栈、动态链接、方法出口 -Xss 本地方法栈 native方法调用 程序计数器 当前线程执行字节码行号 线程私有:程序计数器、虚拟机栈、本地方法栈 | 线程共享:堆、方法区

这张图我每次培训必画。说白了,JVM内存就两大块:线程私有的和线程共享的。

  • 线程私有:程序计数器、虚拟机栈、本地方法栈。每个线程各玩各的,互不干扰。
  • 线程共享:堆、方法区(元空间)。所有线程都能访问,也是GC的重点区域。

嗯,这里要注意:元空间在JDK8之后取代了永久代。它不在堆里了,用的是本地内存。这个改动影响很大,后面细说。

2.2 堆内存参数:-Xms 和 -Xmx

堆是JVM管理内存的主战场。几乎所有对象都在这里分配。调堆参数,是每个Java工程师的必修课。

-Xms:初始堆大小

JVM启动时分配的堆内存。我习惯设成跟-Xmx一样大。为什么?避免运行时动态扩容带来的性能损耗。你想想看,如果初始只有256M,高峰期突然要扩容到2G,这个扩容过程会触发Full GC,系统就卡住了。

我的经验:生产环境 -Xms 和 -Xmx 务必设成相同值。别给JVM「弹性」的机会,弹性往往意味着抖动。

-Xmx:最大堆大小

堆能扩展到的最大值。这个值受限于物理内存和操作系统位数。32位系统上限约4G,64位基本只受物理内存限制。

举个例子:

# 设置堆最小2G,最大2G
java -Xms2g -Xmx2g -jar myapp.jar

# 设置堆最小512M,最大4G(不推荐生产用)
java -Xms512m -Xmx4g -jar myapp.jar

避坑指南:我曾经接手过一个项目,-Xmx设了8G,但服务器只有8G物理内存。结果操作系统、其他进程、元空间、栈全挤在一起,系统直接OOM Killer把Java进程杀了。记住:-Xmx不能超过物理内存的70%,留点余量给系统和元空间。

堆内存调优原则

  • 别太大:堆太大,GC停顿时间会很长。我见过有人设32G堆,一次Full GC花了十几秒。
  • 别太小:堆太小,频繁GC,吞吐量上不去。
  • 观察后再调:用jstat、jmap看GC频率和堆使用率,别凭感觉设。

2.3 栈内存参数:-Xss

栈是每个线程私有的。它存的是方法调用的「栈帧」——局部变量、操作数栈、方法出口这些。

我遇到过一个问题:一个递归算法,数据量稍微大点就StackOverflowError。查了半天,发现是-Xss设得太小了。

-Xss:设置每个线程栈的大小

默认值因平台而异:Linux x64是1MB,Windows也是1MB,macOS也是1MB。但说实话,大部分场景用不了这么多。

# 设置每个线程栈为256KB
java -Xss256k -jar myapp.jar

# 设置每个线程栈为512KB
java -Xss512k -jar myapp.jar

我的建议:如果系统线程数很多(比如高并发Web服务),可以适当减小-Xss。因为每个线程都占一个栈,栈越小,能创建的线程越多。我曾经把一个网关服务的-Xss从1M降到256K,线程数从2000涨到了8000,吞吐量翻了一倍。

但别减得太狠。我见过有人设64K,结果正常业务逻辑都栈溢出了。一般来说,256K到512K是个比较稳妥的范围。如果代码里有深度递归,可能需要1M甚至更大。

2.4 元空间参数:-XX:MetaspaceSize

元空间存的是类的元数据——类结构、方法、字段、常量池这些。JDK8之前叫永久代,在堆里。JDK8之后改成了元空间,用本地内存。

这个改动的好处很明显:不再受堆大小限制。以前永久代经常OOM,特别是动态生成类的框架(CGLIB、JSP、反射)用得多的项目。

-XX:MetaspaceSize:元空间触发GC的阈值

注意,它不是初始大小,而是触发Full GC的阈值。当元空间使用量达到这个值,JVM就会触发Full GC来回收无用的类元数据。

# 设置元空间阈值为256M
java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar myapp.jar

关键点:默认的MetaspaceSize大约是20M左右(取决于平台)。对于大多数应用来说太小了。如果不调大,应用启动后很快就会触发一次Full GC,就为了扩容元空间。我见过很多新项目,一启动就卡住,查日志发现是元空间在频繁GC。

-XX:MaxMetaspaceSize:元空间最大大小

这个参数限制元空间能用到多大。如果不设,理论上元空间可以占满所有本地内存。但我不建议这么做——万一有类加载泄漏,内存会被慢慢吃光。

参数 作用 默认值 建议值
-XX:MetaspaceSize 触发Full GC的阈值 约20M 256M ~ 512M
-XX:MaxMetaspaceSize 元空间最大大小 无限制 512M ~ 1G

避坑指南:我曾经遇到一个Spring Boot应用,用了很多动态代理和CGLIB。没设MaxMetaspaceSize,结果元空间涨到了2G才被操作系统杀掉。从那以后,我每个项目都会显式设置这两个参数。记住:元空间泄漏比堆泄漏更难排查,因为jmap看不到元空间里的具体对象。

2.5 实战:一个完整的启动参数示例

说了这么多,给个实际能用的配置:

java -Xms4g -Xmx4g \
     -Xss512k \
     -XX:MetaspaceSize=256m \
     -XX:MaxMetaspaceSize=512m \
     -jar myapp.jar

这个配置适合大多数中型Web应用。堆4G,栈512K,元空间256M起步最大512M。嗯,够用了。

但记住:没有银弹。每个应用的内存特征都不一样。有的偏计算(栈需求大),有的偏缓存(堆需求大),有的偏动态类加载(元空间需求大)。最好的办法是:先用默认参数跑起来,然后用jstat、jmap、VisualVM这些工具观察,再针对性调整。

我个人习惯是:上线前先压测,观察内存曲线,确认没有泄漏和异常增长,再定最终参数。别偷懒,这一步省不了。

最后一个小技巧:如果你用的是容器(Docker/K8s),别忘了设置-XX:+UseContainerSupport。否则JVM看不到容器的内存限制,-Xmx设得再合理也没用。JDK8u191+已经支持了,记得用新版本。