一个 jvm 线程占用多少操作系统内存
原文:https://my.oschina.net/xiaominmin/blog/3136486
找到关键点
在看到 12452 个等待在 CachedBnsClient.run 的业务的一瞬间笔者就意识到,肯定是这边的线程导致对外内存泄露了。下面就是根据线程大小计算其泄露内存量是不是确实能够引起 OOM 了。
发现内存计算对不上
由于我们这边设置的 Xss 是 512K,即一个线程栈大小是 512K,而由于线程共享其它 MM 单元(线程本地内存是是现在线程栈上的),所以实际线程堆外内存占用数量也是 512K。进行如下计算:
整个环境一共 4G,加上 JVM 堆内存 1.8G(1792M),已经明显的超过了 4G。
如果按照此计算,应用应用早就被 OOM 了。
怎么回事呢?
为了解决这个问题,笔者又思考了好久。如下所示:
Java 线程底层实现
JVM 的线程在 linux 上底层是调用 NPTL(Native Posix Thread Library)来创建的,一个 JVM 线程就对应 linux 的 lwp(轻量级进程,也是进程,只不过共享了 mm_struct,用来实现线程),一个 thread.start 就相当于 do_fork 了一把。其中,我们在 JVM 启动时候设置了-Xss=512K(即线程栈大小),这 512K 中然后有 8K 是必须使用的,这 8K 是由进程的内核栈和 thread_info 公用的,放在两块连续的物理页框上。如下图所示:
众所周知,一个进程(包括 lwp)包括内核栈和用户栈,内核栈+thread_info 用了 8K,那么用户态的栈可用内存就是:
如下图所示:
Linux 实际物理内存映射
事实上 linux 对物理内存的使用非常的抠门,一开始只是分配了虚拟内存的线性区,并没有分配实际的物理内存,只有推到最后使用的时候才分配具体的物理内存,即所谓的请求调页。如下图所示:
查看 smaps 进程内存使用信息
使用如下命令,查看
实际物理内存使用信息,如下所示:
搜索下 504KB,正好是 12563 个,对了 12563 个线程,其中 Rss 表示实际物理内存(含共享库)92KB,Pss 表示实际物理内存(按比例共享库)92KB(由于没有共享库,所以 Rss==Pss),以第一个 7fa69a6d1000-7fa69a74f000 线性区来看,其映射了 92KB 的空间,第二个映射了 152KB 的空间。如下图所示:
挑出符合条件(即 size 是 504K)的几十组看了下,基本都在 92K-152K 之间,再加上内核栈 8K
注意,实际内存有波动的原因是由于环境不同,从而走了不同的分支,导致栈上的增长不同。
重新进行内存计算
JVM 一开始申请了
即 1.8G 的堆内内存,这里是即时分配,一开始就用物理页框填充。12563 个线程,每个线程栈平均大小 128K,即:
取个整数 128K,就能反映出平均水平。再拿这个 128K * 12563 =1570M = 1.5G,加上 JVM 的 1.8G,就已经达到了 3.3G,再加上 kernel 和日志传输进程等使用的内存数量,确实已经接近了 4G,这样内存就对应上了!(注:用于定量内存计算的环境是一台内存用量将近 4G,但还没 OOM 的机器)
为什么在物理机上没有应用 Down 机
笔者登录了原来物理机,应用还在跑,发现其同样有堆外内存泄露的现象,其物理内存使用已经达到了 5 个多 G!幸好物理机内存很大,而且此应用发布还比较频繁,所以没有被 OOM。Dump 了物理机上应用的线程,
同样用 smaps 查看进程实际内存信息,其平均大小依旧为
继续进行物理内存计算
进一步验证了我们的推理。
这么多线程应用为什么没有卡顿
因为基本所有的线程都睡眠在
上。所以仅仅占用了内存,实际占用的 CPU 时间很少。
总结
查找 Bug 的时候,现场信息越多越好,同时定位 Bug 必须要有实质性的证据。例如内存泄露就要用你推测出的模型进行定量分析。在定量和实际对不上的时候,深挖下去,你会发现不一样的风景!
评论