堆外内存场景、分析及解决方法
问题:某环境发现给定JVM内存为200GB,实际使用量为250GB问题:中车发现给定JVM内存为200GB,实际使用量为250GB
复现:尝试在192.168.10.66机器上复现,复现结果是给定JVM内存为100GB,实际使用量为109GB,现对该场景进行分析
...
已知:总内存 = java heap + no heap(meta space+code cache 等) + 虚拟机进程本身 + 虚拟机栈(线程栈)*线程数 + native heap(一般说的堆外内存)
分析方法:在项目中添加在项目中添加```-XX:NativeMemoryTracking=detail
JVM参数重启项目,使用命令detail```
JVM参数重启项目,写入5分钟后,使用命令```jcmd pid VM.native_memory detaildetail```
查看到的内存分布如下
Total: reserved=109GB, committed=108GB
- Java Heap (reserved=100GB, committed=100GB)
(mmap: reserved=100GB, committed=100GB)
- Thread (reserved=1GB, committed=1GB)
(thread #924)
(stack: reserved=1GB, committed=1GB)
- GC (reserved=4GB, committed=4GB)
(mmap: reserved=4GB, committed=4GB)
- Internal (reserved=4GB, committed=4GB)
(malloc=4GB #17087)
可以看到堆外内存实际占用为4GB,Jprofiler看到的40MB只包含了meta space 和 code space,对我们的分析构成了严重误导!
执行的JAVA指令是:/data/fqx/jdk1.8.0_211/bin/java -Dlogback.configurationFile=./../conf/logback.xml -DIOTDB_HOME=./.. -DTSFILE_HOME=./.. -DIOTDB_CONF=./../conf -DTSFILE_CONF=./../conf -Dname=iotdb\.IoTDB -Xms2048M -Xmx100G -XX:NativeMemoryTracking=detail -Dlogback.configurationFile=./../conf/logback.xml -DIOTDB_HOME=./.. -DTSFILE_HOME=./.. -DIOTDB_CONF=./../conf -DTSFILE_CONF=./../conf -Dname=iotdb\.IoTDB -cp .........
验证方法:为了验证确实是堆外内存导致内存暴涨,我们调整-XX:MaxDirectMemorySize=2G,也就是限制堆外内存最大使用2G,发现内存使用确实下降,但是读写速度有很大的降低(30倍左右),证明IoTDB确实使用了超过2G的堆外内存,如果对堆外内存限制,则性能会有损失,下面是读最大时间的性能截图:
不限制:
限制为2G:
下一步计划:通过加大读写压力使得IoTDB占用内存进一步升高,再次查看堆外内存使用情况,证明堆外内存与读写压力正相关
...
Total: reserved=134GB, committed=132GB
- Java Heap (reserved=100GB, committed=99GB)
(mmap: reserved=100GB, committed=99GB)
- Thread (reserved=1GB, committed=1GB)
(thread #834)
(stack: reserved=1GB, committed=1GB)
- GC (reserved=4GB, committed=4GB)
(mmap: reserved=4GB, committed=4GB)
- Internal (reserved=29GB, committed=29GB)
(malloc=29GB #15358)
已知堆外内存在满了之前都不会主动释放,那么这个堆外内存将会一直增长到阈值(默认与堆内内存一样大),然后才释放,如果没到阈值系统内存就不够了,就会被杀死,具体释放逻辑可以见:https://blog.csdn.net/u013096088/article/details/78774627
最新情况:堆外内存增长到36GB之后就不继续增长了,可能有内部释放机制
堆外内存分析
0.文档结构
1-6节列举了一些分析过程和工具,第7节总结了排查方法,第8节是相关参考资料
1.使用堆外内存的原因:
避免与操作系统交互时jvm gc移动某一段buffer的位置,导致io操作使用不正确的地址[1]
推论:需要buffer的系统调用均需要堆外内存,要么是显式声明在java代码里(DirectBytebuffer),要么是隐含在c代码中(BufferedOutputStream)
2.使用google-perf工具分析c语言调用与堆栈,确定问题[2][3]
(1)安装流程(具体步骤在参考资料中,这里列出主要步骤)
- 安装lib-uwind库
- 安装google-ppfor工具
- 增加环境变量,改变jvm使用的malloc库
- 运行iotdb一段时间
- 使用google-ppfor生成分析文件
(2)8存储组,50设备,每个设备100个传感器下内存正常,堆外内存使用不多,在合理范围内,需要能够复现堆外内存的场景
注:图中红框就是申请堆外内存的部分
(3)增加压力后出现JVM crash,是GC执行过程中的NULL指针,怀疑工具库实现有bug,这也意味着生产环境不能贸然使用该工具
3.每一个IO thread都会cache一部分的堆外buffer,不会释放,所以解决堆外内存多有两个方法[4]:
(1)减少IO线程
(2)jdk版本大于1.8后可以使用-Djdk.nio.maxCachedBufferSize去限制cache buffer的使用
如果不限制,则每次申请direct buffer就会cache,除非使用了cache中的buffer,也就是说堆外内存只增不降
由此做一组实验,其他参数相同的情况下,增加thrift线程数(也就是client数),看堆外内存使用情况:
线程数 | 堆外内存使用 |
50 | 270MB |
100 | 300MB |
200 | 320MB |
300 | 350MB |
可以看出thrift线程数对堆外内存有影响,但是不是那么大,别的IO线程可能占了更大的部分,需要进一步分析。
0.11版本thrift线程池很大小,田原已经限制了WAL总的堆外内存池,很大程度上解决了问题
4.堆外内存的主要来源[5]
- 线程与sockets
- direct ByteBuffers的使用
- 第三方库使用native内存(可以从google-ppfor中看出来)
5.内存分析神器——jxray[6]
虽然是收费软件,但是功能很强大,可以试用14天,可以分析出很多内存问题,
举个例子:iotdb中有大量重复的string
6.排查方法总结
(1)使用jxray(见5)分析dump的堆,查看第18项和22项(Off-heap memory used by java.nio.DirectByteBuffers and Thread stacks),排查IO使用的buffer和线程栈空间
(2)使用google-pprof(见2)生成本地方法内存使用图,查看本地方法(比如压缩,编码等)使用的内存空间
7.参考资料
[1]https://stackoverflow.com/questions/5670862/bytebuffer-allocate-vs-bytebuffer-allocatedirect (based on book of java ino)
[2] https://blog.csdn.net/21aspnet/article/details/88032700
[3] https://blog.csdn.net/woaiwojia6699/article/details/111224528
[4] https://dzone.com/articles/troubleshooting-problems-with-native-off-heap-memo
[5]https://stackoverflow.com/questions/39329455/java-non-heap-memory-analyzes