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

2021-02-04 12:51:56 +08:00
 manecocomph

原文链接: 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 代表最大值.

2474 次点击
所在节点    Java
4 条回复
guo4224
2021-02-04 13:12:02 +08:00
你这是什么牛逼 os
manecocomph
2021-02-04 13:53:20 +08:00
@guo4224 一般的 Linux. 没看出来哪里牛逼...
liuhuan475
2021-02-04 14:46:10 +08:00
每个线程有预留的内存 用来创建新对象的 本地线程分配缓存(Thread Local Allocation Buffer,TLAB)
manecocomph
2021-02-04 16:52:18 +08:00
@liuhuan475 说的对. TLAB 是在堆上年轻代的, PLAB 是堆上老年代的. 都不属于线程栈的空间. 有空写一个 TLAB/PLAB card marking 的文章.

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/751213

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX