【转载】JVM 实际内存占用超过 Xmx 的原因, 设置 Xmx 的技巧
JVM实际内存占用超过Xmx的原因,设置Xmx的技巧
前言不知道大家在开发过程中有没有遇到过类似的问题,明明通过 JVM 参数-Xmx4g 设置了最大堆内存大小为 4g,但是程序运行一段时间后发现占用的内存明显超过了 8g,却并没有出现内存溢出等问题,那是什么东西占用了额外的内存空间呢?
一、背景 1.通过 free -g 查看服务器内存使用情况
2.通过 ps 查看 java 进程项目启动命令为:
3.通过 top 命令查看资源使用情况
VIRT:virtual memory usage 虚拟内存 1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等 2、假如进程申请 100m 的内存,但实际只使用了 10m,那么它会增长 100m,而不是实际的使用量
RES:resident memory usage 常驻内存 1、进程当前使用的内存大小,但不包括 swap out2、包含其他进程的共享 3、如果申请 100m 的内存,实际使用 10m,它只增长 10m,与 VIRT 相反 4、关于库占用内存的情况,它只统计加载的库文件所占内存大小
SHR:shared memory 共享内存 1、除了自身进程的共享内存,也包括其他进程的共享内存 2、虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小 3、计算某个进程所占的物理内存大小公式:RES – SHR4、swap out 后,它将会降下来
问题:java 项目启动通过 Xmx6g 设置了最大堆内存为 6g,但是项目运行一段时间后发现,项目进行占用的内存飙升到了 12g。难道设置的 JVM 参数没有生效?内存是被什么东西吃掉了?
二、通过 jmap 查看 JVM 内存分配 jmap -heap 打印 heap 的概要信息,GC 使用的算法,heap(堆)的配置及 JVM 堆内存的使用情况.命令:
发现 JVM 采用的 G1 垃圾回收器,堆内存的大小为 6g。说明项目启动时设置的 JVM 参数是生效的。
三、通过 NMT 分析 java 进程的内存分配 1.什么是 NMTNMT 的全称是 Native Memory Tracker ,是一个本地内存跟踪工具。常用来分析 JVM 的内存使用情况。
2.如何开启 NMTNMT 功能默认关闭,可以通过以下方式开启:
配置项 说明 off 默认配置 summary 只收集汇总信息 detail 收集每次调用的信息注意,根据 Java 官方文档,开启 NMT 会有 5%-10%的性能损耗;
如果想 JVM 退出时打印退出时的内存使用情况,可以通过如下配置项:
采用如下命令重启项目:
3.通过 jcmd 命令分析 java 进程的内存首先通过 jps 找到对应的 Java 程序的 pid,然后使用如下命令:
也可以通过一下命令指定内存单位,并将结果输出到文本中
可以看到 java 进程的整个 memory 主要包含了 Java Heap、Class、Thread、Code、GC、Internal、Symbol、Native Memory Tracking、unknown 这几部分;其中 reserved 表示应用可用的内存大小,committed 表示应用正在使用的内存大小。
分析:可以看到,除了 Java Heap 占用了 8g 内存,Class 和 Internal 也各占用了 1g 左右的内存。
JVM 的内存先放一张 JVM 的内存划分图,总体上可以分为堆和非堆(粗略划分,基于 java8)
那么一个 Java 进程最大占用的物理内存为:
堆和非堆内存堆和非堆内存有以下几个概念:init 表示 JVM 在启动时从操作系统申请内存管理的初始内存大小(以字节为单位)。JVM 可能从操作系统请求额外的内存,也可以随着时间的推移向操作系统释放内存(经实际测试,这个内存并没有过主动释放)。这个 init 的值可能不会定义。
used 表示当前使用的内存量(以字节为单位)
committed 表示保证可供 Jvm 使用的内存大小(以字节为单位)。 已提交内存的大小可能随时间而变化(增加或减少)。 JVM 也可能向系统释放内存,导致已提交的内存可能小于 init,但是 committed 永远会大于等于 used。
max 表示可用于内存管理的最大内存(以字节为单位)。
NMT 的输出说明:
nmt 返回结果中有 reserved 和 committed 两个值,这里解释一下:reservedreserved memory 是指 JVM 通过 mmaped PROT_NONE 申请的虚拟地址空间,在页表中已经存在了记录(entries),保证了其他进程不会被占用。
在堆内存下,就是 xmx 值,jvm 申请的最大保留内存。
committedcommitted memory 是 JVM 向操做系统实际分配的内存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,相当于程序实际申请的可用内存。在堆内存下,就是 xms 值,最小堆内存,heap committed memory。
注意,committed 申请的内存并不是说直接占用了物理内存,由于操作系统的内存管理是惰性的,对于已申请的内存虽然会分配地址空间,但并不会直接占用物理内存,真正使用的时候才会映射到实际的物理内存。所以 committed > res 也是很可能的.
四、解决 Internal 内存占用过大问题通过 NMT 的内存分析,我们已经定位到内存占用持续增长的原因,那么如果解决 nternal 内部内存持续增长,又不触发 full gc 的问题呢?看看我们之前的启动的 jvm 参数:
说明:环境是 jdk1.8,堆内存分配 8g,采用 G1 垃圾回收器。
通过 jmap 查看内存分配:
发现最大堆内存 MaxHeapSize 和我们的启动参数设置的一致。但是发现最大元数据空间内存非常大,这显然不是一个合适的值。
修改 jvm 参数:
注意:-XX:PermSize=256m -XX:MaxPermSize=512m 这两个参数对于 1.8 就是过期的参数。jdk1.8 的元空间大小要通过参数-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m 来控制。
修改后重启应用通过 jmap 查看内存分配:
JDK8 里的元空间实际上使用的也是堆外内存,默认没有设置元空间大小的情况下,元空间最大堆外内存大小和 Xmx 是一致的。
这里需要额外注意的是:永久代(JDK8 的原生去,换成元数据空间)存放 JVM 运行时使用的类,永久代的对象在 full GC 时进行垃圾收集。
下面的图描述了从 1.7 到 1.8,永久代的变更:
总结本文主要介绍如何结合 top,jmap,NMT 工具对 java 进程的内存进行分析。1、JDK1.8 开始,自带的 hostspot 虚拟机取消了过去的永久区,而新增了 metaspace 区,从功能上看,metaspace 可以认为和永久区类似,其最主要的功用也是存放类元数据,但实际的机制则有较大的不同。2、对于 JVM 里面的内存需要在启动时进行限制,包括我们熟悉的堆内存,也要包括直接内存和元生区,这是保证线上服务正常运行最后的兜底。3、重新熟悉 java 进程的内存组成。java 进程的内存组成 = heap + stack + metaspaceSize + directMemory 除了通过-Xmx4g -Xms4g 参数控制程序启动的堆内存外,不要忽视-Xss1024K 控制每个 stack 的大小。元空间限制:-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m 直接内存使用限制:-XX:MaxDirectMemorySize=128m
设置 Xmx 参数大小的技巧:
根据文章得出:Java 整个堆大小设置,Xmx 和 Xms 设置为老年代存活对象的 3-4 倍,即 FullGC 之后的老年代内存占用的 3-4 倍;
于是,我们可以这么设置:
1.先设置比较大的 Xmx,将项目部署到正式环境稳定运行一段时间
2.使用命令手动触发 FullGC
jcmd [jvm 的进程 id] GC.run
3.使用 jstat -gc 或 jmap -heap 等命令查看堆内存,然后进行设置 Xmx
评论