原文链接: https://mp.weixin.qq.com/s/wA3pUemz5oWJX6Zp9HFIGA
原文排版比较好一点, 欢迎讨论.
若是有人问你正在运行的 Java 程序的堆占用了多少内存, 你一个命令就给出了答案; 若是有人问你正在运行的 Java 程序的线程栈使用了多少内存, 该怎么得到答案呢?
有人的 Java 程序遇到了 OOM, 程序崩溃之前, 只给出了这么一句关键遗言: "java.lang.OutOfMemoryError: unable to create new native thread". 从这一句关键的遗言中, 我们并不能完全推导出它崩溃之前到底发生了什么事情. Google 给出的答案里, 有的说是遇到的操作系统的 limits 限制, 有的说内存真的被用光了.
本文并不想去探讨这个 OOM 的具体原因, 而是去追问其中的一个分支问题: Java 的线程到底占用了多少物理内存?
首先, 根据用途, 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 作为 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 多一些. 这才是真正的结果.
这其实就是虚拟内存和真实物理内存差异的原因. 若 Xss 要求是 1M, 那么每个线程会申请 1M 的虚拟内存, 可是大部分线程并不会使用这么多, 也就没必要占用这么多物理内存, 使用多少个页(匿名 page), 就提交多少个页. 若按照每页 4K 计算, 也就是平均 25 个页左右, 就满足了大部分线程的内存需求.
另外, 如果我们查看 IBM JDK 或 Eclipse OpenJ9 的文档, 我们可能看到另外 2 个启动的 flags, 分别是: -Xiss 和 -Xssi. 分别代表栈的 Initial Stack Size (初始值) 和 Stack Size Increment (渐增值). 所以我们之前讨论的 Xss 代表最大值.
1
guo4224 2021-02-04 13:12:02 +08:00
你这是什么牛逼 os
|
2
manecocomph OP @guo4224 一般的 Linux. 没看出来哪里牛逼...
|
3
liuhuan475 2021-02-04 14:46:10 +08:00
每个线程有预留的内存 用来创建新对象的 本地线程分配缓存(Thread Local Allocation Buffer,TLAB)
|
4
manecocomph OP @liuhuan475 说的对. TLAB 是在堆上年轻代的, PLAB 是堆上老年代的. 都不属于线程栈的空间. 有空写一个 TLAB/PLAB card marking 的文章.
|