为什么JVM占用了超过-Xmx配置的内存?
为什么JVM占用了超过-Xmx配置的内存?
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情
前言
一般JVM我们都会配置上 Xmx参数,限制堆内存的使用
但是在top指令或者其他的指令上看到的值却不是我们期望的Xmx的数字
实际上Xmx占用的内存只是JVM所占内存的一部分,JVM经过各种版本的演化,实际上有了许多的变化
可以从下图看出
一个比较大的变化是 JDK8
已经没有了PermSpace, 增加了 metaspace 的区域,用来保存常量池和类常量池,这部分的内存不是分配在堆上的,而是分配在堆外的,通常如果不设置的话,这部分会无限增长,使用 jmap -heap 指令看出maxmetaspacesize的值非常大,可以认为就是没有限制。
这部分的堆外内存只有在full gc时才会被回收
metaspace
可以通过如下几个参数对metaspace 的大小进行限制
- -XX:MetaspaceSize=N :这个参数是初始化的Metaspace大小,该值越大触发Metaspace GC的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控Metaspace的大小,可能增加上线也可能降低。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为21810376B(大约20.8M)
- -XX:MaxMetaspaceSize=N :这个参数用于限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。默认无上限。
- -XX:MinMetaspaceFreeRatio=N :当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增长Metaspace的大小。在本机该参数的默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
- -XX:MaxMetasaceFreeRatio=N :当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。在本机该参数的默认值为70,也就是70%。
- -XX:MaxMetaspaceExpansion=N :Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)
- -XX:MinMetaspaceExpansion=N :Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)
一个误解
使用jstat -gcutil的结果中的M代表的是元空间的使用比例。又例如:测试环境上某个服务为例,配置了-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
,通过jstat -gcutil pid
查看M
的值为98.32
,即Meta区使用率也达到了98.32%
:
然后,再通过jstat -gc 4210 2s 3
命名查看,结果如下图所示,计算MU/MC即Meta区的使用率确实达到了98.32%
,但是MC,即Metaspace Capacity只有55296k,并不是参数MetaspaceSize
指定的256m:
那么-XX:MetaspaceSize=256m
的含义到底是什么呢?
其实,这个JVM参数是指Metaspace扩容时触发FullGC的初始化阈值,也是最小的阈值。这里有几个要点需要明确:
- 如果没有配置
-XX:MetaspaceSize
,那么触发FGC的阈值是21807104(约20.8m),可以通过jinfo -flag MetaspaceSize pid得到这个值;jps -v也可以查看jvm的参数设置情况。 - 如果配置了
-XX:MetaspaceSize
,那么触发FGC的阈值就是配置的值; - Metaspace由于使用不断扩容到
-XX:MetaspaceSize
参数指定的量,就会发生FGC;且之后每次Metaspace扩容都可能会发生FGC(至于什么时候会,比较复杂,跟几个参数有关); - 如果Old区配置CMS垃圾回收,那么扩容引起的FGC也会使用CMS算法进行回收;
- 如果MaxMetaspaceSize设置太小,可能会导致频繁FullGC,甚至OOM;
建议
MetaspaceSize
和MaxMetaspaceSize
设置一样大;- 具体设置多大,建议稳定运行一段时间后通过
jstat -gc pid
确认且这个值大一些,对于大部分项目256m即可。目前我们的使用也是这一个值
\
实际的JVM内存占用如何计算
Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss]
所以实际如果我们配置了如下参数
-Xmx512m -Xss1m -MaxMetaSpaceSize=256m 的JVM实际占用的大小为 512+(1*线程数)+256
可以通过如下指令输出JVM 的内存占用,使用这个指令需要在JVM启动时添加参数
java -XX:NativeMemoryTracking=detail
jcmd pid VM.native_memory
输出如下,可以看到整个JVM使用的内存就是由一下的这些部分组成的
总结
当时的服务实际上时占用了2.5G的内存,这部分因为当时没有加上跟踪参数,所以暂时还无法查看到
根据以上信息推论可以推论得到,占用的大头应该是metaspace
这部分的内存一开始并没有限制,不会因为这部分的内容触发 full gc,动态产生的class都会放在这个区域,进程运行的时间越长,这个区域就会越大,同时也会观察到实际的堆占用并没有增加
现在开发环境采用的参数为 -Xmx512m -Xss512k -XX:MaxMetaspaceSize
=256m,需要观察一段时间看是否限制住了
也可以通过命令 jcmd pid VM.native_memory 建立的快照来对比发生泄漏的区域,找到问题的根源