動手探究Java記憶體泄露問題

來源:互聯網
上載者:User

標籤:

在本系列教程中,將帶大家動手探究Java記憶體泄露之謎,並教授給讀者相關的分析方法。以下是一個案例。

最近有一個伺服器,經常啟動並執行時候就出現過載宕機的現象。重啟指令碼和系統後,該個問題還是會出現。儘管有大量的資料丟失,但因不是關鍵業務,問題並 不嚴重。不過還是決定作進一步的調查,來看下問題到底出現在哪。首先注意到的是,伺服器通過了所有的單元測試和完整的Integration Environment的測試。在測試環境下使用測 試資料時運行正常,那麼為什麼在生產環境中運行會出現問題呢?很容易會想到,也許是因為實際運行時的負載大於測試時的負載,甚至超過了設計的負荷,從而耗 盡了資源。但是到底是什麼資源,在哪裡耗盡了呢?下面我們就研究這個問題

為了示範這個問題,首先要做的是編寫一些記憶體泄露的代碼,將使用生產-消費者模式去實現,以便更好說明問題。

例子中,假定有這樣一個情境:假設你為一個證劵經紀公司工作,這個公司將股票的銷售額和股份記錄在資料庫中。通過一個簡單進程擷取命令並將其存放在一個隊列中。另一個進程從該隊列中讀取命令並將其寫入資料庫。命令的POJO對象十分簡單,如下代碼所示:
 

 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()方法的時候將分別建立一個新的線程。

第一個抽象類別是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 ,這個類每隔幾秒就列印出隊列的大小,代碼如下:

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去監控應用的記憶體情況,這需要作一些配置,配置如下:

-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

動手探究Java記憶體泄露問題

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.