Java有記憶體流失嗎?有。雖然有人說這個說法不準確,但是在C/C++程式中,我們把由當前進程開闢但當前進程在邏輯上卻無法再管理的那些記憶體稱為被進程泄漏的記憶體。事實上java同樣會有這樣的情況。
當我們最先接觸java時就因為它自動管理記憶體不需要程式員手工幹預而帶來的方便性的原因喜歡上了它,但這個自動並不是全能的。對於一些隱性引用所引起的記憶體流失,有時很長時間甚至幾個月,幾年我們也很難發現,除非是非常有經驗的人去仔細地查看源碼,借用heap分析工具細緻地分析才能發現。
最簡單的java記憶體流失是一種資料結構的實現。比如:
class InnerStack<E> { private int size; private int INITIALCAPACITY = 32; private E[] os; @SuppressWarnings("unchecked") public InnerStack(){ os = (E[]) new Object[INITIALCAPACITY]; } public void push(E o) { checkCapacity(); this.os[size++] = o; } public E pop() { if (size <= 0) throw new EmptyStackException(); E o = (E) this.os[--size]; this.os[size] = null; // 1 return o; } public E peek() { if (size <= 0) throw new EmptyStackException(); E o = (E) this.os[this.size - 1]; return o; } @SuppressWarnings("unchecked") private void checkCapacity() { if (size == os.length) { os = (E[]) new Object[size * 2 + 1]; } }}
在JAVA中99.9%的情況下,我們不需要寫type var = null;這種文法,但在自己實現的資料結構中,如果上例注釋1處os[10]指向的對象被pop出去,調用的人使用完了當然的想法是希望它能被系統回收,但是由於InnerStack中還有一個內部數組中有一個引用指向它,這個對象被不能在調用者使用完後立即標記為可回收。
如果同樣的方法實現迴圈隊列,情況還有些好轉,即使沒有打斷引用,當入隊的元素多於資料長度時原來的引用就會被後來入隊的引用覆蓋掉。但對於自動擴充的棧來說,如果某一時刻容量到了一個峰值,比如底層數組長被擴充到65,在os[64]如果pop出去後沒有設定
os[64] = null;那麼以後在很長時間不會再訪問到這個位置(峰值嘛)那麼它指向的對象雖然在外部已經“使用”完成了,但卻無法被回收,別小看這幾個對象,因為它們還會引用別的對象。關係很複雜,可能會造成很大的記憶體流失。
同理如果底層是其它的資料結構,比如List等對象支援,如果對象被擷取後,底層的對象容器是否及時地remove,是否調用clear,都會造成對象被無意地引用而不能被回收。需要把一些對象先緩衝起來然後再擷取使用的時候,最好是能選擇象WeakHashMap這樣的弱引用資料結構,以便對象在外部擷取後能儘快地回收。
Effective Java在提到其它情況的記憶體流失時提到了Listeners和callbacks,其實這些都不是主要的,因為一個Listener被add後,你並不能確定應該在什麼時候應該被清除,因為絕大多數Listener是和應用的生命週期相同,除非你在退出之前的邏輯中處理它們,否則它們沒有被回收的理由。但是真正引起大量的記憶體流失的實際應用中,NIO的使用一定要非常非常的小心。
在很多時間,NIO的一個IO通道一次不能讀取完成完成所有傳輸的資料,那麼我們無論把已經有的資料緩在自己的資料結構中和SelectorKey對應,還是把資料attach到SelectorKey上,始終有個風險,就是那個Key對應的用戶端有可能在非常惡劣的網路下或乾脆斷線,你所註冊的事件根本就沒有返回結果,則儲存原來的資料永遠沒法處理。對於寫出操作同樣有這樣的問題。所以一定要在把開始註冊的時間和Selector已經處理過的次數attach到Key中然後每次處理的時候判斷一下,對於經過多次處理以及超過一定時間沒有讀取完成你要的資料可以做相應的處理,及時把你處理這個連結的資料結構釋放出來。