3. JVM 核心参数详解:堆内存、栈内存、元空间与直接内存
JVM 内存管理,说白了就是给 Java 应用画地皮、分地盘。你参数设得好,应用跑得稳;设得不好,OOM 就来找你喝茶了。我这些年处理过的线上事故,十有八九都跟内存参数有关。今天咱们就把堆、栈、元空间、直接内存这四个核心区域掰开揉碎讲清楚。
核心原则:JVM 内存参数不是越多越好,也不是越少越好。关键是要理解每个区域的作用,然后根据业务场景做取舍。
3.1 堆内存(Heap)—— 对象的家
堆是 JVM 管理的最大一块内存。你 new 出来的对象,基本都住在这里。堆又分三块:新生代、老年代、还有 JDK8 以后消失的永久代(被元空间取代了)。
3.1.1 堆大小设置
两个最基础的参数:
-Xms // 初始堆大小
-Xmx // 最大堆大小
我个人习惯把 -Xms 和 -Xmx 设成一样。为什么?因为扩容缩容是有代价的。JVM 在运行时动态调整堆大小,会触发 Full GC。你想想看,线上服务正在处理请求,突然来一次 Full GC,那延迟抖动谁受得了?
我的经验:对于 4C8G 的容器,我通常设 -Xms4g -Xmx4g。留 3-4G 给操作系统和元空间。别把宿主机内存吃满,否则 OOM Killer 会直接干掉你的进程。
3.1.2 新生代与老年代比例
新生代大小直接影响 Minor GC 的频率。参数有这些:
| 参数 | 作用 | 默认值 |
|---|---|---|
-Xmn |
设置新生代大小 | 堆的 1/3 |
-XX:NewRatio |
老年代/新生代比例 | 2(老年代是新生代 2 倍) |
-XX:SurvivorRatio |
Eden/Survivor 比例 | 8(Eden:From:To = 8:1:1) |
举个例子:堆 4G,-Xmn1g,那么新生代 1G,老年代 3G。新生代里 Eden 约 800M,两个 Survivor 各 100M。
我曾经遇到过一个批处理任务,对象生命周期极短,但量大。默认的新生代太小,导致 Minor GC 每秒触发几十次。后来我把 -Xmn 调到堆的 60%,GC 频率直接降了一个数量级。
注意:新生代不是越大越好。新生代太大,老年代就小了。如果老年代装不下长生命周期对象,就会触发 Full GC,那才是真正的性能杀手。
3.2 栈内存(Stack)—— 线程的私人空间
每个线程都有自己的栈。栈里存的是局部变量、方法调用、操作数栈这些东西。栈的大小决定了你能递归多深。
3.2.1 栈大小设置
-Xss // 每个线程的栈大小
默认值因平台而异:Linux x64 下通常是 1MB。对于大多数业务应用,1MB 是够用的。但如果你有深度递归,或者方法调用链特别长,就得调大。
我记得有一次,一个同事写了个递归解析 JSON 的代码,数据嵌套了 5000 层。默认栈大小直接 StackOverflowError。调成 -Xss2m 才跑通。但说实话,这种场景更应该优化代码逻辑,而不是无脑加栈。
避坑指南:线程数 * 栈大小 = 栈总内存。如果你有 500 个线程,每个栈 1MB,光栈就占 500MB。高并发场景下,栈大小设得太大,内存压力会很大。我一般建议 256KB-512KB 起步,不够再加。
3.3 元空间(Metaspace)—— 类的归宿
JDK8 以后,永久代被元空间取代。元空间不再使用堆内存,而是使用本地内存(Native Memory)。这意味着元空间的大小只受操作系统限制。
3.3.1 元空间参数
-XX:MetaspaceSize // 元空间初始大小(触发 GC 的阈值)
-XX:MaxMetaspaceSize // 元空间最大大小
很多人以为元空间不会 OOM,其实不然。如果 MaxMetaspaceSize 不设,理论上可以无限膨胀。但如果你有类加载泄漏(比如热部署场景),元空间会一直涨,直到把系统内存吃光。
我建议显式设置 MaxMetaspaceSize。对于大多数微服务,256M-512M 足够了。如果你用 Groovy、动态代理、CGLIB 这些动态生成类的技术,可以适当放大到 1G。
关键区别:永久代(JDK7 及以前)受 -XX:MaxPermSize 控制,属于堆内。元空间(JDK8+)属于堆外,只受本地内存限制。升级到 JDK8 后,记得把 -XX:PermSize 和 -XX:MaxPermSize 去掉,换成元空间参数。
3.4 直接内存(Direct Memory)—— 零拷贝的利器
直接内存是 NIO 和 Netty 的常用技术。它绕过了 JVM 堆,直接在操作系统层面分配内存。好处是减少了数据在堆内和堆外之间的拷贝,性能更高。
3.4.1 直接内存参数
-XX:MaxDirectMemorySize // 直接内存最大大小
默认值等于 -Xmx。也就是说,如果你不设,直接内存最大可以跟堆一样大。这其实挺危险的。因为直接内存不归 GC 管,它由 Cleaner 机制回收。如果分配速度超过回收速度,就会 OOM。
我遇到过一个问题:一个 Netty 应用,堆内存只用了 2G,但直接内存飙到了 4G,最后进程被操作系统杀掉了。排查发现是 MaxDirectMemorySize 没设,默认跟 -Xmx 一样是 4G,但堆外还有元空间和其他开销,加起来超了。
建议:显式设置 -XX:MaxDirectMemorySize,一般设为堆大小的 1/4 到 1/2。比如堆 4G,直接内存设 1G-2G。同时监控直接内存的使用量,别等出事了再查。
3.5 知识体系总览
下面这张图把四个内存区域的关系画清楚了。你可以看到堆内、堆外、线程私有、线程共享的划分。
3.6 实战参数组合推荐
说了这么多理论,给几个我常用的参数组合吧。你可以根据业务场景直接套用。
| 场景 | 推荐参数 | 说明 |
|---|---|---|
| 微服务(4C8G) | -Xms4g -Xmx4g -Xmn1.5g -Xss256k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1g |
堆 4G,新生代 1.5G,栈 256K,元空间 256M,直接内存 1G |
| 批处理(8C16G) | -Xms10g -Xmx10g -Xmn6g -Xss512k -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:MaxDirectMemorySize=2g |
堆 10G,新生代 6G(对象生命周期短),栈 512K |
| 高并发网关(4C8G) | -Xms4g -Xmx4g -Xmn2g -Xss256k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=2g |
直接内存放大,适合 Netty 场景 |
最后说一句:参数是死的,业务是活的。上线前一定要做压测,观察 GC 日志和内存使用曲线。我见过太多人照搬网上的参数,结果线上跑崩了。记住,没有银弹,只有适合你业务的参数。