良好的Java編碼習慣(二)
Item5:消除過期對象的引用
JVM為我們實現了GC(垃圾回收)的功能,讓我們從手工管理內存中解放了出來,這固然很好,但並不意味著我們就再也不需要去考慮內存管理的事情了;我們用簡單的棧實現的例子來解釋:
public class Stack { private Object[] elements; private in size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack(){ elements= new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e){ ensureCapacity(); elements[size++] = e; } public Object pop(){ if(size == 0){ return new EmptyStackException(); }return element[--size]; } //保證每當棧滿就會自動擴容為原來的兩倍 private void ensureCapacity(){ if(elements.length == size){ elements = Arrays.copyOf(elements, 2*size+1); } } }
這段程序沒有什麽明顯的錯誤,無論怎麽測試,結果似乎都是正確的,但不嚴格的講,這段程序存在"內存泄漏"的風險,極端情況下,會導致磁盤交換(Disk Paging),甚至是程序失敗(OutOfMemoryError);
為什麽是內存泄漏?書中給出的解釋是:從棧中pop出來的對象的引用還被棧內部維護著,這些引用被稱作"過期引用"(指下標小於size的那些元素的引用),所以,GC不會去處理該對象以及該對象所引用的其他對象,內存就被這樣漸漸"填滿"導致溢出.
修復方法,改寫pop()
public Object pop(){ if(size == 0){ throw new EmptyStackException(); } Object result = element[--size]; elements[size] = null; //清空出棧對象的引用,告知GC可以釋放其資源
return result; }
手動將過期引用告知垃圾回收器還有的好處就是,如果之後又用了此過期引用,程序就會報空指針異常,而不是一直"錯誤下去"..
內存泄漏的另一常見來源是緩存,對象引用在緩存中就很容易被忘記,如果不用它會長時間駐留緩存.對應的幾種解決方法:
1.使用WeakHashMap代表緩存,該類擁有expungeStaleEntries方法,用於清除那些外部沒有引用並且緩存內存在的鍵,來段代碼說明:
public class Test { public static void main(String[] args) throws Exception { String a = new String("a"); String b = new String("b"); Map weakmap = new WeakHashMap(); Map map = new HashMap(); map.put(a, "aaa"); map.put(b, "bbb"); weakmap.put(a, "aaa"); weakmap.put(b, "bbb"); map.remove(a); a=null; b=null; System.gc(); } }
由於HashMap移除了a的引用,且清除了a的外部引用,此時a的引用只有WeakHashMap來維護了,此時WeakHashMap會自動舍棄掉a,但HashMap還保存著b的引用,所以b不會被WeakHashMap丟棄掉。在工程當中WeakHashMap是很實用的,我們使用短時間內就過期的緩存時最好使用weakHashMap。
2.可以將清除過期引用的任務交給“Time”或“ScheduledThreadPoolExecutor”來完成
即定時清除過期引用。
3.使用LinkedHashMap當做緩存,調用removeEldesttEntry方法清除過期引用
4.直接使用java.lang.ref
說道ref包,就繞不開Java的四類引用:強引用,軟引用,弱引用,虛引用。這裏大致概括一下,具體引用就不過多贅述,細節可以參照深入探討java.lang.ref包
強引用:只要引用存在,垃圾回收器永遠不會回收,Object obj = new Object()的形式
軟引用:非必須引用,內存溢出之前進行回收,可以通過以下代碼實現,SoftReference<Object> sf = new SoftReference<Object>(obj)的形式,通過sf.get()獲取對象,軟引用主要用戶實現類似緩存的功能,在內存足夠的情況下直接通過軟引用取值,無需從繁忙的真實來源查詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。
弱引用:第二次垃圾回收時回收,WeakReference<Object> wf = new WeakReference<Object>(obj)的形式,弱引用主要用於監控對象是否已經被垃圾回收器標記為即將回收的垃圾,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器標記。
虛引用:垃圾回收時回收,無法通過引用取到對象值,PhantomReference<Object> pf = new PhantomReference<Object>(obj)的形式,pf.isEnQueued()主要用於檢測對象是否已經從內存中刪除。
關於內存泄漏的檢測,往往不會表現的很明顯,但它能在系統中存在很多年,只有通過檢查代碼或借助於Heap剖析工具(Heap Profiler)發現問題,關於Heap Profiler我有機會在單獨介紹。
Item6:避免使用終結方法(finalizer)
下面由一段代碼引入:
/** * @author YHW * @ClassName: Test1 * @Description: * @date 2019/1/4 20:55 */ public class Test1 { public static void main(String[] args){ Bob bob = new Bob(); System.out.println(bob.getState()); Thread thread = new Thread(){ @Override public void run() { super.run(); try{ sleep(2000); }catch(Exception e){ e.printStackTrace(); }finally{ bob.setClosed(true); } } }; thread.start(); System.out.println(bob.getState()); } }
運行結果截圖:
在main方法中開了一個子線程,我故意睡了2秒,這時候主線程已經走完了,故根本走不到終結方法那裏,程序就”關閉“了;
這就引出了終結方法所具備的特性:
1.finalizer方法的線程優先級比當前程序的其他線程優先級要低,且JAVA語言規範不保證任何線程中finalizer方法的執行;
2.及時執行終結方法是JVM的一大功能,但在不同的JVM實現都大相徑庭,有時候終結方法的線程優先級會非常低,造成JVM“沒時間”釋放不用的資源,引起OutOfMemoryError;
3.唯一聲稱保證終結方法執行的System.runFinalizersOnExit和Runtime.runFinalizersOnExit都存在致命缺陷,已經廢棄了;
4.finalizer會有非常嚴重的性能損失;
良好的Java編碼習慣(二)