動(dòng)手探究Java內(nèi)存泄露問(wèn)題
譯文在本系列教程中,將帶大家動(dòng)手探究Java內(nèi)存泄露之謎,并教授給讀者相關(guān)的分析方法。以下是一個(gè)案例。
最近有一個(gè)服務(wù)器,經(jīng)常運(yùn)行的時(shí)候就出現(xiàn)過(guò)載宕機(jī)的現(xiàn)象。重啟腳本和系統(tǒng)后,該個(gè)問(wèn)題還是會(huì)出現(xiàn)。盡管有大量的數(shù)據(jù)丟失,但因不是關(guān)鍵業(yè)務(wù),問(wèn)題并不嚴(yán)重。不過(guò)還是決定作進(jìn)一步的調(diào)查,來(lái)看下問(wèn)題到底出現(xiàn)在哪。首先注意到的是,服務(wù)器通過(guò)了所有的單元測(cè)試和完整的集成環(huán)境的測(cè)試。在測(cè)試環(huán)境下使用測(cè)試數(shù)據(jù)時(shí)運(yùn)行正常,那么為什么在生產(chǎn)環(huán)境中運(yùn)行會(huì)出現(xiàn)問(wèn)題呢?很容易會(huì)想到,也許是因?yàn)閷?shí)際運(yùn)行時(shí)的負(fù)載大于測(cè)試時(shí)的負(fù)載,甚至超過(guò)了設(shè)計(jì)的負(fù)荷,從而耗盡了資源。但是到底是什么資源,在哪里耗盡了呢?下面我們就研究這個(gè)問(wèn)題
為了演示這個(gè)問(wèn)題,首先要做的是編寫一些內(nèi)存泄露的代碼,將使用生產(chǎn)-消費(fèi)者模式去實(shí)現(xiàn),以便更好說(shuō)明問(wèn)題。
例子中,假定有這樣一個(gè)場(chǎng)景:假設(shè)你為一個(gè)證劵經(jīng)紀(jì)公司工作,這個(gè)公司將股票的銷售額和股份記錄在數(shù)據(jù)庫(kù)中。通過(guò)一個(gè)簡(jiǎn)單進(jìn)程獲取命令并將其存放在一個(gè)隊(duì)列中。另一個(gè)進(jìn)程從該隊(duì)列中讀取命令并將其寫入數(shù)據(jù)庫(kù)。命令的POJO對(duì)象十分簡(jiǎn)單,如下代碼所示:
- 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;
- //這里故意設(shè)置Order對(duì)象足夠大,以方便例子稍后在運(yùn)行的時(shí)候耗盡內(nèi)存
- 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;
- }
- }
這個(gè)POJO對(duì)象是Spring應(yīng)用的一部分,該應(yīng)用有三個(gè)主要的抽象類,當(dāng)Spring調(diào)用它們的start()方法的時(shí)候?qū)⒎謩e創(chuàng)建一個(gè)新的線程。
第一個(gè)抽象類是OrderFeed。run()方法將生成一系列隨機(jī)的Order對(duì)象,并將其放置在隊(duì)列中,然后它會(huì)睡眠一會(huì)兒,又再接著生成一個(gè)新的Order對(duì)象,代碼如下:
- 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調(diào)用,開始生產(chǎn)order對(duì)象
- */
- 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();
- }
- }
#p#
第二個(gè)類是OrderRecord,這個(gè)類負(fù)責(zé)從隊(duì)列中提取Order對(duì)象,并將它們寫入數(shù)據(jù)庫(kù)。問(wèn)題是,將Order對(duì)象寫入數(shù)據(jù)庫(kù)的耗時(shí)比產(chǎn)生Order對(duì)象的耗時(shí)要長(zhǎng)得多。為了演示,將在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();
- }
- }
- }
- /**
- * 模擬記錄到數(shù)據(jù)庫(kù)的方法,這里只是簡(jiǎn)單讓其睡眠一秒
- */
- public void recordOrder(Order order) throws InterruptedException {
- TimeUnit.SECONDS.sleep(1);
- }
- }
為了證明這個(gè)效果,特意增加了一個(gè)監(jiān)視類 OrderQueueMonitor ,這個(gè)類每隔幾秒就打印出隊(duì)列的大小,代碼如下:
- 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();
- }
- }
- }
- }
接下來(lái)配置Spring框架的相關(guān)配置文件如下:
- <?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>
接下來(lái)運(yùn)行這個(gè)Spring應(yīng)用,并且可以通過(guò)jConsole去監(jiān)控應(yīng)用的內(nèi)存情況,這需要作一些配置,配置如下:
- -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
如果你看看堆的使用量,你會(huì)發(fā)現(xiàn)隨著隊(duì)列的增大,堆的使用量逐漸增大,如下圖所示,你可能不會(huì)發(fā)現(xiàn)1KB的內(nèi)存泄露,但當(dāng)達(dá)到1GB的內(nèi)存溢出就很明顯了。所以,接下來(lái)要做的事情就是等待其溢出,然后進(jìn)行分析。
#p#
接下來(lái)我們來(lái)看下如何發(fā)現(xiàn)并解決這類問(wèn)題。在Java中,可以借助不少自帶的或第三方的工具幫助我們進(jìn)行相關(guān)的分析。
下面介紹分析程序內(nèi)存泄露問(wèn)題的三個(gè)步驟:
- 提取發(fā)生內(nèi)存泄露的服務(wù)器的轉(zhuǎn)儲(chǔ)文件。
- 用這個(gè)轉(zhuǎn)儲(chǔ)文件生成報(bào)告。
- 分析生成的報(bào)告。
有幾個(gè)工具能幫你生成堆轉(zhuǎn)儲(chǔ)文件,分別是:
- jconsole
- visualvm
- Eclipse Memory Analyser Tool(MAT)
用jconsole提取堆轉(zhuǎn)儲(chǔ)文件
使用jconsole連接到你的應(yīng)用:?jiǎn)螕鬗Beans選項(xiàng)卡打開com.sun.management包,點(diǎn)擊HotSpotDiagnostic,點(diǎn)擊Operations,然后選擇dumpHeap。這時(shí)你將會(huì)看到dumpHeap操作:它接受兩個(gè)參數(shù)p0和p1。在p0的編輯框內(nèi)輸入一個(gè)堆轉(zhuǎn)儲(chǔ)的文件名,然后按下DumpHeap按鈕就可以了。如下圖:
用jvisualvm提取堆轉(zhuǎn)儲(chǔ)文件
首先使用jvisual vm連接示例代碼,然后右鍵點(diǎn)擊應(yīng)用,在左側(cè)的“application”窗格中選擇“Heap Dump”。
注意:如果需要分析的發(fā)生內(nèi)存泄露的是在遠(yuǎn)程服務(wù)器上,那么jvisualvm將會(huì)把轉(zhuǎn)存出來(lái)的文件保存在遠(yuǎn)程機(jī)器(假設(shè)這是一臺(tái)unix機(jī)器)上的/tmp目錄下。
用MAT來(lái)提取堆轉(zhuǎn)儲(chǔ)文件
jconsole和jvisualvm本身就是JDK的一部分,而MAT或被稱作“內(nèi)存分析工具”,是一個(gè)基于eclipse的插件,可以從eclipse.org下載。
最新版本的MAT需要你在電腦上安裝JDk1.6。如果你用的是Java1.7版本也不用擔(dān)心,因?yàn)樗鼤?huì)自動(dòng)為你安裝1.6版本,并且不會(huì)和安裝好的1.7版本產(chǎn)生沖突。
使用MAT的時(shí)候,只需要點(diǎn)擊“Aquire Heap Dump”,然后按步驟操作就可以了,如下圖:
要注意的是,使用上面的三種方法,都需要配置遠(yuǎn)程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
何時(shí)提取堆轉(zhuǎn)存文件
那么在什么時(shí)候才應(yīng)該提取堆轉(zhuǎn)存文件呢?這需要耗費(fèi)點(diǎn)心思和碰下運(yùn)氣。如果過(guò)早提取了堆轉(zhuǎn)儲(chǔ)文件,那么將可能不能發(fā)現(xiàn)問(wèn)題癥結(jié)所在,因?yàn)樗鼈儽缓戏?,非泄露類的?shí)例屏蔽了。不過(guò)也不能等太久,因?yàn)樘崛《艳D(zhuǎn)儲(chǔ)文件也需要占用內(nèi)存,進(jìn)行提取的時(shí)候可能會(huì)導(dǎo)致應(yīng)用崩潰。
最好的辦法是將jconsole連接到應(yīng)用程序并監(jiān)控堆的占用情況,知道它何時(shí)在崩潰的邊緣。因?yàn)闆]有發(fā)生內(nèi)存泄露時(shí),三個(gè)堆部分指標(biāo)都是綠色的,這樣很容易就能監(jiān)控到,如下圖:
分析轉(zhuǎn)儲(chǔ)文件
現(xiàn)在輪到MAT派上用場(chǎng)了,因?yàn)樗旧砭褪窃O(shè)計(jì)用來(lái)分析堆轉(zhuǎn)儲(chǔ)文件的。要打開和分析一個(gè)堆轉(zhuǎn)儲(chǔ)文件,可以選擇File菜單的Heap Dump選項(xiàng)。選擇了要打開的文件后,將會(huì)看到如下三個(gè)選項(xiàng):
#p#
選擇Leak Suspect Report選項(xiàng)。在MAT運(yùn)行幾秒后,會(huì)生成如下圖的頁(yè)面:
如餅狀圖顯示:疑似有一處發(fā)生了內(nèi)存泄露。也許你會(huì)想,這樣的做法只有在代碼受到控制的情況下才可取。畢竟這只是個(gè)例子,這又能說(shuō)明什么呢?好吧,在這個(gè)例子里,所有的問(wèn)題都是淺然易見的;線程a占用了98.7MB內(nèi)存,其他線程用了1.5MB。在實(shí)際情況中,得到的圖表可能是上圖那樣。讓我們繼續(xù)探究,會(huì)得到如下圖:
如上圖所示,報(bào)告的下一部分告訴我們,有一個(gè)LinkedBlockQueue占用了98.46%的內(nèi)存。想要進(jìn)一步的探究,點(diǎn)擊Details>>就可以了,如下圖:
可以看到,問(wèn)題確實(shí)是出在我們的orderQueue上。這個(gè)隊(duì)列里存儲(chǔ)了所有生成的隨機(jī)生成的Order對(duì)象,并且可以被我們上篇博文里提到的三個(gè)線程OrderFeed、OrderRecord、OrderMonitor訪問(wèn)。
那么一切都清楚了,MAT告訴我們:示例代碼中有一個(gè)LinkedBlockQueue,這個(gè)隊(duì)列用盡了所有的內(nèi)存,從而導(dǎo)致了嚴(yán)重的問(wèn)題。不過(guò)我們不知道這個(gè)問(wèn)題為什么會(huì)產(chǎn)生,也不能指望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