资讯动态

你的 Java 进程究竟吃了多少内存?—— 从 RSS、VSZ 到堆内外的“隐形贪吃蛇”

发布时间:2026/10/20 4:06:47 来源:尧图企业网站定制
你运行一个-Xmx2g的 Java 应用top一看 RES 显示 3.2GB。你慌了JVM 堆明明只占 2GB剩下的 1.2GB 是谁吃的容器频频 OOM Kill可你无论如何优化堆内存都没用。答案不在 JVM 堆里而在进程地址空间的每一寸领土—— 堆外内存、线程栈、JIT 代码缓存、元空间……我是Evan一个在智荟知识库项目中被“内存泄漏”折磨过三天三夜的 JavaAI 学生。今天我从操作系统的进程内存模型出发带你解剖一个 Java 进程的真实内存构成弄懂 RSS、VSZ、页错误、容器 OOM以及怎么排查ByteBuffer和 JNI 堆外内存的“隐形黑洞”。 写在前面大二学操作系统老师讲“虚拟内存、驻留内存”时我只当八股背。直到我在知识汇教育平台部署 SpringCloud 微服务时一个只设了-Xmx512m的容器竟然被 K8s 以“内存超限”杀掉free、top、pmap这些命令我第一次用上才发现 JVM 堆只是冰山一角。这篇博客我带你站在操作系统视角完整还原一个 Java 进程到底吃了多少内存——以及怎么精准测量它。一、虚拟内存 VS 驻留内存你看着很大实际没那么大VSZVirtual Set Size进程申请的虚拟地址空间大小。包括所有映射的内存堆、栈、共享库、内存映射文件等。只是“地址范围”不占真实物理内存和磁盘交换空间。RSSResident Set Size当前真正驻留在物理内存中的部分。也就是你top命令看到的 RES 列反映实际内存占用。关键理解一个 Java 进程的 VSZ 可能高达几十 GB因为预留了大的虚拟地址空间但 RSS 只有几个 GB。容器 OOM 看的是 RSS加上部分页缓存等而不是 VSZ。# 查看 Java 进程的内存统计 pidjps | grep MyApp | awk {print $1} cat /proc/$pid/status | grep -E VmRSS|VmSize二、页错误Page Fault内存从虚拟到物理的“卡顿”当 CPU 访问一个虚拟地址但该地址没有对应的物理页框时会触发页错误。Java 中的典型场景JVM 启动时分配的大堆-Xms2g只是虚拟分配实际物理内存页只有在第一次写入时才通过缺页分配。这就是为什么-Xms设大了 RSS 不一定立刻涨。使用MappedByteBuffer映射大文件时访问某页会触发严重缺页从磁盘读。三、Java 进程的内存划分不止是堆一个典型的 Java 进程HotSpot VM在操作系统看来地址空间包含这些区域3.1 JVM 堆Heap-Xms初始大小-Xmx最大大小。存放对象实例GC 管理。堆内的内存会被记入 RSS。3.2 元空间Metaspace替代永久代存储类元数据、方法信息。默认无上限可能膨胀导致 OOM。可以用-XX:MaxMetaspaceSize限制。属于堆外内存但 JVM 会主动分配和回收。3.3 JIT 代码缓存Code CacheJIT 编译后的热点方法机器码存放位置。-XX:ReservedCodeCacheSize默认 240MB。满了会导致编译线程停止性能下降。3.4 线程栈Thread Stack每个线程占用独立栈空间-Xss默认 1MB。300 个线程 300MB RSS。多线程程序的内存大头经常在这里。在知识汇的秒杀场景中线程池过大曾导致 RSS 超限。3.5 堆外内存Direct MemoryByteBuffer.allocateDirect()分配。不受 GC 管理需要手动释放或依赖Cleaner。Netty、AI 推理库通过 JNI经常大量使用。3.6 JNI / 本地代码和共享库加载的.so/.dll文件映射到进程地址空间。如果用 JNI 调用 C 库比如深度学习推理那部分内存 JVM 完全无法控制。四、排查实战我的容器 OOM 血泪史在知识汇教育平台上线前我用 Docker Compose 跑了一个 SpringBoot Redis MinIO 服务。堆内存设了-Xmx512m但容器内存上限--memory1g依旧频繁 OOM Kill。排查步骤docker stats发现容器 RSS 在 900MB1.1GB 波动。top进容器看进程java进程 RES 约 850MB但-Xmx只有 512MB。pmap -x pid查看地址空间分布pmap -x 1 | grep -E total|anon发现多个anon段其中有一个256MB 的匿名映射很可疑。排查代码发现我们用了 Netty 做异步通信Netty 默认使用堆外内存PooledByteBufAllocator并且没有主动释放。另外RAG 服务加载的 Embedding 模型通过 JNI 分配了约 100MB 堆外内存。解决方案限制 Netty 堆外内存-Dio.netty.maxDirectMemory128M卸载 JNI 模型时主动调用释放函数。增大容器内存限制到 1.5GB并做好监控。五、工具箱Java 内存排查常用命令 总结核心结论一个 Java 进程的真实内存 堆 元空间 代码缓存 线程栈 堆外内存 JNI 共享库。容器 OOM 排查必须跳出-Xmx用操作系统的视角审视整个 RSS。思考题你有一个 Java 应用在物理机上用-Xmx2g运行正常RSS 稳定在 2.5GB 左右。你把它放进 Docker 容器设置--memory3g结果 OOM Kill。同样的负载、同样的 JVM 参数为什么容器内反而更容易超限提示考虑内核内存、页缓存、以及容器内存统计的working set与 RSS 的区别欢迎在评论区留下你的分析 —— 下一篇我会聊聊“容器内的 JVM 内存cgroups 和 /proc/meminfo 的谎言与真相”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑