轉自:http://blog.hesey.net/2014/05/gc-oriented-java-programming.htmlhtml
Java程序員在編碼過程當中一般不須要考慮內存問題,JVM通過高度優化的GC機制大部分狀況下都可以很好地處理堆(Heap)的清理問題。以致於許多Java程序員認爲,我只須要關心什麼時候建立對象,而回收對象,就交給GC來作吧!甚至有人說,若是在編程過程當中頻繁考慮內存問題,是一種退化,這些事情應該交給編譯器,交給虛擬機來解決。java
這話其實也沒有太大問題,的確,大部分場景下關心內存、GC的問題,顯得有點「杞人憂天」了,高老爺說過:程序員
過早優化是萬惡之源。算法
但另外一方面,什麼纔是「過早優化」?數據庫
If we could do things right for the first time, why not?編程
事實上JVM的內存模型( JMM )理應是Java程序員的基礎知識,處理過幾回JVM線上內存問題以後就會很明顯感覺到,不少系統問題,都是內存問題。數組
對JVM內存結構感興趣的同窗能夠看下 淺析Java虛擬機結構與機制 這篇文章,本文就再也不贅述了,本文也並不關注具體的GC算法,相關的文章汗牛充棟,隨時可查。緩存
另外,不要期望GC優化的這些技巧,能夠對應用性能有成倍的提升,特別是對I/O密集型的應用,或是實際落在YoungGC上的優化,可能效果只是幫你減小那麼一點YoungGC的頻率。安全
但我認爲,優秀程序員的價值,不在於其所掌握的幾招屠龍之術,而是在細節中見真著,就像前面說的,若是咱們能夠一次把事情作對,而且作好,在容許的範圍內儘量追求卓越,爲何不去作呢?數據結構
大部分GC算法,都將堆內存作分代(Generation)處理,可是爲何要分代呢,又爲何不叫內存分區、分段,而要用面向時間、年齡的「代」來表示不一樣的內存區域?
GC分代的基本假設是:
絕大部分對象的生命週期都很是短暫,存活時間短。
而這些短命的對象,偏偏是GC算法須要首先關注的。因此在大部分的GC中,YoungGC(也稱做MinorGC)佔了絕大部分,對於負載不高的應用,可能跑了數個月都不會發生FullGC。
基於這個前提,在編碼過程當中,咱們應該儘量地縮短對象的生命週期。在過去,分配對象是一個比較重的操做,因此有些程序員會盡量地減小new對象的次數,嘗試減少堆的分配開銷,減小內存碎片。
可是,短命對象的建立在JVM中比咱們想象的性能更好,因此,不要吝嗇new關鍵字,大膽地去new吧。
固然前提是不作無謂的建立,對象建立的速率越高,那麼GC也會越快被觸發。
結論:
分配小對象的開銷很是小,不要吝嗇去建立。
GC最喜歡這種小而短命的對象。
讓對象的生命週期儘量短,例如在方法體內建立,使其能儘快地在YoungGC中被回收,不會晉升(romote)到年老代(Old Generation)。
基於大部分對象都是小而短命,而且不存在多線程的數據競爭。這些小對象的分配,會優先在線程私有的 TLAB 中分配,TLAB中建立的對象,不存在鎖甚至是CAS的開銷。
TLAB佔用的空間在Eden Generation。
當對象比較大,TLAB的空間不足以放下,而JVM又認爲當前線程佔用的TLAB剩餘空間還足夠時,就會直接在Eden Generation上分配,此時是存在併發競爭的,因此會有CAS的開銷,但也還好。
當對象大到Eden Generation放不下時,JVM只能嘗試去Old Generation分配,這種狀況須要儘量避免,由於一旦在Old Generation分配,這個對象就只能被Old Generation的GC或是FullGC回收了。
GC算法在掃描存活對象時一般須要從ROOT節點開始,掃描全部存活對象的引用,構建出對象圖。
不可變對象對GC的優化,主要體如今Old Generation中。
能夠想象一下,若是存在Old Generation的對象引用了Young Generation的對象,那麼在每次YoungGC的過程當中,就必須考慮到這種狀況。
Hotspot JVM爲了提升YoungGC的性能,避免每次YoungGC都掃描Old Generation中的對象引用,採用了 卡表(Card Table) 的方式。
簡單來講,當Old Generation中的對象發生對Young Generation中的對象產生新的引用關係或釋放引用時,都會在卡表中響應的標記上標記爲髒(dirty),而YoungGC時,只須要掃描這些dirty的項就能夠了。
可變對象對其它對象的引用關係可能會頻繁變化,而且有可能在運行過程當中持有愈來愈多的引用,特別是容器。這些都會致使對應的卡表項被頻繁標記爲dirty。
而不可變對象的引用關係很是穩定,在掃描卡表時就不會掃到它們對應的項了。
注意,這裏的不可變對象,不是指僅僅自身引用不可變的final對象,而是真正的Immutable Objects。
早期的不少Java資料中都會提到在方法體中將一個變量置爲null可以優化GC的性能,相似下面的代碼:
List<String> list = new ArrayList<String>();
// some code
list = null; // help GC事實上這種作法對GC的幫助微乎其微,有時候反而會致使代碼混亂。
我記得幾年前 @rednaxelafx 在HLL VM小組中詳細論述過這個問題,原帖我沒找到,結論基本就是:
在一個很是大的方法體內,對一個較大的對象,將其引用置爲null,某種程度上能夠幫助GC。
大部分狀況下,這種行爲都沒有任何好處。
因此,仍是早點放棄這種「優化」方式吧。
GC比咱們想象的更聰明。
在不少Java資料上都有下面兩個奇技淫巧:
經過Thread.yield()讓出CPU資源給其它線程。
經過System.gc()觸發GC。
事實上JVM從不保證這兩件事,而System.gc()在JVM啓動參數中若是容許顯式GC,則會觸發FullGC,對於響應敏感的應用來講,幾乎等同於自殺。
So,讓咱們牢記兩點:
Never use Thread.yield()。
Never use System.gc()。除非你真的須要回收Native Memory。
第二點有個Native Memory的例外,若是你在如下場景:
· 使用了NIO或者NIO框架(Mina/Netty)
· 使用了DirectByteBuffer分配字節緩衝區
· 使用了MappedByteBuffer作內存映射
因爲Native Memory只能經過FullGC(或是CMS GC)回收,因此除非你很是清楚這時真的有必要,不然不要輕易調用System.gc(),且行且珍惜。
另外爲了防止某些框架中的System.gc調用(例如NIO框架、Java RMI),建議在啓動參數中加上-XX:+DisableExplicitGC來禁用顯式GC。
這個參數有個巨大的坑,若是你禁用了System.gc(),那麼上面的3種場景下的內存就沒法回收,可能形成OOM,若是你使用了CMS GC,那麼能夠用這個參數替代:-XX:+ExplicitGCInvokesConcurrent。
關於System.gc(),能夠參考 @bluedavy 的幾篇文章:
Java容器的一個特色就是能夠動態擴展,因此一般咱們都不會去考慮初始大小的設置,不夠了反正會自動擴容唄。
可是擴容不意味着沒有代價,甚至是很高的代價。
例如一些基於數組的數據結構,例如StringBuilder、StringBuffer、ArrayList、HashMap等等,在擴容的時候都須要作ArrayCopy,對於不斷增加的結構來講,通過若干次擴容,會存在大量無用的老數組,而回收這些數組的壓力,全都會加在GC身上。
這些容器的構造函數中一般都有一個能夠指定大小的參數,若是對於某些大小能夠預估的容器,建議加上這個參數。
但是由於容器的擴容並非等到容器滿了才擴容,而是有必定的比例,例如HashMap的擴容閾值和負載因子(loadFactor)相關。
Google Guava框架對於容器的初始容量提供了很是便捷的工具方法,例如:
Lists.newArrayListWithCapacity(initialArraySize);
Lists.newArrayListWithExpectedSize(estimatedSize);
Sets.newHashSetWithExpectedSize(expectedSize);
Maps.newHashMapWithExpectedSize(expectedSize);這樣咱們只要傳入預估的大小便可,容量的計算就交給Guava來作吧。
反例:
若是採用默認無參構造函數,建立一個ArrayList,不斷增長元素直到OOM,那麼在此過程當中會致使:
屢次數組擴容,從新分配更大空間的數組
屢次數組拷貝
內存碎片
爲了減小對象分配開銷,提升性能,可能有人會採起對象池的方式來緩存對象集合,做爲複用的手段。
可是對象池中的對象因爲在運行期長期存活,大部分會晉升到Old Generation,所以沒法經過YoungGC回收。
而且一般……沒有什麼效果。
對於對象自己:
若是對象很小,那麼分配的開銷原本就小,對象池只會增長代碼複雜度。
若是對象比較大,那麼晉升到Old Generation後,對GC的壓力就更大了。
從線程安全的角度考慮,一般池都是會被併發訪問的,那麼你就須要處理好同步的問題,這又是一個大坑,而且同步帶來的開銷,未必比你從新建立一個對象小。
對於對象池,惟一合適的場景就是當池中的每一個對象的建立開銷很大時,緩存複用纔有意義,例如每次new都會建立一個鏈接,或是依賴一次RPC。
好比說:
· 線程池
· 數據庫鏈接池
· TCP鏈接池即便你真的須要實現一個對象池,也請使用成熟的開源框架,例如Apache Commons Pool。
另外,使用JDK的ThreadPoolExecutor做爲線程池,不要重複造輪子,除非當你看過AQS的源碼後認爲你能夠寫得比Doug Lea更好。
儘量縮小對象的做用域,即生命週期。
若是能夠在方法內聲明的局部變量,就不要聲明爲實例變量。
除非你的對象是單例的或不變的,不然儘量少地聲明static變量。
java.lang.ref.Reference有幾個子類,用於處理和GC相關的引用。JVM的引用類型簡單來講有幾種:
· Strong Reference,最多見的引用
· Weak Reference,當沒有指向它的強引用時會被GC回收
· Soft Reference,只當臨近OOM時纔會被GC回收
· Phantom Reference,主要用於識別對象被GC的時機,一般用於作一些清理工做當你須要實現一個緩存時,能夠考慮優先使用WeakHashMap,而不是HashMap,固然,更好的選擇是使用框架,例如Guava Cache。
最後,再次提醒,以上的這些未必能夠對代碼有多少性能上的提高,可是熟悉這些方法,是爲了幫助咱們寫出更卓越的代碼,和GC更好地合做。