java的堆内存、非堆内存构成

从容器的自动kill理解java程序的内存使用

一、操作系统的oom kill

引发操作系统对进程进行oom kill的原因:

1、当ram与swap耗尽,操作系统选择低优先级、耗用ram最多的进程进行kill

2、进程占用的ram超出cgroup的限制,如docker/k8s就是利用cgroup限制可用的系统资源

查看系统诊断日志,确认近期是否发生oom kill:

dmesg -T

二、java程序的oom error

java自身对内存进行分区管理,任何一个区域的使用超出限制,都会引发oom error,生成dump并退出程序;如:老生代里长期不释放的类实例、metaspace加载太多类定义、DirectMemory放太多缓冲数据。

三、java的内存构成

为避免oom kill和oom error,需要了解java占用的内存构成,保证每个区域设置了合适的上限,且总大小不超过ram/cgroup的限制。
top命令看到的RES值,即进程实际使用的内存,java进程的RES主要由以下部分构成:

(一) Native Memory

1、Heap

堆内存,放置未释放的类实例,新老生代共用,由-Xmx或-XX:MaxRAMPercentage设置最大值,默认值是1/4的当前ram或1/4的cgroup最大限制

2、Class

放置类定义,用-XX:MaxMetaspaceSize设置最大值,默认无限制

3、Thread

放置stack,即stackoverflow里的stack

4、Code

放置JIT字节码运行时编译成的机器码;如果使用Janino等在线编译工具,要释放类实例和加载类定义的classloader对象后,才能在old gc时回收类定义和机器码占用的内存

5、GC

辅助运行GC逻辑需要使用的内存

6、Other

other部分包含了使用Unsafe.allocateMemory方法在用户空间申请的内存块。

java进程进行普通I/O操作时,数据要在heap、用户空间、内核空间三者流动;
java的DirectByteBuffer类内部使用Unsafe.allocateMemory方法申请内存块,并把申请的内存大小计入DirectMemory指标(用-XX:MaxDirectMemorySize设置最大值);
把java的NIO结合DirectByteBuffer使用时,就不需要再在heap分配缓冲区,也省去了heap与用户空间的数据交换(如果使用memory-mapped file,则连DirectMemory也不用分配,省去了用户空间与内核空间的数据交换)。

要统计Native Memory,需在启动参数里加入-XX:NativeMemoryTracking=summary,查看命令:

jcmd <pid> VM.native_memory summary scale=MB

PS:创建DirectByteBuffer,就可以看到Other增大的部分和分配的DirectByteBuffer大小一致

(二) SHR

shared memory,共享库/文件占用的内存;如:使用memory-mapped file来与操作系统的存储I/O交互时,部分内核态的公共文件缓存就记入了此进程使用的共享内存。
使用top命令直接查看SHR;RES包含了SHR

四、常见java内存失控场景

(一)堆内存

1、集合/数组对象未释放

在ThreadLocal上使用或间接引用了集合,在bean的field上使用集合,集合/数组类型的局部变量没有及时设置为null;

上述用法导致老生代使用的内存超出限制,易引发java.lang.OutOfMemoryError: Java heap space

(二)非堆内存

1、file channel、socket channel、自行申请的DirectByteBuffer未及时close

导致DirectMemory在old gc触发前,一直不释放,Other区持续扩大,易在k8s环境下引发oom kill;

如果并发管理失控,同时操作过多channel,也会导致java.lang.OufOfMemoryError: Direct Buffer Memory

2、在线编译功能的classloader对象未释放

导致在线生成的对象使用完毕并释放后,对象在Class、Code区残留的定义信息不能释放,引发java.lang.OutOfMemoryError: metaspace


版权声明:本文为wzp1986原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>