目录
本文主要是结合JVM内存模型,借助大佬经验使用倒推法学习JVM堆内存分配调参
JVM内存模型
下图是 JDK 1.6、1.7、1.8 的内存模型演变过程,其实这个内存模型就是 JVM 运行时数据区依照JVM虚拟机规范的具体实现过程。在图中各个版本的迭代都是为了更好的适应CPU性能提升,最大限度提升的JVM运行效率。这些版本的JVM内存模型主要有以下差异:
- JDK 1.6:有永久代,静态变量存放在永久代上。
- JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
- JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的
元空间
。但字符串常量池仍然存放在堆上。
一个JVM进程内存占用的例子
例:JVM进程内存占用。 8 CPU, 12 GB 物理内存,OpenJDK 11.0.7+10 | ||||||||
---|---|---|---|---|---|---|---|---|
堆 | 非堆 | 总占用 | ||||||
<不细分> | 元空间 | 线程相关 | 代码缓存 | GC 相关 | Java NIO 使用的直接内存 | 其它 | ||
committed | 8192 MB | 196 MB | 73 MB | 110 MB | 368 MB | 229 MB | 59 MB | 9226 MB |
reserved | 8192 MB | 1195 MB | 589 MB | 250 MB | 368 MB | 229 MB | 59 MB | 10881 MB |
33381 个类 | 584 个线程 | 不一定全是 |
*文末有该例的详细数据。
在内存一定且够用的情况下,下面通过倒推法找出合适的堆空间大小。
系统占用
JVM 进程之外,主要是操作系统和其它进程所占的内存。
在 KVM(实体机类似)虚假机上,另外运行的进程主要是 flume, 用于日志收集的。两个 flume 相关的进程共占内存大概是 500 MB. 其它进程或者说操作系统大概占 700 MB. 这些部分合起来留点富余,至少要保留 1.5 G 的空间。用 kvm 的总物理内存去掉这部分,就是留给 JVM 进程的总可用内存大小 。比如,4G - 1.5G = 2.5G, 8G - 1.5G = 6.5G, 12G - 1.5G = 11.5G.
特殊的, docker 容器的实例和 kvm 虚拟机不同,没有那么多操作系统相关的线程占用内存,也没有 flume 进程用于处理日志收集,目前除 tomcat 进程外,主要有一个 bistoury 进程占用空间较明显一些,committed 136MB, 但这个应该是 bistoury 进程相对空闲时候的占用,不同的使用可能会有不同的占用情况。建议至少预留 500MB ~ 1GB 的空间。如果 docker 容器的内存空间较充足,比如 12G,可以无脑考虑跟 kvm 一样,认为环境占用的内存是 1.5G.
实体机的物理内存一般都比较大,主要会影响 GC 相关的内存占用也会相应增大,请以实际情况为准。JVM外的系统占用也可以多留一下富余。
JVM的非堆内存
一个应用程序的源代码和各部分的运行情况确定以后,这部分空间一般也是确定的,需要通过一定的工具来查看,从而掌握具体的情况。不同的应用之间差异可能会比较明显。
这里说明一下内存各部分分配时主要考虑的几个方面:
元空间
主要存储类信息和代码相关的内容,这部分空间在 JDK1.8 之后,默认是无限制的,如果需要限制,可通过 -XX:MaxMetaspaceSize=[size] 指定可提交的最大空间限制。JDK1.8 之前,通过 -XX:MaxPermSize=[size] 参数设置,效果类似。
-XX:MaxMetaspaceSize=[size] 这个参数虽然不指定时是无限制的,事实上还是受制于系统可用的空间大小。如果堆分配的过大,留给这部分可用的空间可能就会很紧张。如果物理内存不够用,就会有部分数据使用 swap. docker 不允许使用 swap, 所以一旦超出可用量,可能的结果就是进程崩溃。
-XX:MetaspaceSize=[size] 这个参数主要影响什么时候第一次触发 GC(一般会是 Full GC, 因为要有效回收元空间的内容,需要先回收堆中的内容)。第一次触发后,触发 GC 的阈值会根据实际的占用情况动态变化。之前遇到过一种情况是这个值不设置,JVM启动时选择的初始值较小,随着系统加载的内容越来越多,系统整个启动和预热的过程中会不断地触发 GC 并增长实际的占用空间,导致的问题会比较明显。不记得这个问题所对应的 JDK 版本了,新版本的 JDK 可能表现会更好。但建议总是设置这个值为系统稳定工作时实际占用空间的至少 120%.
为了减少 JVM 进程 reserved 的空间大小(或者叫虚拟地址空间大小),建议设置 -XX:MaxMetaspaceSize=[size] 的值为 2 倍的 -XX:MetaspaceSize=[size] 这个值。即使不这么设置,也要在设置堆空间时留出这么多的空间。设置上还有一个好处是,便于快速识别到这个空间的需求大小,也方便其它空间的计算。
线程相关
在 linux x64 平台上,JVM 线程栈的默认空间大小为 1024KB, 可通过 -XX:ThreadStackSize=[size] 或者 -Xss[size] 进行调整,一般不需要调整这个值。
从上面的例子可以看到,584个线程预留的空间 reserved 大小为 589 MB,而实际提交占用的空间为 73 MB. 在这个例子中,实际占用的情况相对是比较低的,而且这个跟捕捉这个信息时进程所处的执行状况也比较相关。
最保险的作法是留出和线程个数相当的 MB 大小的空间。如果想少留这部分空间,需要仔细观察进行在各种情况下的实际占用情况。
代码缓存
用于存储 JIT 编译后的代码。
-XX:ReservedCodeCacheSize=[size] 参数用于限定这个空间的最大值,默认为 240MB. 另外,这个值最大允许的值为 2GB. 如果需要设置超过 240MB 的值,建议按实际占用情况的 150% 进行设置。
JIT 编译后的代码不一定会常驻内存,如果这些代码长时间不被执行,是有可能会被卸载的。所以,如果程序的某些部分不是一直在使用,这个空间的大小可能会经常波动。
GC 相关
这部分是 GC 工作时需要的一些内部空间。其大小跟管理的堆空间大小有一定关系,堆空间越大,这个空间相应的也会越大。
这个没有参数来影响,但考虑堆空间大小时要把这部分的空间预留出来。实体机的物理内存一般都比较大,分配的堆空间相应的也会比较大,可考虑给 GC 这部分多留一些空间,请以实际情况为准。
Java NIO 使用的直接内存
Dubbo、netty 以及一些异步的 HTTP Client 工具都可能会使用这个空间。应以程序工作稳定时的实际占用为准。
-XX:MaxDirectMemorySize=[size] 可用于限定这个空间的最大值,默认为 0, 表示由 JVM 选择合适的大小 。
其它
还有好多细项,所占内存不多,因为其它项一般都留了不少富余,这部分内容一般可不考虑。
如果要查看 JVM 进程的非堆内存占用情况,可使用命令 jcmd <pid> VM.native_memory 达到目的,其所呈现的内容由 -XX:NativeMemoryTracking=[mode] 参数影响,mode 取值有 off, summary, detail 三个,建议使用 summary, 在线上未发现明显影响。
docker 的机器的 jcmd 没有成功执行过,估计跟 bistoury 进程有关,目前只能通过 bistoury 的页面来查看各项指标,只是没有 jcmd 分的那么细,主要内容也是可以找到的。
堆的大小
当上面的部分都确定以后,堆的大小自然也就出来了。上面的部分所需的空间预留充足以后剩下的部分可尽数分给堆空间。用这个值设置 -Xms[size] 和 -Xmx[size] 即可(总是把 -Xms 和 -Xmx 设置为合适的相同值一般是较好的推荐作法)。
以上面的例子来看,
commited: JVM非堆大小 = 196 + 73 + 110 + 368 + 229 + 59 = 1035MB = 1GB, 堆大小可取 12 - 1.5 - 1 = 9.5GB
reserved: JVM非堆大小 = 1195 + 589 + 250 + 368 + 229 + 59 = 2690MB = 2.6GB, 堆大小可取 12 - 1.5 - 2.6 = 7.9GB
堆的实际配置是 8GB,相当于预留了较多的富余以备波动之需。如果想多给堆分配一些,以这个例子来说,不建议分配超过 9GB 的堆空间。因为超过了 9GB 的空间,相当于非堆部分的富余量就很小了,有点波动可能就会出问题。
以该例来看,建议指定的 JVM 启动参数为 "-Xms8g -Xmx8g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m".
建议参考值
4G 物理内存,最大 2G 堆空间;
8G 物理内存,最大 5G 堆空间;
12G 物理内存,最大 9G 堆空间,建议 8G.
大内存物理机,建议留出系统占用量之后,剩余空间的 80% 分配给堆内存。
元空间,如无特殊情况,建议按照 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m 进行设置。第2个参数可以不设置,但建议总是设置第一个参数。
注意
除了 Java 自身管理的堆内和堆外内存之外,其实还有一部分内存占用容易被忽视,就是在系统的 cache 中占用的部分。
我们自己负责的应用在 cache 中所占部分,观察到的从 500M ~ 1.5G 不等,不同的应用差异也比较大。
上面的配置值的建议值,并未考虑这部分的占用。所以在实际中,应该把这部分也考虑进去,留出相应的空间。尤其是容器化部署的应用。
top 命令和 java 的各种查看内存占用的工具,都不显示这部分内存占用,而 docker 的 Watcher 监控里对应用进程的内存占用的监控是有这部分的。
参考
附:文章开头部分的例子的详细数据
需要开启参数:-XX:NativeMemoryTracking=detail
查看命令:jcmd <PID> VM.native_memory summary
Native Memory Tracking: Total: reserved=11141643KB, committed=9446843KB - Java Heap (reserved=8388608KB, committed=8388608KB) (mmap: reserved=8388608KB, committed=8388608KB) - Class (reserved=1223914KB, committed=200554KB) (classes #33381) ( instance classes #31535, array classes #1846) (malloc=11498KB #155642) (mmap: reserved=1212416KB, committed=189056KB) ( Metadata: ) ( reserved=163840KB, committed=163328KB) ( used=153557KB) ( free=9771KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=25728KB) ( used=20700KB) ( free=5028KB) ( waste=0KB =0.00%) - Thread (reserved=603124KB, committed=74364KB) (thread #584) (stack: reserved=600268KB, committed=71508KB) (malloc=2105KB #3506) (arena=751KB #1167) - Code (reserved=255553KB, committed=112873KB) (malloc=7865KB #34588) (mmap: reserved=247688KB, committed=105008KB) - GC (reserved=376369KB, committed=376369KB) (malloc=31773KB #74855) (mmap: reserved=344596KB, committed=344596KB) - Compiler (reserved=3098KB, committed=3098KB) (malloc=3033KB #4022) (arena=65KB #5) - Internal (reserved=4869KB, committed=4869KB) (malloc=4837KB #11584) (mmap: reserved=32KB, committed=32KB) - Other (reserved=234017KB, committed=234017KB) (malloc=234017KB #361) - Symbol (reserved=31041KB, committed=31041KB) (malloc=27037KB #368757) (arena=4004KB #1) - Native Memory Tracking (reserved=11484KB, committed=11484KB) (malloc=541KB #7560) (tracking overhead=10943KB) - Arena Chunk (reserved=695KB, committed=695KB) (malloc=695KB) - Logging (reserved=7KB, committed=7KB) (malloc=7KB #264) - Arguments (reserved=19KB, committed=19KB) (malloc=19KB #520) - Module (reserved=7310KB, committed=7310KB) (malloc=7310KB #24522) - Synchronizer (reserved=1529KB, committed=1529KB) (malloc=1529KB #12818) - Safepoint (reserved=8KB, committed=8KB) (mmap: reserved=8KB, committed=8KB)