线上JVM thread 3000+ 未OOM的思考

某天线上突然出了报警。

JVM Thread Count > 3000 触发。

首先观察 集群的监控,发现不是个例,此应用的集群线程数都是2900~3000左右。

1.jstack 文件分析 

经过业务和框架代码的问题,最终定位是 存在大量 ZkClient 对象, 每个 ZkClient 建连都会启动3个线程。

ZkClient 类 会启动 一个 名称为  ZkClient-EventThread- 的线程。

接着 调用 zkconnection.connect(watcher); 方法时,会创建 一个 ClientCnxn 的实例 。

org.apache.zookeeper.ClientCnxn#start 会启动,两个线程。localhost-startStop-sendThread 和 localhost-startStop-eventThread 。

问题原因最终定位,并修复。

同时给我产生了一定的困惑。

为什么3000+的线程却没有OOM?

理想中的计算公式

最大可用内存/Xss = 最大线程

Xss 参数配置 java8 及以上 默认为 1024K

容器内存 5.5G,Xmx 堆内存 4G,元空间 256MB

可用 内存 1280M/1M = 1280;最多1280左右个线程,机器就撑不住了。

网上查了一些资料 

查看Thread 到底占用了多少内存

需要开启 JVM参数:-XX:NativeMemoryTracking=detail

然后执行 ,就可以看到 jvm 本地内存的分析

jcmd {pid} VM.native_memory

Native Memory Tracking:

Total: reserved=3439080KB, committed=2228892KB
-                 Java Heap (reserved=1468416KB, committed=1468416KB)
                            (mmap: reserved=1468416KB, committed=1468416KB)

-                     Class (reserved=1149482KB, committed=114346KB)
                            (classes #18958)
                            (malloc=2602KB #43119)
                            (mmap: reserved=1146880KB, committed=111744KB)

-                    Thread (reserved=238418KB, committed=238418KB)
                            (thread #856)
                            (stack: reserved=234588KB, committed=234588KB)
                            (malloc=2827KB #4282)
                            (arena=1003KB #1708)

-                      Code (reserved=266377KB, committed=94193KB)
                            (malloc=16777KB #19327)
                            (mmap: reserved=249600KB, committed=77416KB)

-                        GC (reserved=102043KB, committed=102043KB)
                            (malloc=14783KB #36820)
                            (mmap: reserved=87260KB, committed=87260KB)

-                  Compiler (reserved=1974KB, committed=1974KB)
                            (malloc=1844KB #3350)
                            (arena=131KB #6)

-                  Internal (reserved=179912KB, committed=179912KB)
                            (malloc=179880KB #86296)
                            (mmap: reserved=32KB, committed=32KB)

-                    Symbol (reserved=22345KB, committed=22345KB)
                            (malloc=19652KB #207060)
                            (arena=2693KB #1)

-    Native Memory Tracking (reserved=6934KB, committed=6934KB)
                            (malloc=536KB #7399)
                            (tracking overhead=6398KB)

-               Arena Chunk (reserved=309KB, committed=309KB)
                            (malloc=309KB)

-                   Unknown (reserved=2868KB, committed=0KB)
                            (mmap: reserved=2868KB, committed=0KB)

可以看到两种类型的内存:

“Reserved:” 由操作系统承诺的可用内存大小。但尚未分配,JVM 无法访问

”Committed:“ 已被 JVM 分配,可访问

在“Thread”部分可以看到,‘reserved’ 和 ‘committed’ 大小相同,接近于“线程数 ‘ 1MB”。这时因为 JVM 一开始就尽可能地为线程分配内存。

可以发现,确实实际占用内存没那么多。

查看 每个线程栈的大小 

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,那么用户态的栈可用内存就是:

1024k-8k=1016k

Linux实际物理内存映射

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

查看smaps进程内存使用信息

执行命令

cat /proc/[pid]/smaps > smaps.txt

可以看到 

7f06b7f02000-7f06b8000000 rw-p 00000000 00:00 0

Size:               1016 kB

Rss:                  92 kB

Pss:                  92 kB

Shared_Clean:          0 kB

Shared_Dirty:          0 kB

Private_Clean:         0 kB

--

MMUPageSize:           4 kB

Locked:                0 kB

ProtectionKey:         0

VmFlags: mr mp me ac sd

7f06cc0ef000-7f06cc1ed000 rw-p 00000000 00:00 0

Size:               1016 kB

Rss:                 104 kB

Pss:                 104 kB

Shared_Clean:          0 kB

Shared_Dirty:          0 kB

Private_Clean:         0 kB

--

MMUPageSize:           4 kB

Locked:                0 kB

ProtectionKey:         0

VmFlags: mr mp me ac sd

7f06cc7fa000-7f06cc8f8000 rw-p 00000000 00:00 0

Size:               1016 kB

Rss:                 104 kB

Pss:                 104 kB

Shared_Clean:          0 kB

Shared_Dirty:          0 kB

Private_Clean:         0 kB

--

MMUPageSize:           4 kB

Locked:                0 kB

ProtectionKey:         0

VmFlags: mr mp me ac sd

7f06cc8fb000-7f06cc9f9000 rw-p 00000000 00:00 0

Size:               1016 kB

Rss:                 108 kB

Pss:                 108 kB

Shared_Clean:          0 kB

Shared_Dirty:          0 kB

Private_Clean:         0 kB

--

MMUPageSize:           4 kB

Locked:                0 kB

ProtectionKey:         0

size:用户态的大小  1016k

搜索下1016k,正好是1988个,对了1988个线程,其中Rss表示实际物理内存(含共享库)92KB,Pss表示实际物理内存(按比例共享库)92KB(由于没有共享库,所以Rss==Pss),以第一个7f06b7f02000-7f06b8000000

线性区来看,其映射了92KB的空间,第二个映射了104KB的空间。如下图所示:

线程栈并没有占用1M的实际内存 。并且可能每个线程的分支不一样,导致栈上的增长不同

大部分都是 150K左右。  2000个线程也就 300M左右,所以并没有出现OOM。

参考:

解Bug之路-记一次JVM堆外内存泄露Bug的查找 - 腾讯云开发者社区-腾讯云

Java优化知行合一(1):JVM占用内存区域和优化 - 知乎

Java 线程究竟占用多少内存_mob60475704c528的技术博客_51CTO博客

JVM中的本地内存追踪NMT(Native Memory Tracking)_阿达King的博客-CSDN博客_nativememorytracking

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值