Java 的 ObjectOutputStream 和 ObjectInputStream 各自保留一個對已發送/已接收物件的參考的串列,就是這些參考,會阻止垃圾收集器對這些物件記憶體的釋放,
當新物件的數量不斷增長時(比方說在服務器中),最終將拋出"Java.lang.OutOfMemoryError",解決辦法就是使用 writeUnshared() 和 readUnshared() 方法來取代 writeObject() 和 readObject() 方法,
介紹
怎樣在 Java 中創建一個記憶體泄漏?這是一個很好地面試題,因為 Java 編程中由于底層的自動垃圾收集功能而無需擔心記憶體釋放問題,但在復雜的情況下你仍然會遭遇 Java.lang.OutOfMemoryError 問題,這里就描述了一種這樣的情況:由 java.io.ObjectInputStream 和 java.io.ObjectOutputStream 所實作的 Java 序列化機制所引起的記憶體泄漏,
原因
ObjectOutputStream 和 ObjectInputStream 都各自維護了一個對其已發送/已接收物件的參考表,所以當 ObjectOutputStream 重新發送某物件時,可以仍發送該物件的句柄,相應的 ObjectInputStream 則將接收到的句柄轉換為先前接收到的物件的參考,Java 檔案中這樣描述:
使用共享機制 (…) 對單個物件的多個參考進行編碼,
可以說,Java 的這一 feature 減少了帶寬和記憶體的使用量,但也僅適用于通過鏈接定期重新發送物件的程式中,但在通過鏈接發送新物件的情況下,此功能除了不必要的(不斷增長的)參考表以外沒有任何作用,
垃圾收集器的作業原理就是清理不再被參考的物件,但由于 ObjectInputStream 始終保持了對其所接收的每個物件的參考,它阻止了垃圾收集器對通過流所接收的任何物件的清理,就在諸如服務器之類的程式中,它們接收到越來越多的物件,最終記憶體將被耗盡,引發 OutOfMemoryError,
情況識別和分析
OOM 可能有很多種不同的原因所造成,為了能夠確定 OOM 是上文所討論的 ObjectInputStream 相關 feature 所造成的,我們首先得來分析該行程的 heap dump,有很多種辦法可以針對一個 Java 行程生成一個 heap dump,也有很多種辦法來分析這些 dump 檔案,我們可以通過將 JVM 命令列選項添加到行程執行命令列來生成 dump,還可以針對一個已經在運行中的程式來生成一個 dump,其中,通過添加命令列選項的方式可以在有 OOM 錯誤時生成 dump 檔案(當然,還有一些其他命令列選項通過信號控制方式來在任意時間生成 dump,不管程式狀態如何),命令列選項如下:
-XX:+HeapDumpOnOutOfMemoryError
在 OOM 拋出時,一個名為 java_pid.hprof 的檔案將會在該行程的執行目錄所生成,有 n 多 dump 分析程式可以對 dump 檔案進行分析,本文只對 jvm 原生自帶的 VisualVM 進行介紹,
在 VisualVM 中,打開你收集到的 hprof 檔案,在 classes 選項卡中,可以找到所有的類,這些類默認是按照 dump 中所存在的它們所擁有的實體數進行排序,當然,頂部的類極可能就是實體數暴多且導致記憶體錯誤的那個類,按字母順序對類串列進行排序也是很有幫助的,這樣可以將同一包的類分組在一起,進而可以對它們進行比較,
雙擊該串列中的一個類,視圖切換至 instances 選項卡,對于每個實體,都有兩個視窗,下邊的那個保存指向該實體的參考路徑,每個路徑的邊緣是一個垃圾收集 root(由三角形標記),這樣每個邊緣點都是一個物件,該物件持有一個參考,防止 GC 對該物件的清理,如果很多這種爆炸的類實體都指向了一個 ObjectInputStream 物件,那么導致 OOM 的罪魁禍首可能就是本文所討論的主題,
解決方案
首先,不要做的事情:嘗試讓 GC 更好的作業是沒有好處的,比如呼叫
System.gc()
命令,OOM 事件的先決條件就是 GC 已經盡其最大努力分配了所需要的記憶體,但并未成功分配,
為了使物件流不再使用上文討論的參考機制,I/O 方法:
writeObject()
readObject()
需要替換為以下方法:
writeUnshared()
readUnshared()
不同于 readObject() 方法的是,readUnshared() 方法不會在 ObjectInputStream 的參考串列中保存參考,因此能夠防止記憶體泄漏,需要注意的是,如果傳輸物件已經實作了來自 Serializable 介面的 readResolve() 方法,則該物件的參考可能還是會被傳遞并保留在參考串列中,可以在 VisualVM 中對該參考的持有者進行進一步分析,
還有一種解決方案是在每次寫入后呼叫 ObjectOutputStream reset() 方法,這將具有相同的效果,
關于使用 C vs. Java 所開發服務器的討論
本文所討論的記憶體泄漏很難被預料到,具有自動垃圾收集功能的語言進行開發的本質是,開發人員無需投入精力來了解處于不同狀態的程式的記憶體狀態,當然,這也是自動垃圾收集的優勢之一,使用自動決策演算法的陷阱是其決策必須保守,當關注 GC 時,這意味著在不確定是否需要清除記憶體時,它將始終選擇不清除可能不需要的記憶體,而不會冒清除所需記憶體的風險,這就是為什么每個參考物件都不會被清除的原因,盡管程式不會使用所有參考物件,在由 ObjectInputStream 引起泄漏的情況下,對于開發人員來說,參考是完全隱藏和意外的,導致記憶體泄漏的其他情況可能是,例如,當保存對靜態物件的參考數量不斷增長時(永遠不會清除自身),或者在傳遞內部類物件時(保留對外部類的隱式參考),這些示例對開發人員來說更為明確,但泄漏被提前發現的機會仍然很小,
另一種選擇是不使用自動垃圾收集,而是讓開發人員自己處理所有記憶體,就像在 C 和 C++ 中進行開發時所做的那樣,這種開發需要開發人員做更多的作業,但是對程式的記憶體狀態的理解水平要高得多,它雖然也不能防止記憶體錯誤的發生,但是可以使開發人員在分析它們時更了解發生了什么,
C 語言和 Java 語言對開發人員對記憶體責任的不同態度的另一個例子是,在 C 網路程式中,總是很清楚通過鏈接發送的每個資料段的大小,而不是 Java 的 Object 流那樣對其中完全不清楚,在 C 語言中,開發人員甚至還必須考慮作業系統對資料結構進行的地址對齊(使用 attribute((packed))),
大多數服務器程式都是用 Java 或 Python 等帶有自動垃圾收集器的語言所撰寫的,對于大部分人來講,節省開發人員的時間比這里提到的陷阱更有價值,
原文鏈接:Java memory leak caused by ObjectInputStream
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/232560.html
標籤:java
