動手探究Java內存泄露問題
在本系列教程中,將帶大家動手探究Java內存泄露之謎,并教授給讀者相關的分析方法。以下是一個案例。
最近有一個服務器,經常運行的時候就出現過載宕機的現象。重啟腳本和系統后,該個問題還是會出現。盡管有大量的數據丟失,但因不是關鍵業務,問題并 不嚴重。不過還是決定作進一步的調查,來看下問題到底出現在哪。首先注意到的是,服務器通過了所有的單元測試和完整的集成環境的測試。在測試環境下使用測試數據時運行正常,那么為什么在生產環境中運行會出現問題呢?很容易會想到,也許是因為實際運行時的負載大于測試時的負載,甚至超過了設計的負荷,從而耗 盡了資源。但是到底是什么資源,在哪里耗盡了呢?下面我們就研究這個問題
為了演示這個問題,首先要做的是編寫一些內存泄露的代碼,將使用生產-消費者模式去實現,以便更好說明問題。
例子中,假定有這樣一個場景:假設你為一個證劵經紀公司工作,這個公司將股票的銷售額和股份記錄在數據庫中。通過一個簡單進程獲取命令并將其存放在一個隊列中。另一個進程從該隊列中讀取命令并將其寫入數據庫。命令的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中,可以借助不少自帶的或第三方的工具幫助我們進行相關的分析。
下面介紹分析程序內存泄露問題的三個步驟:
- 提取發生內存泄露的服務器的轉儲文件。
- 用這個轉儲文件生成報告。
- 分析生成的報告。
有幾個工具能幫你生成堆轉儲文件,分別是:
- 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