Java微服务内存占用分析

本文深入探讨Java进程中各组件如何占用内存,包括Java堆、线程、类加载、符号表、垃圾回收器、代码缓存、编译器、DirectBuffer及NativeLibrary等。通过实例展示不同配置下SpringCloud微服务的内存消耗,提供微服务内存分配公式,帮助理解Java应用内存使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、Java 进程中有哪些组件会占用内存?

通过 Native Memory Tracking 可以观察到有以下 JVM 组件。在命令行加上-XX:NativeMemoryTracking=summary,会增加3MB左右内存,损失5%-10%CPU,不宜用于生产环境。

使用 jcmd <pid> VM.native_memory summary scale=MB 可以查看内存分配结果。如果报告"Native memory tracking is not enabled", 是因为该进程起动时命令行没有加上-XX:NativeMemoryTracking=summary参数。

1.1 Java 堆

最显而易见的就是 Java 堆,它是 Java 对象存在的地方。它会占用 -Xmx 参数指定大小的内存。

1.2 线程

线程堆栈也会申请内存。堆栈大小由 -Xss 选项指定,默认每个线程1M,幸运的是情况并非那么糟糕。操作系统会以延迟分配的方式分配内存页面,比如在第一次使用时分配,因此实际使用的内存要低得多,通常每个线程堆栈占用80至200KB。

还有其他 JVM 部件会占用本地内存,但它们在总内存消耗中通常比例不大。

1.3 类加载

类的元数据存储在 Metaspace 堆外区域中,包括方法字节码、符号、常量池、注解等。加载的类越多,使用的元数据就越多。可以通过 -XX:MaxMetaspaceSize(默认无上限)和 -XX:CompressedClassSpaceSize(默认1G)选项控制元数据总大小。

1.4 符号表

JVM 有两个主要的 hashtable:符号表包含名称、签名、标识符等,String 表包含对 interned String 引用。如果 Native Memory Tracking 显示 String 表使用了大量内存,这可能意味着应用程序调用 String.intern 过于频繁。

1.5 垃圾回收器

GC 需要额外的内存进行堆管理,主要用于 GC 自身的结构与算法。这些结构包括 Mark Bitmap、Mark Stack(遍历对象关系图)、Remembered Set(记录 region 之间引用)等等。其中一些可以直接调优,例如 -XX: MarkStackSizeMax 选项,另一些依赖于堆布局。其中 G1 region (-XX:G1HeapRegionSize)占用内存较大,Remembered Set 占用内存较小。

GC 的内存开销因算法而异,其中 -XX:+UseSerialGC 与 -XX:+UseShenandoahGC 的开销最小,而 G1 或 CMS 则会轻松占用大约10%的堆内存。

1.6 代码缓存
代码缓存包含动态生成的代码,JIT 编译生成的方法、解释器以及运行时 stub 代码。代码大小受 -XX:ReservedCodeCacheSize选项限制(默认为240M)。关闭 -XX:-TieredCompilation 可以减少已编译代码的数量,从而减小代码缓存。

1.7 编译器

JIT 编译器本身工作时也需要内存。可以通过关闭 Tiered Compilation 或者 -XX:CICompilerCount 减少编译使用的线程数。


1.8 Direct Buffer

应用程序可以通过 ByteBuffer.allocateDirect 调用直接请求非堆内存。默认的非堆内存大小限制由 -Xmx 选项指定,但也可以使用 -XX:MaxDirectMemorySize 覆盖配置。Direct ByteBuffer 包含在 Native Memory Tracking 输出的 Other 区域,在 JDK 11 之前包含在 Internal 区域。

通过 JMX 可以在 JConsole 或 Java Mission Control 中直接看到 Direct Memory 的使用量:

1.9 Native Library

System.Loadlibrary 加载的 JNI 代码可以不受 JVM 控制分配堆外内存,标准 Java Class Library 也是如此。尤其是未关闭的 Java 资源可能造成本地内存泄漏。典型的例子是 ZipInputStream 和 DirectoryStream。

JVMTI 代理,尤其是 jdwp 调试代理,也会造成内存消耗过多。

二 实例

2.1 一个很简单的Java程序内存占用: 15MB

Class: 5039 KB
Thread: 3042 KB
Code: 2557 KB
JavaHeap: 2048 KB
Symbol: 1742 KB
Internal: 230 KB
NativeMemoryTracking: 42 KB

ArenaChunk: 32 KB
GC: 19 KB
Total: 14751 KB [ Heap= 2048 , Thread= 3042 Class= 6781 , Other: 2880 ]

命令行:java -Xint -XX:+UseSerialGC -XX:NativeMemoryTracking=summary -Xmx1024k -Xss160K cn.PigEater

Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode) java version "1.8.0_181"

SprintBoot Jar包 1.7MB .

 

2.2 一个功能简单的Spring Cloud微服务(使用redis/mybatis)内存占用:162MB

JavaHeap: 64 MB
Class: 62 MB
Symbol: 17 MB
Thread: 11 MB
NativeMemoryTracking: 3 MB
Code: 3 MB
Internal: 2 MB
Total: 162 MB [ Heap= 64 , Thread= 11 Class+Symbol= 79 , Other: 8 ]

命令行:java -XX:+UseSerialGC -XX:NativeMemoryTracking=summary -Xms64M -Xmx64M -Xss256k -XX:MaxMetaspaceSize=128M -XX:MetaspaceSize=64M -Xint -Deureka.client.serviceUrl.defaultZone=http://10.32.0.21:8761/eureka/ -jar target/user-kernel-server-0.0.1-SNAPSHOT.jar

SpringBoot Jar包 59MB

 

运行完10000条请求后内存占用:

Class: 68 MB
JavaHeap: 64 MB
Symbol: 18 MB
Thread: 14 MB
NativeMemoryTracking: 3 MB
Internal: 3 MB
Code: 3 MB
Total: 173 MB [ Heap= 64 , Thread= 14 Class= 86 , Other: 9 ]

 

2.3 功能简单的Spring Cloud微服务,使用Serial垃圾回收,64MB Heap内存,执行10000个请求后:220MB

Class: 75 MB
JavaHeap: 64 MB
Code: 41 MB
Thread: 18 MB
Symbol: 18 MB
NativeMemoryTracking: 3 MB
Internal: 3 MB

Total: 222 MB [ Heap= 64 , Thread= 18 Class= 93 , Other: 47 ]

命令行:java -XX:+UseSerialGC -XX:NativeMemoryTracking=summary -Xms64M -Xmx64M -Xss256k -Deureka.client.serviceUrl.defaultZone=http://10.32.0.21:8761/eureka/ -jar target/user-kernel-server-0.0.1-SNAPSHOT.jar

2.4 功能简单的Spring Cloud微服务,使用Serial垃圾回收,256MB Heap内存,执行10000个请求后:430MB

JavaHeap: 256 MB
Class: 74 MB
Code: 42 MB
Internal: 19 MB
Thread: 18 MB
Symbol: 18 MB
NativeMemoryTracking: 3 MB
GC: 1 MB
Total: 431 MB [ Heap= 256 , Thread= 18 Class= 92 , Other: 65 ]

命令行:java -XX:+UseSerialGC -XX:NativeMemoryTracking=summary -Xms256M -Xmx256M -Xss256k -Deureka.client.serviceUrl.defaultZone=http://10.32.0.21:8761/eureka/ -jar target/user-kernel-server-0.0.1-SNAPSHOT.jar

 

2.5 功能简单的Spring Cloud微服务,使用G1垃圾回收,256MB Heap内存,执行10000个请求后:510MB

JavaHeap: 256 MB
Class: 74 MB
GC: 59 MB
Code: 42 MB
Thread: 38 MB
Internal: 20 MB
Symbol: 18 MB
NativeMemoryTracking: 3 MB
Unknown: 0 MB
Total: 510 MB [ Heap= 256 , Thread= 38 Class= 92 , Other: 124 ]

 

三、微服务内存分配大致公式,以256MB Heap内存,Serial GC计算:

heap + 最大thread_num * 256k + jar_package_size * 3 + GC(Serial 0, CMS/G1 heap * 10%)

如:64MB heap, 60线程, jar包60MB,占用内存大约为 64 + 60 * 256k + 60 * 2 = 199 MB 可以正常动行,一般不会超过260 MB

      512MB heap, 300线程,jar包120MB,占用内丰大约为 512 + 300 * 0.25MB + 120 * 2 = 830MB,一般不会超过950MB

 

使用策略

1.1

Heap

1.2

Thread

1.3

Class

1.4

Symbol

1.5

GC

1.6

Code

1.7

Internal

1.8

Direct Buffer

1.9

Native Library

总计

使用策略

1.1

Heap

1.2

Thread

1.3

Class

1.4

Symbol

1.5

GC

1.6

Code

1.7

Internal

1.8

Direct Buffer

1.9

Native Library

总计

JDK1.8 分配方法

-Xmx256M

默认为机器内存的1/4

最小2MB

-Xss256k

默认为1M

最小160k

参考Jar包大小

加上JVM系统中必须的Jar包。

同1.3。

-XX:+UseSerialGC

-XX:+UseG1GC

动态增长,默认与GC有关,SerialGC接近0。

-Xint

-Xmixed

动态增长,与Jar包大小相关。

动态增长,与Jar包大小相关。默认没有使用,大小为0。默认没有使用,大小为0。 
2.1 简单Java程序2MB3MB5MB1.7MB 2.5MB   15MB
2.2 微服务64MB11MB62MB17MB 3MB2MB  162MB
2.3 微服务64MB18MB75MB18MB 41MB3MB  222MB
2.4 微服务 256MB18MB74MB18MB 42MB19MB  431MB
2.5 微服务+G1256MB38MB74MB18MB59MB42MB20MB  510MB
2.6 微服务+G11024MB77MB80MB18MB81MB60MB53MB  1398MB

 

 

<think>嗯,我现在遇到了一个问题,就是Linux里运行的Java微服务占用内存RES很高,但是生成的内存快照文件却不大,分析起来没什么价值。这该怎么办呢?让我先理清楚情况。 首先,RES(Resident Set Size)高说明进程占用了很多物理内存,但生成的内存快照比如heap dump却不大,可能说明堆内存并不是问题所在。那问题可能出在堆外内存或者JVM的其他区域。这时候需要考虑几个可能性:堆外内存泄漏,比如使用NIO的DirectBuffer或者MappedByteBuffer,或者是JNI调用导致的内存泄漏,或者是JVM自身的内存区域比如Metaspace、线程栈等。 接下来,我应该如何确认是堆外内存的问题呢?可能需要用一些工具来检查。例如,使用pmap命令查看进程的内存映射,或者查看/proc/<pid>/smaps文件,分析内存区域的分布。另外,JVM的Native Memory Tracking(NMT)功能可以帮助跟踪JVM内部的内存使用情况,看看是否有异常。 另外,有没有可能是一些第三方库或框架在使用堆外内存?比如Netty这样的网络库会使用Direct Buffer,如果配置不当或者没有正确释放,可能导致内存泄漏。这时候需要检查代码和相关配置,确保资源正确释放。 如果堆内存快照不大,那生成快照的时候可能没有包含堆外内存的信息,所以需要其他手段。另外,检查JVM参数是否正确,比如MaxDirectMemorySize是否设置合理,或者是否有未限制的地方。 还有,系统级别的内存工具,比如top、htop、vmstat,可以观察内存的使用情况,看看是否有其他进程占用内存,或者系统的缓存是否过高。不过问题集中在特定的Java服务上,所以可能还是该服务自身的问题。 另外,JVM的GC日志分析是否有帮助?看看GC的频率和效果,如果堆内存正常,但GC正常,可能问题不在堆内。这时候可能需要更多的监控数据,比如使用jstat查看内存各区域的使用情况,或者开启NMT来跟踪。 总结下来,可能的步骤是: 1. 使用NMT分析JVM内部内存分配。 2. 检查pmap和/proc下的内存信息,找出大块的内存区域。 3. 检查是否有使用堆外内存的代码或库,并确保正确管理。 4. 使用系统工具监控整体内存使用,确认是否存在其他问题。 5. 考虑是否有文件映射、线程创建过多导致栈内存占用高等情况。 不过具体怎么做,可能需要一步步来验证。比如先开启NMT,然后重启服务,观察内存增长情况,看哪些部分在不断增加。或者对比不同时间点的pmap输出,找出变化较大的内存区域。同时,检查应用日志,看看是否有异常或频繁的资源申请释放操作。 可能还需要更新JDK版本,因为某些版本的JDK存在内存泄漏的bug。或者检查操作系统和依赖库的版本兼容性。 总之,这个问题需要系统地排查堆外内存的使用情况,结合JVM和操作系统的工具进行分析,找到具体的内存消耗点,然后针对性地优化或修复。</think>针对Java微服务RES内存高但堆内存快照无分析价值的问题,可按以下步骤排查和解决: --- ### **一、确认内存分配来源** 1. **区分堆内存与堆外内存** - **堆内存**:通过`jmap`或`jcmd`生成的Heap Dump分析(若Dump文件小,说明堆内存正常)。 - **堆外内存**:常见于以下场景: - Direct Buffer(NIO的`ByteBuffer.allocateDirect()`) - JNI调用(本地库分配的内存) - 内存映射文件(`MappedByteBuffer`) - 元空间(Metaspace)、线程栈、JIT编译代码等。 --- ### **二、使用工具定位堆外内存问题** 1. **Native Memory Tracking (NMT)** - **启用NMT**:启动JVM时添加参数: ```bash -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics ``` - **查看内存统计**: ```bash jcmd <pid> VM.native_memory detail ``` 重点关注`Internal`、`Other`等非堆区域。 2. **pmap分析内存映射** - 查看进程内存分布: ```bash pmap -x <pid> | sort -n -k3 ``` 查找占用大的匿名内存块(如`[anon]`)。 3. **检查/proc/<pid>/smaps** - 分析具体内存段细节: ```bash cat /proc/<pid>/smaps | grep -i rss | awk '{sum+=$2} END {print sum}' ``` 定位占用高的内存区域(如`mmap`文件映射或线程栈)。 --- ### **三、常见堆外内存泄漏场景** 1. **Direct Buffer泄漏** - **原因**:未正确释放`DirectByteBuffer`(依赖GC触发`Cleaner`)。 - **排查**: - 通过`jcmd <pid> VM.info`查看`DirectByteBuffer`使用量。 - 代码检查:确保`ByteBuffer.allocateDirect()`后调用`((DirectBuffer) buffer).cleaner().clean()`(需谨慎)。 2. **MappedByteBuffer泄漏** - **原因**:未关闭内存映射文件。 - **解决**:通过反射调用`sun.misc.Cleaner`释放(需兼容性处理)。 3. **JNI库泄漏** - **排查**:检查JNI调用的本地代码是否存在未释放内存的操作。 --- ### **四、系统级内存分析** 1. **监控系统内存** - 使用`top`、`htop`、`vmstat`观察整体内存和Swap使用情况。 - 通过`grep -i commit /proc/meminfo`检查系统提交内存总量。 2. **对比进程内存与JVM内存** - **公式**:`RES内存 ≈ 堆内存 + 元空间 + 线程栈 + Direct Buffer + 其他Native内存` - 若`RES`远大于堆内存,需重点排查堆外部分。 --- ### **五、优化与修复** 1. **限制堆外内存** - 设置JVM参数限制Direct Buffer大小: ```bash -XX:MaxDirectMemorySize=256m ``` 2. **调整元空间大小** - 防止Metaspace膨胀: ```bash -XX:MaxMetaspaceSize=256m ``` 3. **减少线程数** - 控制线程栈内存占用(默认1MB/线程): ```bash -Xss256k ``` 4. **升级依赖库** - 如Netty等库可能存在Direct Buffer泄漏的旧版本问题。 --- ### **六、示例操作流程** 1. **步骤1:开启NMT并重启服务** ```bash java -XX:NativeMemoryTracking=detail -jar your-service.jar ``` 2. **步骤2:定期抓取NMT数据** ```bash jcmd <pid> VM.native_memory baseline jcmd <pid> VM.native_memory detail.diff ``` 3. **步骤3:结合pmap定位匿名内存块** ```bash pmap -x <pid> | grep 'anon' | sort -n -k3 ``` --- ### **七、参考资料** - 工具: - Eclipse Memory Analyzer(MAT):分析Heap Dump。 - gdb:调试Native内存(需谨慎使用)。 - 文档: - [Oracle NMT指南](https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html) - [Linux pmap手册](https://man7.org/linux/man-pages/man1/pmap.1.html) 通过上述方法,可系统化定位RES高的根源,针对性解决堆外内存泄漏或配置问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值