Java线程:它们的内存效率高吗?
Java应用程序倾向于包含数百个(有时是数千个)线程。这些线程中的大多数处于WAITING或TIMED_WAITING(即休眠)状态,而只有一小部分正在主动执行代码行。因此,我们很想知道休眠线程是否比活动线程消耗更少的内存。
为了弄清楚这个问题的答案,我进行了一项小型研究。
线程堆栈中存储了什么?
在继续阅读之前,您应该首先知道线程堆栈中存储了哪些信息。要完整了解线程堆栈中存储的信息。简而言之,以下内容存储在线程的堆栈中:
1,在方法中创建的局部变量。
2,线程当前正在执行的代码路径。
为了方便我们的学习,我们编写了两个简单的程序。让我们回顾这两个程序及其性能特征。
1、带有空堆栈框架的线程
创建了一个简单的Java程序,它将创建1000个线程。该程序中的所有线程的堆栈帧几乎都为空,因此不必消耗任何内存。
public class EmptyStackFrameProgram {
public void start() {
// Create 1000 threads
for (int counter = 0; counter < 1000; ++counter) {
new EmptyStackFrameThread().start();
}
}
}
public class EmptyStackFrameThread extends Thread {
public void run() {
try {
// Just sleep forever
while (true) {
Thread.sleep(10000);
}
} catch (Exception e) {
}
}
}
在此Java程序中,EmptyStackFrameProgram该类中创建了1000个线程。所有EmptyStackFrameThread线程都进入无限睡眠状态,并且它们什么也不做。这意味着它们的堆栈框架将几乎为空,因为它们没有执行任何新的代码行或创建任何新的局部变量。
注意: 我们将线程置于无限睡眠状态,以便它们不会消失,这对于研究其内存使用情况至关重要。
2、带有已加载堆栈框架的线程
这是另一个简单的Java程序,它将创建1000个线程。该程序中的所有线程都将在堆栈帧中完全加载数据,因此它们将比早期程序消耗更多的内存。
public class FullStackFrameProgram {
public void start() {
// Create 1000 threads with full stack
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) {
}
}
/**
* Loop for 10,000 times and then sleep. So that stack will be filled up.
*
* @param counter
* @throws Exception
*/
private void simpleMethod(int x) throws Exception {
// Creating local variables to fill up the stack.
float y = 1.2f * x;
double z = 1.289898d * x;
// Looping for 10,000 iterations to fill up the stack.
if (x < 10000) {
simpleMethod(++x);
}
// After 10,000 iterations, sleep forever
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包含10,000+个堆栈帧。除此之外,的每个堆栈帧都FullStackFrameThread将包含局部变量x,y,z。这将导致FullStackFrameThread堆栈已满载。因此,人们会期望FullStackFrameThread堆栈消耗更多的内存。
内存消耗
我们使用以下设置执行了以上两个程序:
1,将线程的堆栈大小配置为2 MB(即,将-Xss2m JVM参数传递给两个程序)。
2,使用OpenJDK 1.8.0_265、64位服务器VM。
3,在AWS’t3a.medium’EC2实例上同时运行两个程序。
在下面,您可以查看系统监视工具“ top”报告的程序内存消耗。
会注意到这两个程序正消耗4686 MB的内存。这表明两个程序线程都消耗相同的内存量,即使该程序线程处于FullStackFrameThread活动状态,而EmptyStackFrameThread几乎处于休眠状态。 为了验证该理论,我们使用JVM根本原因分析工具yCrash进一步分析了这两个程序。以下是yCrash工具生成的线程分析报告
yCrash还清楚地指出EmptyStackFrameProgram包含两个堆栈框架的FullStackFrameProgram1,000个线程,而包含10,000堆栈框架的1,000个线程。
结论
这项研究清楚地表明,内存是在创建时分配给线程的,而不是根据线程的运行时需求分配的。超级工作线程和几乎休眠的线程都消耗相同数量的内存。现代Java应用程序倾向于创建数百个(有时数千个)线程。但是这些线程大多数都处于WAITING或TIMED_WAITING状态,并且什么也不做。鉴于线程在创建时会立即占用分配的最大内存量,作为应用程序开发人员,您可以执行以下操作来优化应用程序的内存消耗:
1.仅为您的应用程序创建必要线程。
2.尝试为您的应用程序线程提供最佳的堆栈大小(即-Xss)。因此,如果将线程的堆栈大小(即-Xss)配置为2 MB,并且在运行时您的应用程序仅使用512 KB,则将为应用程序中的每个线程浪费1.5 MB的内存。如果您的应用程序有500个线程,则每个JVM实例将浪费750 MB(即500个线程x 1.5 MB)的内存,这在现代云计算时代并不便宜。
您可以使用诸如yCrash工具来告诉您有多少个活动线程以及处于休眠状态的线程。它还可以告诉您每个线程的堆栈有多深。根据这些报告,您可以为应用程序提供最佳的线程数和线程的堆栈大小。
作者:令狐义卓
链接:https://juejin.cn/post/6936043006566858782
来源:掘金