1. 程式人生 > >應用jacob元件造成的記憶體溢位解決方案(java.lang.OutOfMemoryError: Java heap space)

應用jacob元件造成的記憶體溢位解決方案(java.lang.OutOfMemoryError: Java heap space)

轉自(http://www.myexception.cn/ruby-rails/903889.html)

使用jacob元件造成的記憶體溢位解決方案(java.lang.OutOfMemoryError: Java heap space)

都說記憶體洩漏是C++的通病,記憶體溢位是Java的硬傷,這個頭疼的問題算是讓我給碰到了。我在做的這個功能涉及到修改word文件,因為微軟沒有公開word原始碼,所以直接用java流來讀取word的後果是讀出來的會是亂碼,經過查資料得知可以使用poi和jacob來操作word,jacob使用起來相對poi要方便很多,因此我選擇了jacob,Jacob 是Java-COM Bridge的縮寫,它在Java與微軟的COM元件之間構建一座橋樑。使用Jacob自帶的DLL動態連結庫,並通過JNI(Java Native Interface Java本地呼叫)的方式實現了在Java平臺上對COM程式的呼叫。因為dll檔案不能在linux上執行,而客戶端只和linux互動,所以還需要一個windows伺服器,這兩個伺服器不斷的互相下載word,下載的頻繁度最高連續達到十萬次,以下是伺服器之間的互動圖:

當功能實現了之後進行了一下測試,結果記憶體溢位了,於是就開始連查帶改弄了半個月,檢查開啟的流有沒有關閉,有沒有大量使用靜態變數,有沒有大量使用String進行字串拼接,遺憾的是沒有找出問題在哪裡(說明我寫的程式碼質量還是不錯的),也試圖增加jvm記憶體,但增加jvm記憶體只能治標而不能治本,不是可靠的辦法,經過大量查閱資料,得知com的執行緒回收不由java垃圾回收器進行處理,因此,每new一次jacob提供的類就要分配一定大小的記憶體給該操作,new出來的這個com物件在使用結束之後產生的垃圾java是無法回收的,new出來的物件越來越多,記憶體溢位就不可避免了,即使增加jvm記憶體也只是暫時的,遲早這些物件會把記憶體用完。既然java不能回收這些垃圾,那麼com元件也應該提供了回收垃圾的方法,最後得知是ComThread.InitSTA()和ComThread.Release()方法,這兩個方法其實就是初始化一個執行緒和結束這個執行緒,在建立com物件的時候初始化一個執行緒來執行這個物件,這個物件使用結束之後再結束執行緒,問題就這樣得到解決了,程式連續執行一兩天記憶體一直很平穩,弄了快一個月的問題終於解決了,以下是全部程式碼:

<pre name="code" class="java">/**
 * @fileName MSWordManager.java
 * @description 該類用於查詢word文件指定位置並將圖片插入
 * @date 2011-10-21
 * @time 
 * @author wst
 */
public class MSWordManager {
	private Logger log = Logger.getLogger(MSWordManager.class);
	
	// word文件
	private Dispatch doc;
	// word執行程式物件
	private ActiveXComponent word;
	// 所有word文件集合
	private Dispatch documents;
	// 選定的範圍或插入點
	private Dispatch selection;
	public static int instanceSize=3;//一個執行緒存放的MSWordManager數量

	public MSWordManager(int index) {
		if (word == null) {
			word = new ActiveXComponent("Word.Application");
			//為true表示word應用程式可見
			word.setProperty("Visible", new Variant(false));
		}
		if (documents == null){
			documents = word.getProperty("Documents").toDispatch();
		}
		if(index==0){
			ComThread.InitSTA();//初始化一個執行緒並放入記憶體中等待呼叫
		}
	}
	/**
	 * 開啟一個已經存在的文件
	 * @param docPath 要開啟的文件
	 * @param key 文字框的內容,根據該key獲取文字框當前位置
	 * @date 2011-12-9
	 * @author wst
	 */
	public void openDocumentAndGetSelection(String docPath, String key) {
		try{
			closeDocument();
			// 開啟文件
			doc = Dispatch.call(documents, "Open", docPath).toDispatch();
			// shapes集合
			Dispatch shapes = Dispatch.get(doc, "Shapes").toDispatch(); 
			// shape的個數
			String Count = Dispatch.get(shapes, "Count").toString(); 
			for (int i = 1; i <= Integer.parseInt(Count); i++) {
				// 取得一個shape
				Dispatch shape = Dispatch.call(shapes, "Item", new Variant(i)).toDispatch(); 
				// 從一個shape裡面獲取到文字框
				Dispatch textframe = Dispatch.get(shape, "TextFrame").toDispatch();
				boolean hasText = Dispatch.call(textframe, "HasText").toBoolean();
				if (hasText) {
					// 獲取該文字框物件
					Dispatch TextRange = Dispatch.get(textframe, "TextRange").toDispatch();
					// 獲取文字框中的字串
					String str = Dispatch.get(TextRange, "Text").toString();
					//獲取指定字元key所在的文字框的位置  
					if (str != null && !str.equals("") && str.indexOf(key) > -1) {
						//當前文字框的位置  
						selection = Dispatch.get(textframe, "TextRange").toDispatch();
						// 情況文字框內容
						Dispatch.put(selection, "Text", ""); 
						break;
					}
				}
			}
		}catch(Exception e){
			log.error(e);
			return;
		}
	}

	/**
	 * 在當前位置插入圖片
	 * @param imagePath 產生圖片的路徑
	 * @return 成功:true;失敗:false
	 */
	public boolean insertImage(String imagePath) {
		try{
			Dispatch.call(Dispatch.get(selection, "InLineShapes").toDispatch(),"AddPicture", imagePath);
		}catch(Exception e){
			log.error(e);
			return false;
		}
		return true;
	}

	//關閉文件
	public void closeDocument() {
		if (doc != null) {
			Dispatch.call(doc, "Close");
			doc = null;
		}
	}

	//關閉全部應用
	public void close(int index) {
		if (word != null) {
			Dispatch.call(word, "Quit");
			word = null;
		}
		selection = null;
		documents = null;
		if(index==instanceSize){
			//釋放佔用的記憶體空間,因為com的執行緒回收不由java的垃圾回收器處理
			ComThread.Release();
		}
	}
}


問題解決了,雖然寫的java程式沒有什麼問題,但是也學習到了一些如何防止記憶體溢位的知識,下面來看看我在網路找到的幾種常見的記憶體溢位以及如何檢測出記憶體溢位和出來辦法。

一、 幾種典型的記憶體洩漏

    我們知道了在Java中確實會存在記憶體洩漏,那麼就讓我們看一看幾種典型的洩漏,並找出他們發生的原因和解決方法。 
    1 全域性集合 
    在大型應用程式中存在各種各樣的全域性資料倉庫是很普遍的,比如一個JNDI-tree或者一個session table。在這些情況下,必須注意管理儲存庫的大小。必須有某種機制從儲存庫中移除不再需要的資料。 
    通常有很多不同的解決形式,其中最常用的是一種週期執行的清除作業。這個作業會驗證倉庫中的資料然後清除一切不需要的資料。 
    另一種管理儲存庫的方法是使用反向連結(referrer)計數。然後集合負責統計集合中每個入口的反向連結的數目。這要求反向連結告訴集合何時會退出入口。當反向連結數目為零時,該元素就可以從集合中移除了。 
    2 快取 
    快取一種用來快速查詢已經執行過的操作結果的資料結構。因此,如果一個操作執行需要比較多的資源並會多次被使用,通常做法是把常用的輸入資料的操作結果進行快取,以便在下次呼叫該操作時使用快取的資料。快取通常都是以動態方式實現的,如果快取設定不正確而大量使用快取的話則會出現記憶體溢位的後果,因此需要將所使用的記憶體容量與檢索資料的速度加以平衡。 
    常用的解決途徑是使用java.lang.ref.SoftReference類堅持將物件放入快取。這個方法可以保證當虛擬機器用完記憶體或者需要更多堆的時候,可以釋放這些物件的引用。 
    3 類裝載器 
    Java類裝載器的使用為記憶體洩漏提供了許多可乘之機。一般來說類裝載器都具有複雜結構,因為類裝載器不僅僅是隻與"常規"物件引用有關,同時也和物件內部的引用有關。比如資料變數,方法和各種類。這意味著只要存在對資料變數,方法,各種類和物件的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關聯,同時也可以和靜態資料變數關聯,那麼相當多的記憶體就可能發生洩漏。 

二、 如何檢測和處理記憶體洩漏 
    如何查詢引起記憶體洩漏的原因一般有兩個步驟:第一是安排有經驗的程式設計人員對程式碼進行走查和分析,找出記憶體洩漏發生的位置;第二是使用專門的記憶體洩漏測試工具進行測試。 
    第一個步驟在程式碼走查的工作中,可以安排對系統業務和開發語言工具比較熟悉的開發人員對應用的程式碼進行了交叉走查,儘量找出程式碼中存在的資料庫連線宣告和結果集未關閉、程式碼冗餘等故障程式碼。 
    第二個步驟就是檢測Java的記憶體洩漏。在這裡我們通常使用一些工具來檢查Java程式的記憶體洩漏問題。市場上已有幾種專業檢查Java記憶體洩漏的工具,它們的基本工作原理大同小異,都是通過監測Java程式執行時,所有物件的申請、釋放等動作,將記憶體管理的所有資訊進行統計、分析、視覺化。開發人員將根據這些資訊判斷程式是否有記憶體洩漏問題。這些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。 
    1 檢測記憶體洩漏的存在 
    這裡我們將簡單介紹我們在使用Optimizeit檢查的過程。通常在知道發生記憶體洩漏之後,第一步是要弄清楚洩漏了什麼資料和哪個類的物件引起了洩漏。 
    一般說來,一個正常的系統在其執行穩定後其記憶體的佔用量是基本穩定的,不應該是無限制的增長的。同樣,對任何一個類的物件的使用個數也有一個相對穩定的上限,不應該是持續增長的。根據這樣的基本假設,我們持續地觀察系統執行時使用的記憶體的大小和各例項的個數,如果記憶體的大小持續地增長,則說明系統存在記憶體洩漏,如果特定類的例項物件個數隨時間而增長(就是所謂的“增長率”),則說明這個類的例項可能存在洩漏情況。 
    另一方面通常發生記憶體洩漏的第一個跡象是:在應用程式中出現了OutOfMemoryError。在這種情況下,需要使用一些開銷較低的工具來監控和查詢記憶體洩漏。雖然OutOfMemoryError也有可能應用程式確實正在使用這麼多的記憶體;對於這種情況則可以增加JVM可用的堆的數量,或者對應用程式進行某種更改,使它使用較少的記憶體。 
    但是,在許多情況下,OutOfMemoryError都是記憶體洩漏的訊號。一種查明方法是不間斷地監控GC的活動,確定記憶體使用量是否隨著時間增加。如果確實如此,就可能發生了記憶體洩漏。

  2 處理記憶體洩漏的方法

  一旦知道確實發生了記憶體洩漏,就需要更專業的工具來查明為什麼會發生洩漏。JVM自己是不會告訴您的。這些專業工具從JVM獲得記憶體系統資訊的方法基本上有兩種:JVMTI和位元組碼技術(byte code instrumentation)。Java虛擬機器工具介面(Java Virtual Machine Tools Interface,JVMTI)及其前身Java虛擬機器監視程式介面(Java Virtual Machine Profiling Interface,JVMPI)是外部工具與JVM通訊並從JVM收集資訊的標準化介面。位元組碼技術是指使用探測器處理位元組碼以獲得工具所需的資訊的技術。 
    Optimizeit是Borland公司的產品,主要用於協助對軟體系統進行程式碼優化和故障診斷,其中的Optimizeit Profiler主要用於記憶體洩漏的分析。Profiler的堆檢視就是用來觀察系統執行使用的記憶體大小和各個類的例項分配的個數的。 
    首先,Profiler會進行趨勢分析,找出是哪個類的物件在洩漏。系統執行長時間後可以得到四個記憶體快照。對這四個記憶體快照進行綜合分析,如果每一次快照的記憶體使用都比上一次有增長,可以認定系統存在記憶體洩漏,找出在四個快照中例項個數都保持增長的類,這些類可以初步被認定為存在洩漏。通過資料收集和初步分析,可以得出初步結論:系統是否存在記憶體洩漏和哪些物件存在洩漏(被洩漏)。 
    接下來,看看有哪些其他的類與洩漏的類的物件相關聯。前面已經談到Java中的記憶體洩漏就是無用的物件保持,簡單地說就是因為編碼的錯誤導致了一條本來不應該存在的引用鏈的存在(從而導致了被引用的物件無法釋放),因此記憶體洩漏分析的任務就是找出這條多餘的引用鏈,並找到其形成的原因。檢視物件分配到哪裡是很有用的。同時只知道它們如何與其他物件相關聯(即哪些物件引用了它們)是不夠的,關於它們在何處建立的資訊也很有用。 
    最後,進一步研究單個物件,看看它們是如何互相關聯的。藉助於Profiler工具,應用程式中的程式碼可以在分配時進行動態新增,以建立堆疊跟蹤。也有可以對系統中所有物件分配進行動態的堆疊跟蹤。這些堆疊跟蹤可以在工具中進行累積和分析。對每個被洩漏的例項物件,必然存在一條從某個牽引物件出發到達該物件的引用鏈。處於堆疊空間的牽引物件在被從棧中彈出後就失去其牽引的能力,變為非牽引物件。因此,在長時間的執行後,被洩露的物件基本上都是被作為類的靜態變數的牽引物件牽引。 
    總而言之, Java雖然有自動回收管理記憶體的功能,但記憶體洩漏也是不容忽視,它往往是破壞系統穩定性的重要因素。