(2020年8月23日 20:52:46)

java有多个内存区域,不同的区域有不同的OOM排查方式。根据排查方式的分类,可以分为5份:

  1. java堆内存,特指young区和old区,这两个区也是GC主要工作的内存区域,一般情况下我们使用的内存也在这个范围内,用-Xmx指定的内存大小也是这两个的大小之和。

  2. Metaspace(java1.6叫永久代,java8已经是metaspace)、Compressed Class Space、Code Cache,这几个在用jstat -gc可以看到MCCCSC这两列,可以认为这两个就是这类内存的大小,这些不属于堆内存。一般这几个内存很稳定,存的是类和方法等信息。除非有动态生成类信息,否则这些内存比较少出问题。

  3. 堆外内存,java申请的,主要是unsafe包和DirectByteBuffer申请的内存。这个内存jstat -gc里面体现不出来,ps aux的物理内存则包含这部分内存。但是这部分内存可以由java本身跟踪出来,一般也可以和GC挂钩起来。

  4. 堆外内存,第三方native code申请的。这个内存jstat -gc里面体现不出来,ps aux的物理内存则包含这部分内存。这部分内存java跟踪不出来,不属于java管的范围了。

  5. 线程栈占用的内存。

1. 堆内存

堆内存可以被java GC管理,通过常规的jstat和jmap工具,一般能查出是哪些对象导致的OOM。特别注意的是,出现OOM的线程和实际导致OOM的线程不一定是同一个,一般出现OOM时,CPU会飙升,因为GC一直在频繁进行,此时GC占CPU的比例可能高达90%多,而正常情况下,这个比例不应该超过10%。

堆内存可以dump出来之后,用jprofiler等工具分析,包括哪些对象的占用比较大、GC root reference的引用关系(例如被静态成员变量引用)。

2. Metaspace等

这块内存也可以从jstat查看,还可以用jinfo查看jvm中加载的class类的个数,看是否有异常。

3. 堆外内存:unsafe包和DirectByteBuffer

在代码https://code.pugwoo.com/learning/java-labs/src/branch/master/jvm-memory 中有unsafe包和DirectByteBuffer申请内存的例子,同时部署在rancher上jvm-memory。

DirectByteBuffer是基于unsafe工具来做的,但它和GC挂钩,可以被GC自动回收掉,而unsafe不能。所以一般我们不会直接用unsafe。

这部分申请的内存,在jstat和arthas等工具中是看不到的。要跟踪这部分内存的申请和占用,需要在启动java进程时添加参数:-XX:NativeMemoryTracking=detail

然后等待java进程运行一段时间,然后执行:

jcmd <PID> VM.native_memory

输出结果中,Internal这一项的committed值就是这两个申请的内存大小。

-                  Internal (reserved=1333KB, committed=1333KB)
                            (malloc=1301KB #8266) 
                            (mmap: reserved=32KB, committed=32KB)

但是这个值并没有包含native code第三方申请的内存。

另外,上面申请的内存,也可以体现在ps aux显示的物理占用内存上,这里还有一个神器,可以看整个java进程(实际上就是当作一个普通进程来看)的虚拟内存段和实际对应的物理内存大小:

pmap -x <PID>

输出为:

Address           Kbytes     RSS   Dirty Mode  Mapping
00000000f0000000   11008   10880   10880 rw---   [ anon ]
00000000f0ac0000   76352       0       0 -----   [ anon ]
00000000f5550000   21888   11496   11496 rw---   [ anon ]
.
. 省略很多行
.
00007ffd4232a000       8       0       0 r----   [ anon ]
00007ffd4232c000       8       4       0 r-x--   [ anon ]
ffffffffff600000       4       0       0 r-x--   [ anon ]
---------------- ------- ------- ------- 
total kB         3327408  130140  114196

KBytes这行是虚拟内存大小,RSS是物理内存大小。最后total的RSS物理内存大小和ps aux看到的是一致的。

说明:申请内存本身是个逻辑操作,并不会真正使用到物理内存,所以它很快。然后对内存进行读写时,才会真正用到物理内存(而且是按需的,读写多少占用多少),才会体现在上述工具的物理内存数据上。

说明:这里我申请了100M的内存,返回给我内存的起始offset,但是通过pmap查询却可以看到这个offset对应的内存段有300M的内存,可能是系统优化预留的,还没试如果我写超过了100M的内存,会怎样,如果写超过300M的内存,会怎样,或者更特别的,我随便写其它位置的内存,会怎样?

4. 第三方native code中申请的内存

这里也有一个测试代码,也是放在jvm-memory的那份代码中,用C语言的malloc申请内存,然后编译为so库,然后java用jna加载并调用申请内存。

申请内存本身是个逻辑操作,只有在真正读写内存时,才会真正使用到物理内存,而且是按需的,读写多少占用多少。

这一部分申请的内存,java并没有办法跟踪到,可以认为已经是java范围之外的了,所以只能用操作系统的工具来排查了。

首先这个内存可以在ps auxpmap -x PID中体现出来,所以土一点连猜带蒙的做法,可以实时关注pmap的变化,记录下哪个内存段增长得比较快,然后用gdb工具attach上去(特别注意,此时该进程是停止状态,不要在生产上这么做,要退出或kill掉gdb才恢复正常),然后dump出这段内存,然后到本地分析一下这段内存里面放的是什么东西。简单示例的几个命令:

gdb attach 1 # attach一个pid

# 在gdb命令中执行:(内存地址范围从pmap拿)
dump memory /tmp/mem1.dump 0x00000000f0000000 0x00000000f2a10000

# dump出文件之后拿到本地分析
# 例如:查看mem1.dump内存中至少大于10字符的字符
strings -10 mem1.dump

上面的这种方式比较抓瞎,更靠谱一点的方式是用strace来跟踪系统调用,一般系统调用申请内存的函数是brk,mmap,munmap。java一般是多线程,所以要跟踪所有子线程的系统调用。命令:

strace -pf <PID> # 这个是attach到一个运行中的java进程,也可以用strace起一个java进程,捕捉最开始的调用

然后关注mmap、brk、munmap这些系统调用,例如:

上图就可以看到malloc函数对应的系统调用mmap,申请了一个100003840字节大小的内存,申请到的内存地址是0x7fc65e467000,还可以看到申请这个内存的pid是29,然后用top -H查看所有的线程(下一页按Page Down),新版的top可以看到线程的名称了:

可以看到是一条http-nio-8080的线程,是tomcat的处理线程,所以这个内存请求是一次前端url请求。我们可以再把这条进程的所有系统调用看看,看申请内存的上下文都做了什么,可以看到:

它是调用了GET /native_allocate_memory这个请求而申请了,范围就又进一步缩小了。

5. 进程(线程)占用的内存

首先线程占用的内存,不在堆和metaspace里面。我做个简单的实验,创建1000个线程,有一部分(大概一半)内存占用体现在ps aux的进程占用上,还有一部分体现在操作系统内存占用上(用free命令看)。

另外还有一个现象,java默认一个线程的栈大小是1M,那么1台1G内存的机器,操作系统和开了java之后剩下800M这样,那按理论就只能开800个线程。但是实际上做个实验,创建2000个线程也没有问题,此时系统只减少了40M的内存,平均每个线程才20K;当增加到6000个线程时,ps aux看到的java增加了180M内存这样,而free则减少了300M,平均每个线程50K。这说明,栈的1M大小只是虚拟内存占这么大,物理内存也是只有使用到了才实际占用。

所以整个java进程及其线程的内存占用,基本还是看jinfo <PID>里的线程数,一般情况java也就几百个线程,不会超过1000个的。

参考文章

  1. http://blog.itpub.net/31559354/viewspace-2708985/
  2. Java Nio之直接内存 https://www.jianshu.com/p/502a1af6cf3f
文档更新时间: 2020-08-27 10:29   作者:nick