V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
manecocomph
V2EX  ›  Java

Java 的线程到底占用了多少内存?

  •  
  •   manecocomph · 2021-02-04 12:51:56 +08:00 · 2500 次点击
    这是一个创建于 1422 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接: https://mp.weixin.qq.com/s/wA3pUemz5oWJX6Zp9HFIGA

    原文排版比较好一点, 欢迎讨论.

    若是有人问你正在运行的 Java 程序的堆占用了多少内存, 你一个命令就给出了答案; 若是有人问你正在运行的 Java 程序的线程栈使用了多少内存, 该怎么得到答案呢?

    故事背景

    有人的 Java 程序遇到了 OOM, 程序崩溃之前, 只给出了这么一句关键遗言: "java.lang.OutOfMemoryError: unable to create new native thread". 从这一句关键的遗言中, 我们并不能完全推导出它崩溃之前到底发生了什么事情. Google 给出的答案里, 有的说是遇到的操作系统的 limits 限制, 有的说内存真的被用光了.

    本文并不想去探讨这个 OOM 的具体原因, 而是去追问其中的一个分支问题: Java 的线程到底占用了多少物理内存?

    有关 Xss 和 ThreadStackSize

    首先, 根据用途, Java 的内存使用可以分为: 堆区 (年轻代, 老年代, 元数据区) , 栈区, 编译后的代码区, 编译器代码区, GC 管理程序区, JVM 自身的代码区 和 符号区等. 一般情况下, 占大头的是堆区. 栈区根据线程数可能大小不一.

    在 JVM 的 flags 里面, 有 2 个参数是与栈大小相关的. 分别是 -Xss 和 -XX:ThreadStackSize. 我们可以认为 2 个 flags 其实代表一个意思, 只是一个是简写, 一个是全量写法. 根据官方文档, 它设置的是一个线程 Stack 的大小. 若是不设置, 根据操作系统的不同, 有不同的默认值. 如 64 位的 Linux 下, 默认是 1MB.

    线程栈的大小

    是不是根据 Xss 的值乘以线程数, 就得到了所有线程栈占用的物理内存大小呢? 于是, 我找到一个基于 JDK 8, 正在运行, 并且线程数目巨大(其实是有线程泄漏的 bug)的程序. 使用 NMT 得到了下面的结果:

    这代表什么呢? 当时的活跃线程数大概是 13991 个, 栈所声明要使用的内存数 14454495KB, 实际提交 (committed) 的 内存约 13.71G. 这是一个声明了使用 8 核 16G 内存的 container 进程, 而这个 Java 进程的栈却告诉我们: 栈使用了 13.71G 内存, 加上堆占用的 8 个多 G, 该进程已经提交了 24G 多的内存占用 (没开 SWAP). 这明显已经矛盾.

    同时, 我们通过 ps 命令可以看到, 该进程占用大约 13G 的 RSS 内存. Container 设置的内存最大值是 16G, 当前还有 500M 的空余. (该 Container 是一个 fat Container, 里面还有其它辅助进程). 这是一个合理的情形.

    数据对不上, 至少有一个人在说谎.

    NMT 的 bug

    NMT 作为 JVM 提供的一个追踪原生内存使用量的工具, 最早主要用来追查内存泄漏的. 主要的内存泄漏大都集中在堆区. 对于栈区, 早期的 NMT 的计算方式主要以线程数乘以每个线程可以使用的最大内存量( Xss)得到的. 所以, 直到 2018 年, 有人报了这个问题, 才有了这个修复: https://bugs.openjdk.java.net/browse/JDK-8191369.

    但是这个修复主要修了 Linux 和 Windows 版本. 所以即便我在 MAC 上下载了 JDK 15 的 release 版本, 依旧有这个数据问题.

    修复后的结果

    我找了一个 Linux 上基于 JDK 11 的程序, 使用 NMT 之后, 终于看到了想要的结果:

    这里大约有 310 个运行中线程, 使用了大概 38M, 平均使用 100K 多一些. 这才是真正的结果.

    为什么保留 1M, 却只使用了 100K?

    这其实就是虚拟内存和真实物理内存差异的原因. 若 Xss 要求是 1M, 那么每个线程会申请 1M 的虚拟内存, 可是大部分线程并不会使用这么多, 也就没必要占用这么多物理内存, 使用多少个页(匿名 page), 就提交多少个页. 若按照每页 4K 计算, 也就是平均 25 个页左右, 就满足了大部分线程的内存需求.

    另外, 如果我们查看 IBM JDK 或 Eclipse OpenJ9 的文档, 我们可能看到另外 2 个启动的 flags, 分别是: -Xiss 和 -Xssi. 分别代表栈的 Initial Stack Size (初始值) 和 Stack Size Increment (渐增值). 所以我们之前讨论的 Xss 代表最大值.

    4 条回复    2021-02-04 16:52:18 +08:00
    guo4224
        1
    guo4224  
       2021-02-04 13:12:02 +08:00
    你这是什么牛逼 os
    manecocomph
        2
    manecocomph  
    OP
       2021-02-04 13:53:20 +08:00
    @guo4224 一般的 Linux. 没看出来哪里牛逼...
    liuhuan475
        3
    liuhuan475  
       2021-02-04 14:46:10 +08:00
    每个线程有预留的内存 用来创建新对象的 本地线程分配缓存(Thread Local Allocation Buffer,TLAB)
    manecocomph
        4
    manecocomph  
    OP
       2021-02-04 16:52:18 +08:00
    @liuhuan475 说的对. TLAB 是在堆上年轻代的, PLAB 是堆上老年代的. 都不属于线程栈的空间. 有空写一个 TLAB/PLAB card marking 的文章.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3397 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 04:54 · PVG 12:54 · LAX 20:54 · JFK 23:54
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.