写点什么

一个 jvm 线程占用多少操作系统内存

用户头像
hasWhere
关注
发布于: 2021 年 06 月 16 日

原文:https://my.oschina.net/xiaominmin/blog/3136486

找到关键点

在看到 12452 个等待在 CachedBnsClient.run 的业务的一瞬间笔者就意识到,肯定是这边的线程导致对外内存泄露了。下面就是根据线程大小计算其泄露内存量是不是确实能够引起 OOM 了。

发现内存计算对不上

由于我们这边设置的 Xss 是 512K,即一个线程栈大小是 512K,而由于线程共享其它 MM 单元(线程本地内存是是现在线程栈上的),所以实际线程堆外内存占用数量也是 512K。进行如下计算:

12563 * 512K = 6331M = 6.3G
复制代码

整个环境一共 4G,加上 JVM 堆内存 1.8G(1792M),已经明显的超过了 4G。

(6.3G + 1.8G)=8.1G > 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,那么用户态的栈可用内存就是:

512K-8K=504K
复制代码

如下图所示:


Linux 实际物理内存映射

事实上 linux 对物理内存的使用非常的抠门,一开始只是分配了虚拟内存的线性区,并没有分配实际的物理内存,只有推到最后使用的时候才分配具体的物理内存,即所谓的请求调页。如下图所示:


查看 smaps 进程内存使用信息

使用如下命令,查看

cat /proc/[pid]/smaps > smaps.txt
复制代码

实际物理内存使用信息,如下所示:

7fa69a6d1000-7fa69a74f000 rwxp 00000000 00:00 0 Size:                504 kBRss:                  92 kBPss:                  92 kBShared_Clean:          0 kBShared_Dirty:          0 kBPrivate_Clean:         0 kBPrivate_Dirty:        92 kBReferenced:           92 kBAnonymous:            92 kBAnonHugePages:         0 kBSwap:                  0 kBKernelPageSize:        4 kBMMUPageSize:           4 kB
7fa69a7d3000-7fa69a851000 rwxp 00000000 00:00 0 Size: 504 kBRss: 152 kBPss: 152 kBShared_Clean: 0 kBShared_Dirty: 0 kBPrivate_Clean: 0 kBPrivate_Dirty: 152 kBReferenced: 152 kBAnonymous: 152 kBAnonHugePages: 0 kBSwap: 0 kBKernelPageSize: 4 kBMMUPageSize: 4 kB
复制代码

搜索下 504KB,正好是 12563 个,对了 12563 个线程,其中 Rss 表示实际物理内存(含共享库)92KB,Pss 表示实际物理内存(按比例共享库)92KB(由于没有共享库,所以 Rss==Pss),以第一个 7fa69a6d1000-7fa69a74f000 线性区来看,其映射了 92KB 的空间,第二个映射了 152KB 的空间。如下图所示:

挑出符合条件(即 size 是 504K)的几十组看了下,基本都在 92K-152K 之间,再加上内核栈 8K

(92+152)/2+8K=130K,由于是估算,取整为128K,即反映此应用平均线程栈大小。
复制代码

注意,实际内存有波动的原因是由于环境不同,从而走了不同的分支,导致栈上的增长不同。

重新进行内存计算

JVM 一开始申请了

-Xmx1792m -Xms1792m
复制代码

即 1.8G 的堆内内存,这里是即时分配,一开始就用物理页框填充。12563 个线程,每个线程栈平均大小 128K,即:

128K * 12563=1570M=1.5G的对外内存
复制代码

取个整数 128K,就能反映出平均水平。再拿这个 128K * 12563 =1570M = 1.5G,加上 JVM 的 1.8G,就已经达到了 3.3G,再加上 kernel 和日志传输进程等使用的内存数量,确实已经接近了 4G,这样内存就对应上了!(注:用于定量内存计算的环境是一台内存用量将近 4G,但还没 OOM 的机器)

为什么在物理机上没有应用 Down 机

笔者登录了原来物理机,应用还在跑,发现其同样有堆外内存泄露的现象,其物理内存使用已经达到了 5 个多 G!幸好物理机内存很大,而且此应用发布还比较频繁,所以没有被 OOM。Dump 了物理机上应用的线程,

一共有28737个线程,其中28626个线程等待在CachedBnsClient上。 
复制代码

同样用 smaps 查看进程实际内存信息,其平均大小依旧为

128K,因为是同一应用的原因
复制代码

继续进行物理内存计算

1.8+(28737 * 128k)/1024K =(3.6+1.8)=5.4G
复制代码

进一步验证了我们的推理。

这么多线程应用为什么没有卡顿

因为基本所有的线程都睡眠在

 Thread.sleep(60 * 1000);//一次睡眠60s
复制代码

上。所以仅仅占用了内存,实际占用的 CPU 时间很少。

总结

查找 Bug 的时候,现场信息越多越好,同时定位 Bug 必须要有实质性的证据。例如内存泄露就要用你推测出的模型进行定量分析。在定量和实际对不上的时候,深挖下去,你会发现不一样的风景!

用户头像

hasWhere

关注

间歇性努力的学习渣 2018.04.20 加入

通过博客来提高下对自己的要求

评论

发布
暂无评论
一个jvm线程占用多少操作系统内存