• 线上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

    1. Native Memory Tracking:
    2. Total: reserved=3439080KB, committed=2228892KB
    3. - Java Heap (reserved=1468416KB, committed=1468416KB)
    4. (mmap: reserved=1468416KB, committed=1468416KB)
    5. - Class (reserved=1149482KB, committed=114346KB)
    6. (classes #18958)
    7. (malloc=2602KB #43119)
    8. (mmap: reserved=1146880KB, committed=111744KB)
    9. - Thread (reserved=238418KB, committed=238418KB)
    10. (thread #856)
    11. (stack: reserved=234588KB, committed=234588KB)
    12. (malloc=2827KB #4282)
    13. (arena=1003KB #1708)
    14. - Code (reserved=266377KB, committed=94193KB)
    15. (malloc=16777KB #19327)
    16. (mmap: reserved=249600KB, committed=77416KB)
    17. - GC (reserved=102043KB, committed=102043KB)
    18. (malloc=14783KB #36820)
    19. (mmap: reserved=87260KB, committed=87260KB)
    20. - Compiler (reserved=1974KB, committed=1974KB)
    21. (malloc=1844KB #3350)
    22. (arena=131KB #6)
    23. - Internal (reserved=179912KB, committed=179912KB)
    24. (malloc=179880KB #86296)
    25. (mmap: reserved=32KB, committed=32KB)
    26. - Symbol (reserved=22345KB, committed=22345KB)
    27. (malloc=19652KB #207060)
    28. (arena=2693KB #1)
    29. - Native Memory Tracking (reserved=6934KB, committed=6934KB)
    30. (malloc=536KB #7399)
    31. (tracking overhead=6398KB)
    32. - Arena Chunk (reserved=309KB, committed=309KB)
    33. (malloc=309KB)
    34. - Unknown (reserved=2868KB, committed=0KB)
    35. (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

  • 相关阅读:
    以任意位置中间元素翻转字符串:
    自学Python 37 使用File操作文件
    4.cmake-更好的hello-world
    Python从零到一构建项目
    9 万字 208 道 Java 经典面试题总结 (附答案), 看到就是赚到
    Jmeter(五):json提取器元件及jsonpath介绍,响应断言元件
    flink-cdc同步mysql数据到elasticsearch
    一文搞定 Spring事务
    机器学习笔记--数学库
    threejs官方demo学习(1):animation
  • 原文地址:https://blog.csdn.net/oschina_40730821/article/details/126511949