程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 剖析使用ObjectOutputStream可能引起的內存洩漏

剖析使用ObjectOutputStream可能引起的內存洩漏

編輯:關於JAVA

使用 ObjectOutputStream 來進行對象序列化

相信大多數程序員在使用 Java 進行日常開發工作中,都曾經遇到需要把數據進行序列化的情況,比如寫入文件或者寫入 socket 流。Java 的類庫也提供了豐富工具類供我們使用,這其中就包括 ObjectOutputStream。此類允許我們將 Java 對象的基本數據類型和圖形寫入 OutputStream,在需要將 Java 對象進行序列化操作時,使用該類可以極大為我們提供便利。但是使用不當也會引起一些麻煩。

需要說明的是,在使用 ObjectOutputStream 向 OutputStream 流中寫入的時候會在流中添加寫入 Object 的信息。這樣的話在我們使用 ObjectOutputStream 編寫跨語言的 Socket 通信時會遇到問題。因為數據流中加入的是 Java 特有的信息,如 Class 類型以及成員變量的類型信息,只有使用 ObjectInputStream 才能解析這些特定的信息。

好了,通過上面的說明,我們現在對 ObjectOutputStream 有了一個大體的了解,接下來我們使用具體的代碼來看一下如果使用 ObjectOutputStream 來把對象序列化到對象中。

把 Java 對象序列化到文件中

首先我們有一個簡單的 Java 對象類:

清單 1. 要序列化的 java 對象

class MyObject implements Serializable {
   private static final long serialVersionUID = -9163423175612080544L;
   String str1;
   String str2;
}

接下來我們使用 ObjectOutputStream 把 MyObject 對象寫入文件:

清單 2. 寫入文件

FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
MyObject myObj = new MyObject();
myObj.str1 = "test1";
myObj.str2 = "test2";
oos.writeObject(myObj);
fos.close();

我們來看一下寫入文件的內容:

清單 3. 寫入文件內容

#sr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~
#xpt #test1t #test2

我們可以看到寫入的內容中包含了寫入類的類型以及成員變量信息,當然關於插入的內容,我們可以覆蓋 ObjectOutputStream 類的 writeStreamHeader() 方法來實現插入我們自定義的內容。當然如果這樣做的話,就必須對 ObjectInputStream 類進行重寫。

上面是一些題外話,下面回到正題,關於標題中提到的有內存洩漏的問題。為了更清晰直觀的說明該問題,我又寫了一個很簡單的測試,代碼如下:

清單 4. 多次寫入

FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

MyObject myObj = new MyObject();
myObj.str1 = "test1";
myObj.str2 = "test2";
for (int i = 0; i < 5; i++) {
   oos.writeObject(myObj);
}
fos.close();

我們再來看一下寫入的內容:

清單 5. 寫入 5 次

#sr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~ #xpt
#test1t #test2q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #q ~ #

我們可以看到多次寫入同一個類型的對象,那麼對象的類型信息是不會重復寫入的。那麼有人會說了,那是因為你寫入的對象每次的內容都是一樣的,接下來為了得到更清晰的測試結果,我們來讓每次寫入的內容不同。

清單 6. 多次寫入不同內容

FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

for (int i = 0; i < 5; i++) {
   MyObject myObj = new MyObject();
   myObj.str1 = "test1" + i;
   myObj.str2 = "test2" + i;
   oos.writeObject(myObj);
}
fos.close();

我們再來看一下寫入的結果:

清單 7. 寫入 5 次不同內容的對象

#sr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~ #xpt
#test10t #test20q ~ #sq ~ t #test11t #test21q ~ #sq ~ t #test12t #test22q ~ #sq ~ t
#test13t #test23q ~ #sq ~ t #test14t #test24q ~ #

測試小結

通過上面的測試就很容易的發現,我們雖然寫入了 5 次,但是不會每次寫入都會插入寫入對象和成員變量類型的信息,而是在第一次寫入的時候插入一些頭信息,以後再寫就不會再插入了。這實際是 Java 做的優化,通過該優化從而減少 socket 傳輸的開銷。
我想現在應該有人已經看出問題來了,它之所以可以這麼做優化,前提是持有 MyObject 的引用,也就是說,不會釋放掉 MyObject 的引用。如果你是長連接的方式(socket 中很常用),ObjectOutputStream 會一直持有你以前發送過的對象的引用,從而導致 JVM 在進行垃圾回收的時候不能回收之前發送的對象的實例,經過漫長時間的運行,最終導致內存溢出。這一點從我通過 Jprobe 跟蹤也得到了印證。

避免長連接的情況下出現內存溢出

下面我們來談談如何避免該問題,說到這裡我們就得提到 ObjectOutputStream 的 reset 方法了,JDK 文檔中是這麼解釋該方法的:

“重置將丟棄已寫入流中的所有對象的狀態。重新設置狀態,使其與新的 ObjectOutputStream 相同。將流中的當前點標記為 reset,相應的 ObjectInputStream 也將在這一點重置。以前寫入流中的對象不再被視為正位於流中。它們會再次被寫入流。”

就是說調用 reset 那麼就丟棄所持有對象的狀態(也就是釋放掉了對對象的引用),同時會在流中設置 reset 標識。

我們來把之前的代碼稍作修改,在進行一下測試來看看有什麼不同:

清單 8. 重置的方式多次寫入

FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

for (int i = 0; i < 5; i++) {
   MyObject myObj = new MyObject();
   myObj.str1 = "test1" + i;
   myObj.str2 = "test2" + i;
   oos.writeObject(myObj);
   oos.reset();
}
fos.close();

我們來看一下加入 reset 後寫入文件的內容:

清單 9. 重置的方式寫入的內容

#sr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~ #xpt
#test10t #test20q ~ #ysr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t
#Ljava/lang/String;L #str2q ~ #xpt #test11t #test21q ~ #ysr
#com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~ #xpt
#test12t #test22q ~ #ysr #com.travelsky.test.MyObject €喳 +?^`# #L #str1t
#Ljava/lang/String;L #str2q ~ #xpt #test13t #test23q ~ #ysr
#com.travelsky.test.MyObject €喳 +?^`# #L #str1t #Ljava/lang/String;L #str2q ~
#xpt #test14t #test24q ~ #y

這次跟之前不同的,每一次寫入都加入了頭信息且每一次末尾都加入了 y,我想這個標識應該就是 reset 標識,至於具體是什麼,我們沒必要深究了。

結論及一些建議

通過上面一系列的測試,我們大概對 Object 流有了一定了解,那麼具體到我們日常編碼中到底該不該調用 reset 呢,這個我想不能一概而論了。我們通過測試也看到了,在不調用 reset 的方式下,Java 的優化對於減輕 socket 開銷還是很可觀的,當然代價是有的,那就是直到你調用 reset 或者是關閉輸出流之前,對於發送過的對象的實例是不會釋放的。

如果你的程序需要很長時間的運行,建議你還是調用 reset 避免最後內存溢出程序崩潰,但是如果你又要長時間運行,且發送的消息量又很大,那麼調用 reset 無疑會增加開銷,那麼這個時候最好的做法我覺得是自己實現一套機制,定時的調用 reset 或者是定量,比如查看到內存已經漲到一個水平後調用一下,這樣既可以避免內存無限的增長下去,又可以減少不少 socket 通信的開銷。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved