JVM内存模型与关键参数
聊内存溢出,咱们得先搞清楚「内存到底分几块」。这就像修水管,你得知道哪一段堵了,才能对症下药。JVM的内存分区,其实就那几大块:堆、栈、方法区、直接内存。我把它们捋一遍,顺便说说我平时调参的坑。
堆(Heap)—— 大部分对象的“家”
堆是JVM里最大的一块内存,所有new出来的对象都搁这儿。年轻代、老年代,说白了就是堆里的不同区域。年轻代再拆成Eden和两个Survivor区,目的就是提高GC效率。
我在项目里见过一个典型场景:一个定时任务每次生成大量临时对象,但没及时释放,结果年轻代一直Full GC,CPU飙到100%。你想想,对象不断进入老年代,老年代一满,直接OutOfMemoryError。
栈(Stack)—— 每个线程的私房钱
栈存的是局部变量、方法调用帧。每个线程有自己独立的栈,大小可以用-Xss调。默认在Linux上是1MB,Windows上通常512KB。如果你的线程数特别多(比如几千个),栈空间别设太大,否则光栈就吃掉几个G内存。
我调过一个WebSocket服务,连接数一万多,每个线程都开默认1MB栈,结果还没跑业务内存就爆了。后来改成256KB,系统稳如狗。
java.lang.StackOverflowError,先检查递归深度,再考虑是不是栈帧太大。我之前有个正则表达式嵌套递归,直接让栈溢出,改成迭代就解决了。
方法区(Metaspace / 永久代)—— 类元数据的归宿
方法区存的是类信息、常量、静态变量、JIT编译后的代码。JDK8以后方法区改名叫“元空间”(Metaspace),从堆里挪到了本地内存。默认无上限,但最好用-XX:MaxMetaspaceSize限制一下,不然你动态生成类(比如CGLIB代理)能撑爆物理内存。
我记得有次服务上线后,跑了两天内存缓慢增长,最后OOM。一看日志,是某个框架不停生成新的ClassLoader,导致Metaspace无限膨胀。嗯,加了个-XX:MaxMetaspaceSize=256m就控制了。
直接内存(Direct Memory)—— 堆外的不速之客
直接内存是NIO用的,通过ByteBuffer.allocateDirect()分配。它不在堆里,但也会占用物理内存。很多人只调堆参数,忽略了直接内存。一旦分配过多,同样会OutOfMemoryError:Direct buffer memory。
我在一个大文件传输模块里见过:每读一个文件片,都new一个DirectBuffer,用完又不释放。结果直接内存碎片化,系统卡死。后来用-XX:MaxDirectMemorySize=512m限制,配合池化,问题解决。
一张图理清整体结构
关键参数速查表
| 参数 | 作用域 | 说明 | 常见值 |
|---|---|---|---|
-Xms |
堆 | 初始堆大小,启动时分配 | 512m, 2g |
-Xmx |
堆 | 最大堆大小,过大会触发OS swap | 2g, 4g |
-Xss |
栈 | 每个线程的栈容量 | 256k, 512k |
-XX:MetaspaceSize |
方法区 | 元空间初始大小,达到后触发Full GC | 128m, 256m |
-XX:MaxMetaspaceSize |
方法区 | 元空间上限,超限报OOM | 256m, 512m |
-XX:MaxDirectMemorySize |
直接内存 | 最大直接内存,默认等于-Xmx | 512m, 1g |
-XX:NewRatio |
堆内 | 老年代/年轻代比例 | 2, 3 |
我的个人习惯: 线上服务我会统一设置 -Xms=-Xmx,避免动态扩容引起停顿。同时单独配 -XX:MaxMetaspaceSize=256m 和 -XX:MaxDirectMemorySize=512m,让每一个区域都有明确天花板。如果发现某个区域一直接近上限,那就要怀疑代码泄露了。
一段通用的JVM启动参数示例
java -Xms2g -Xmx2g \
-Xss512k \
-XX:MetaspaceSize=128m \
-XX:MaxMetaspaceSize=256m \
-XX:MaxDirectMemorySize=512m \
-XX:+UseG1GC \
-XX:NewRatio=2 \
-jar myapp.jar
你看,-XX:+UseG1GC 是我现在最常用的GC,大堆低延迟。但这不是本章重点,后面几章会细讲GC调优。这里就记住一句话:参数不是越多越好,每个参数背后的内存区域你心里要有数。
-XX:MaxMetaspaceSize,结果一个代码生成器动态创建了几万个类,Metaspace飙到1.5G,直接导致NodeManager把进程杀了。从此以后,MaxMetaspaceSize 是我启动参数的标配。
小结一下:堆管着对象,栈管着线程,方法区管着类,直接内存管着NIO。调参的时候,别只盯着-Xmx,看看其他区域有没有被忽视。我见过很多OOM,其实是Metaspace或直接内存撑爆了,堆反而空得很。好,掌握这些分区,咱们后面才好深入排查内存泄漏。