動手探究Java內存泄露問題

在本系列教程中,將帶你們動手探究Java內存泄露之謎,並教授給讀者相關的分析方法。如下是一個案例。 html

最近有一個服務器,常常運行的時候就出現過載宕機的現象。重啓腳本和系統後,該個問題仍是會出現。儘管有大量的數據丟失,但因不是關鍵業務,問題並 不嚴重。不過仍是決定做進一步的調查,來看下問題到底出如今哪。首先注意到的是,服務器經過了全部的單元測試和完整的集成環境的測試。在測試環境下使用測 試數據時運行正常,那麼爲何在生產環境中運行會出現問題呢?很容易會想到,也許是由於實際運行時的負載大於測試時的負載,甚至超過了設計的負荷,從而耗 盡了資源。可是究竟是什麼資源,在哪裏耗盡了呢?下面咱們就研究這個問題 java

爲了演示這個問題,首先要作的是編寫一些內存泄露的代碼,將使用生產-消費者模式去實現,以便更好說明問題。 git

例子中,假定有這樣一個場景:假設你爲一個證劵經紀公司工做,這個公司將股票的銷售額和股份記錄在數據庫中。經過一個簡單進程獲取命令並將其存放在一個隊列中。另外一個進程從該隊列中讀取命令並將其寫入數據庫。命令的POJO對象十分簡單,以下代碼所示:
  github

 public class Order { 
  
  private final int id; 
  
  private final String code; 
  
  private final int amount; 
  
  private final double price; 
  
  private final long time; 
  
  private final long[] padding; 
  
  /** 
   * @param id 
   *            The order id 
   * @param code 
   *            The stock code 
   * @param amount 
   *            the number of shares 
   * @param price 
   *            the price of the share 
   * @param time 
   *            the transaction time 
   */ 
  public Order(int id, String code, int amount, double price, long time) { 
    super(); 
    this.id = id; 
    this.code = code; 
    this.amount = amount; 
    this.price = price; 
    this.time = time; 
    
    //這裏故意設置Order對象足夠大,以方便例子稍後在運行的時候耗盡內存 
    this.padding = new long[3000]; 
    Arrays.fill(padding, 0, padding.length - 1, -2); 
  } 
  
  public int getId() { 
    return id; 
  } 
  
  public String getCode() { 
    return code; 
  } 
  
  public int getAmount() { 
    return amount; 
  } 
  
  public double getPrice() { 
    return price; 
  } 
  
  public long getTime() { 
    return time; 
  } 
  
} 

這個POJO對象是Spring應用的一部分,該應用有三個主要的抽象類,當Spring調用它們的start()方法的時候將分別建立一個新的線程。 spring

第一個抽象類是OrderFeed。run()方法將生成一系列隨機的Order對象,並將其放置在隊列中,而後它會睡眠一下子,又再接着生成一個新的Order對象,代碼以下: 數據庫

public class OrderFeed implements Runnable { 
 
 private static Random rand = new Random(); 
 
 private static int id = 0; 
 
 private final BlockingQueue<Order> orderQueue; 
 
 public OrderFeed(BlockingQueue<Order> orderQueue) { 
   this.orderQueue = orderQueue; 
 } 
 
 /** 
  *在加載Context上下文後由Spring調用,開始生產order對象 
  */ 
 public void start() { 
 
   Thread thread = new Thread(this, "Order producer"); 
   thread.start(); 
 } 
 
  @Override 
 public void run() { 
 
   while (true) { 
     Order order = createOrder(); 
     orderQueue.add(order); 
     sleep(); 
   } 
 } 
 
 private Order createOrder() { 
 
   final String[] stocks = { "BLND.L", "DGE.L", "MKS.L", "PSON.L", "RIO.L", "PRU.L", 
       "LSE.L", "WMH.L" }; 
   int next = rand.nextInt(stocks.length); 
   long now = System.currentTimeMillis(); 
 
   Order order = new Order(++id, stocks[next], next * 100, next * 10, now); 
   return order; 
 } 
 
 private void sleep() { 
   try { 
     TimeUnit.MILLISECONDS.sleep(100); 
   } catch (InterruptedException e) { 
     e.printStackTrace(); 
   } 
 }

第二個類是OrderRecord,這個類負責從隊列中提取Order對象,並將它們寫入數據庫。問題是,將Order對象寫入數據庫的耗時比產生Order對象的耗時要長得多。爲了演示,將在recordOrder()方法中讓其睡眠1秒。 服務器

public class OrderRecord implements Runnable { 
 
  private final BlockingQueue<Order> orderQueue; 
 
  public OrderRecord(BlockingQueue<Order> orderQueue) { 
    this.orderQueue = orderQueue; 
  } 
 
  public void start() { 
 
    Thread thread = new Thread(this, "Order Recorder"); 
    thread.start(); 
  } 
 
  @Override 
  public void run() { 
 
    while (true) { 
 
      try { 
        Order order = orderQueue.take(); 
        recordOrder(order); 
      } catch (InterruptedException e) { 
        e.printStackTrace(); 
      } 
    } 
 
  } 
 
  /** 
   * 模擬記錄到數據庫的方法,這裏只是簡單讓其睡眠一秒  
   */ 
  public void recordOrder(Order order) throws InterruptedException { 
    TimeUnit.SECONDS.sleep(1); 
  } 
 
}

爲了證實這個效果,特地增長了一個監視類 OrderQueueMonitor ,這個類每隔幾秒就打印出隊列的大小,代碼以下: app

public class OrderQueueMonitor implements Runnable { 
 
  private final BlockingQueue<Order> orderQueue; 
 
  public OrderQueueMonitor(BlockingQueue<Order> orderQueue) { 
    this.orderQueue = orderQueue; 
  } 
 
  public void start() { 
 
    Thread thread = new Thread(this, "Order Queue Monitor"); 
    thread.start(); 
  } 
 
  @Override 
  public void run() { 
 
    while (true) { 
 
      try { 
        TimeUnit.SECONDS.sleep(2); 
        int size = orderQueue.size(); 
        System.out.println("Queue size is:" + size); 
      } catch (InterruptedException e) { 
        e.printStackTrace(); 
      } 
    } 
  } 
 
}

接下來配置Spring框架的相關配置文件以下: 框架

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
xmlns:p="http://www.springframework.org/schema/p" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:context="http://www.springframework.org/schema/context" 
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd" 
default-init-method="start" 
default-destroy-method="destroy"> 
  
<bean id="theQueue" class="java.util.concurrent.LinkedBlockingQueue"/>  
<bean id="orderProducer"> 
<constructor-arg ref="theQueue"/> 
</bean> 
  
<bean id="OrderRecorder"> 
<constructor-arg ref="theQueue"/> 
</bean> 
  
<bean id="QueueMonitor"> 
<constructor-arg ref="theQueue"/> 
</bean> 
  
</beans>

接下來運行這個Spring應用,而且能夠經過jConsole去監控應用的內存狀況,這須要做一些配置,配置以下: dom

-Dcom.sun.management.jmxremote  
-Dcom.sun.management.jmxremote.port=9010  
-Dcom.sun.management.jmxremote.local.only=false  
-Dcom.sun.management.jmxremote.authenticate=false  
-Dcom.sun.management.jmxremote.ssl=false

若是你看看堆的使用量,你會發現隨着隊列的增大,堆的使用量逐漸增大,以下圖所示,你可能不會發現1KB的內存泄露,但當達到1GB的內存溢出就很明顯了。因此,接下來要作的事情就是等待其溢出,而後進行分析。

接下來咱們來看下如何發現並解決這類問題。在Java中,能夠藉助很多自帶的或第三方的工具幫助咱們進行相關的分析。

下面介紹分析程序內存泄露問題的三個步驟:

  1. 提取發生內存泄露的服務器的轉儲文件。
  2. 用這個轉儲文件生成報告。
  3. 分析生成的報告。

有幾個工具能幫你生成堆轉儲文件,分別是:

  • jconsole
  •  visualvm
  • Eclipse Memory Analyser Tool(MAT)

用jconsole提取堆轉儲文件

使用jconsole鏈接到你的應用:單擊MBeans選項卡打開com.sun.management包,點擊 HotSpotDiagnostic,點擊Operations,而後選擇dumpHeap。這時你將會看到dumpHeap操做:它接受兩個參數p0和 p1。在p0的編輯框內輸入一個堆轉儲的文件名,而後按下DumpHeap按鈕就能夠了。以下圖:

用jvisualvm提取堆轉儲文件

首先使用jvisual vm鏈接示例代碼,而後右鍵點擊應用,在左側的「application」窗格中選擇「Heap Dump」。

注意:若是須要分析的發生內存泄露的是在遠程服務器上,那麼jvisualvm將會把轉存出來的文件保存在遠程機器(假設這是一臺unix機器)上的/tmp目錄下。

用MAT來提取堆轉儲文件

jconsole和jvisualvm自己就是JDK的一部分,而MAT或被稱做「內存分析工具」,是一個基於eclipse的插件,能夠從eclipse.org下載。

最新版本的MAT須要你在電腦上安裝JDk1.6。若是你用的是Java1.7版本也不用擔憂,由於它會自動爲你安裝1.6版本,而且不會和安裝好的1.7版本產生衝突。

使用MAT的時候,只須要點擊「Aquire Heap Dump」,而後按步驟操做就能夠了,以下圖:

要注意的是,使用上面的三種方法,都須要配置遠程JMX鏈接以下:

-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=9010 
-Dcom.sun.management.jmxremote.local.only=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false

什麼時候提取堆轉存文件

那麼在何時才應該提取堆轉存文件呢?這須要耗費點心思和碰下運氣。若是過早提取了堆轉儲文件,那麼將可能不能發現問題癥結所在,由於它們被合法,非泄露類的實例屏蔽了。不過也不能等過久,由於提取堆轉儲文件也須要佔用內存,進行提取的時候可能會致使應用崩潰。

最好的辦法是將jconsole鏈接到應用程序並監控堆的佔用狀況,知道它什麼時候在崩潰的邊緣。由於沒有發生內存泄露時,三個堆部分指標都是綠色的,這樣很容易就能監控到,以下圖:

分析轉儲文件

如今輪到MAT派上用場了,由於它自己就是設計用來分析堆轉儲文件的。要打開和分析一個堆轉儲文件,能夠選擇File菜單的Heap Dump選項。選擇了要打開的文件後,將會看到以下三個選項:

選擇Leak Suspect Report選項。在MAT運行幾秒後,會生成以下圖的頁面:

如餅狀圖顯示:疑似有一處發生了內存泄露。也許你會想,這樣的作法只有在代碼受到控制的狀況下才可取。畢竟這只是個例子,這又能說明什麼呢?好吧, 在這個例子裏,全部的問題都是淺然易見的;線程a佔用了98.7MB內存,其餘線程用了1.5MB。在實際狀況中,獲得的圖表多是上圖那樣。讓咱們繼續 探究,會獲得以下圖:

如上圖所示,報告的下一部分告訴咱們,有一個LinkedBlockQueue佔用了98.46%的內存。想要進一步的探究,點擊Details>>就能夠了,以下圖:

能夠看到,問題確實是出在咱們的orderQueue上。這個隊列裏存儲了全部生成的隨機生成的Order對象,而且能夠被咱們上篇博文裏提到的三個線程OrderFeed、OrderRecord、OrderMonitor訪問。

那麼一切都清楚了,MAT告訴咱們:示例代碼中有一個LinkedBlockQueue,這個隊列用盡了全部的內存,從而致使了嚴重的問題。不過咱們不知道這個問題爲何會產生,也不能期望MAT告訴咱們。

本文代碼能夠在:https://github.com/roghughe/captaindebug/tree/master/producer-consumer中下載。

原文連接:http://www.javacodegeeks.com/2013/12/investigating-memory-leaks-part-1-writing-leaky-code.html

相關文章
相關標籤/搜索