Java 应用程序中往往会包含数百(有时是数千)个线程。此类线程会有大部分处于 WAITING、TIMED_WAITING(即:休眠)状态,只有一小部分线程会处于执行代码行的活动状态。因此,我们很想知道休眠线程是否会比活动线程所消耗的内存更少。
为了弄清楚这一问题的答案,我们进行一项小小的调查。调查的结果十分有趣,我们也将在本文中与您分享。
线程的栈中存储的是什么?
在读下去之前,您首先应该知道线程的栈中存储了哪些信息。可参阅这片文章来充分了解存储在线程栈中的信息。简而言之,这些信息会存储在线程的栈中:
- 方法中创建的局部变量。
- 线程当前正在执行的代码路径。
研究
为了便于研究的进行,我们编写了两个简单的程序。让我们来看看这两个程序及其性能特点。
1. 带有空栈帧的线程
我们编写了一个会创建 1000 个线程的简单 Java 程序。程序的所有线程栈帧都会几乎为空,这样就不会消耗任何内存了。
public class EmptyStackFrameProgram {
public void start() {
// 创建 1000 个线程
for (int counter = 0; counter < 1000; ++counter) {
new EmptyStackFrameThread().start();
}
}
}
public class EmptyStackFrameThread extends Thread {
public void run() {
try {
// 永久休眠
while (true) {
Thread.sleep(10000);
}
} catch (Exception e) {
}
}
}
此 Java 程序在“EmptyStackFrameProgram”中创建了 1000 个线程。所有“EmptyStackFrameProgram”线程都处于无限期的休眠中,所以其不会进行什么操作。因此,这些线程的栈帧几乎都是空的,因为其没有执行任何新的代码行或创建任何新的局部变量。
备注:我们将线程置于无限期的休眠状态,这样线程就不会消失。这对于我们研究其内存使用情况至关重要。
2. 具有已加载栈帧的线程
这里是另一个会创建 1000 个线程的简单 Java 程序。程序中的所有线程都会在其栈帧中完全加载数据。这样线程就会比之前的程序占用更多内存了。
public class FullStackFrameProgram {
public void start() {
// 创建 1000 个完全填充数据的线程
for (int counter = 0; counter < 1000; ++counter) {
new FullStackFrameThread().start();
}
}
}
public class FullStackFrameThread extends Thread {
public void run() {
try {
int x = 0;
simpleMethod(x);
} catch (Exception e) {}
}
/**
* 重复 10,000 次然后休眠。这样栈就会被填满。
*
* @param counter
* @throws Exception
*/
private void simpleMethod(int x) throws Exception {
// 创建局部变量以填充栈。
float y = 1.2f * x;
double z = 1.289898d * x;
// 循环 10,000 次迭代以填充栈。
if (x < 10000) {
simpleMethod(++x);
}
// 10,000 次迭代永久休眠
while (true) {
Thread.sleep(10000);
}
}
}
此 Java 程序在“FullStackFrameProgram”中创建了 1000 个线程。所有“FullStackFrameThread”线程都会重复调用“simpleMethod(int counter)”10,000次。调用 10,000 次后,线程将进入无限期的休眠状态。由于线程调用了“simpleMethod(int counter)”10,000次,所以每个线程都会有 10,000 个栈帧,且每个线程都会被局部变量“x”、“y”、“z”填充。

图:EmptyStackFrameThread 与 FullStackFrameThread 栈
以上是 EmptyStackFrameThread 栈与 FullStackFrameThread 栈的可视化图形。您应该能注意到,“EmptyStackFrameThread”中仅包含 2 个栈帧。另一边,“FullStackFrameThread”中则包含 10,000+ 栈帧。除此之外,“FullStackFrameThread”中的每个栈帧都会包含局部变量“x”、“y”、“z”。这就使得“FullStackFrameThread”栈完全加载。因此,大家应该会认为“FullStackFrameThread”栈将消耗更多内存。
内存消耗
在执行上述两个程序时我们采用了以下设置:
a.将线程栈内存大小配置为 2 MB(即:向两个程序传递 -Xss2m JVM 参数)。
b.使用了 OpenJDK 1.8.0_265, 64-Bit 服务器 VM
c.两个程序同时在 AWS ‘t3a.medium’ EC2 实例上运行。
您可在下方看到系统监控工具“top”报告的程序内存消耗情况。

图:两个程序的内存消耗相同
可以看到,两个程序所消耗的内存都是 4686 MB。这表明两个程序的线程都消耗了相同的内存,即使是 FullStackFrameThread 非常活跃,EmptyStackFrameThread 几乎休眠。
为了验证这一理论,我们使用了 JVM 根本原因分析工具 yCrash 来进一步对两个程序进行分析。下面是由 yCrash 工具生成的线程分析报告。

图:yCrash工具报告 EmptyStackFrameThread 栈几乎为空

图:yCrash工具报告 FullStackFrameThread 栈几乎全满
yCrash 工具还明确指出,EmptyStackFrameProgram 包含 1000 个线程和 2 个栈帧,FullStackFrameProgram 则包含 1000 个线程和 10,000 个栈帧。
结论
调查清楚表明,内存是在线程创建时分配的,而不是根据线程的运行时需求分配的。非常活跃的线程和几乎休眠的线程所消耗的内存几乎相同。现代 Java 应用程序往往会创建数百(有时是数千)个线程。但其中的大部分线程都处于 WAITING 或 TIMED_WAITING 状态,不会做什么工作。考虑到线程会在创建时就占用最大的分配内存量,作为程序开发人员,您可以执行以下操作来优化程序的内存消耗:
- 仅为程序创建*必要的*线程。
- 试着确定程序线程的最佳栈大小(即:-Xss)。假设您将线程的栈大小(即:-Xss)配置为 2 MB,并且在运行时如果程序仅使用了 512 KB,那么程序中的每个线程都会浪费 1.5 MB 的内存。如果程序中有 500 个线程,则每个 JVM 实例都将浪费 750 MB(即:500 个线程 x 1.5 MB)的内存,这对于当今的云计算时代而言不“便宜”。
您可使用 yCrash 等工具来了解有多少线程处于活动状态,有多少线程处于休眠状态。同时此类工具还可以告诉您各个线程的栈使用情况。根据这些信息,您就能为程序确定最优线程数和线程栈大小。
Leave a Reply