Java线程:它们的内存效率高吗?

290 阅读5分钟

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工具来告诉您有多少个活动线程以及处于休眠状态的线程。它还可以告诉您每个线程的堆栈有多深。根据这些报告,您可以为应用程序提供最佳的线程数和线程的堆栈大小。 

 更多Java基础技术学习和交流,可以加入我的十年Java学习园地  。