應用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雖然有自動回收管理內存的功能,但內存泄漏也是不容忽視,它往往是破壞系統穩定性的重要因素。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章