Java 垃圾收集(GC)淺談
為什么需要垃圾回收诱告?
哪些內(nèi)存需要回收?
什么時(shí)候回收吠架?
如何回收芙贫?
為什么需要垃圾回收?
? 當(dāng)需要排查各種內(nèi)存溢出傍药、內(nèi)存泄露問題時(shí)磺平,當(dāng)垃圾收集稱為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí),我們就需要對這些“自動(dòng)化”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)拐辽。在構(gòu)建大型程序時(shí)褪秀,GC直接影響著內(nèi)存優(yōu)化和運(yùn)行速度。
Java 內(nèi)存區(qū)域
了解GC機(jī)制之前薛训,需要先搞清楚Java程序在執(zhí)行的時(shí)候媒吗,內(nèi)存究竟是如何劃分的。
私有內(nèi)存區(qū)的區(qū)域名和相應(yīng)的特性如下表所示:
區(qū)域名稱 | 特性 |
---|---|
程序計(jì)數(shù)器 | 指示當(dāng)前程序執(zhí)行到了哪一行乙埃,執(zhí)行Java方法時(shí)記錄正在執(zhí)行的虛擬機(jī)字節(jié)地址闸英;執(zhí)行本地方法時(shí)锯岖,計(jì)數(shù)器值為undefined |
虛擬機(jī)棧 | 用于執(zhí)行Java方法。棧幀存儲(chǔ)局部變量表甫何、操作數(shù)棧出吹、動(dòng)態(tài)鏈接、方法返回一些額外的附加信息辙喂。程序執(zhí)行時(shí)棧幀入棧捶牢;執(zhí)行完成后棧幀出棧 |
本地方法棧 | 用于執(zhí)行本地方法,其他和虛擬機(jī)棧類似 |
虛擬機(jī)棧中的局部變量表
里面存放了三個(gè)信息:
- 各種基本數(shù)據(jù)類型(boolean巍耗、byte秋麸、char、short炬太、int灸蟆、float、long亲族、double)
- 對象引用(reference)
- returnAddress地址
這個(gè)returnAddress和程序計(jì)數(shù)器有什么區(qū)別炒考?前者是指示JVM的指令執(zhí)行到了哪一行,后者是指你的代碼執(zhí)行到哪一行霎迫。
哪些內(nèi)存需要回收斋枢?
私有內(nèi)存區(qū)伴隨著線程的產(chǎn)生而產(chǎn)生,一旦線程中止知给,私有內(nèi)存區(qū)也會(huì)自動(dòng)消除杏慰,因此我們在本文中討論的內(nèi)存回收主要是針對共享內(nèi)存區(qū)。
共享內(nèi)存區(qū):
區(qū)域名稱 | 特性 |
---|---|
Java堆 | Java虛擬機(jī)管理的內(nèi)存中最大的一塊炼鞠,所有線程共享缘滥,幾乎所有的對象實(shí)例和數(shù)組都在這類分配內(nèi)存。GC主要就是在Java堆中進(jìn)行的谒主。 |
方法區(qū) | 用于存儲(chǔ)已被虛擬機(jī)加載的類信息朝扼、常量、靜態(tài)變量霎肯、及時(shí)編譯器編譯后的代碼等數(shù)據(jù)擎颖。但是已被最新的JVM取消了。現(xiàn)在观游,被加載的類作為元數(shù)據(jù)加載到底層操作系統(tǒng)的本地內(nèi)存區(qū)搂捧。 |
Java 堆
堆內(nèi)存是由存活和死亡的對象組成的。存活的對象是應(yīng)用可以訪問的懂缕,不會(huì)被垃圾回收允跑。死亡的對象是應(yīng)用不可訪問尚且還沒有被垃圾收集器回收掉的對象。一直到垃圾收集器把這些對象回收掉之前,他們會(huì)一直占據(jù)堆內(nèi)存空間聋丝。堆是應(yīng)用程序在運(yùn)行期請求操作系統(tǒng)分配給自己的向搞地質(zhì)擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)索烹,是不連續(xù)的內(nèi)存區(qū)域。用一句話總結(jié)堆的作用:程序運(yùn)行時(shí)動(dòng)態(tài)申請某個(gè)大小的內(nèi)存空間弱睦。
新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作百姓,因?yàn)镴ava對象大都具備朝生夕滅的特性,所以Minor GC 非常頻繁况木,一般回收速度也比較快垒拢。
老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC火惊,經(jīng)常會(huì)伴隨至少一次的Minor GC (但非絕對求类,在Parallel Scavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過程)。Major GC的速度一般會(huì)比Minor GC慢10倍以上
新生代:剛剛新建的對象在Eden中矗晃,經(jīng)歷一次Minor GC, Eden中的存貨對象就被移動(dòng)到第一塊survivor space S0仑嗅,Eden被清空宴倍;等Eden區(qū)再滿了张症,就再觸發(fā)一次Minor GC, Eden和S0中的存活對象會(huì)被復(fù)制送入第二塊survivor space S1。S0和Eden被清空鸵贬,然后下一輪S0與S1交換角色俗他,如此循環(huán)往復(fù)。如果對象的復(fù)制次數(shù)達(dá)到16此阔逼,改對象就被送到老年代中兆衅。
至于為什么興盛帶要分出兩個(gè)survivor區(qū),參考博客為什么新生代內(nèi)存需要有兩個(gè)Sruvivor區(qū)
老年代:如果某個(gè)對象經(jīng)歷了幾次垃圾回收之后還存活嗜浮,就會(huì)被存放到老年代中羡亩。老年代的空間一般比新生代大。
對象創(chuàng)建后的內(nèi)存分配
創(chuàng)建一個(gè)對象后危融,他會(huì)被放在堆內(nèi)存的哪個(gè)部分呢畏铆?
什么時(shí)候回收?
Java并沒有給我們提供明確的代碼來標(biāo)注一塊內(nèi)存并將其回收吉殃〈蔷樱或許你會(huì)說,我們可以將相關(guān)對象設(shè)為null或者用System.gc()蛋勺。然而瓦灶,后者將會(huì)嚴(yán)重影響代碼的性能,因?yàn)?strong>每一次顯示調(diào)用system.gc()都會(huì)停止所有響應(yīng)抱完,去檢查內(nèi)存中是否有可回收的對象贼陶,這回對程序的正常運(yùn)行造成極大威脅。另外,調(diào)用該方法并不能保障JVM立即進(jìn)行垃圾回收每界,僅僅是通知JVM要進(jìn)行垃圾回收了捅僵,具體回收與否完全由JVM決定。
生存還是死亡
可達(dá)性算法
這個(gè)算法的基本思路是通過一系列的稱為“GC Roots”的對象作為起始點(diǎn)眨层,從這些節(jié)點(diǎn)開始向下搜索庙楚,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈相連時(shí)趴樱,則證明此對象是不可用的馒闷。
在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
如何回收叁征?
標(biāo)記-清除(Mark-Sweep)算法
分為兩個(gè)階段:首先標(biāo)記出所有需要回收的對象纳账,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。
缺點(diǎn):效率問題捺疼,標(biāo)記和清除兩個(gè)過程的效率都不高疏虫;空間問題,會(huì)產(chǎn)生很多碎片啤呼。
復(fù)制算法
將可用內(nèi)存按容量劃分為大小相等的兩塊卧秘,每次只用其中一塊。當(dāng)這一塊用完了官扣,就將還存活的對象復(fù)制到另外一塊上面翅敌,然后把原始空間全部回收。高效惕蹄、簡單蚯涮。
缺點(diǎn):將內(nèi)存縮小為原來的一半。
標(biāo)記-整理(Mark-Compat)算法
標(biāo)記過程與標(biāo)記-清除算法過程一樣卖陵,但后面不是簡單的清除遭顶,而是讓所有存活的對象都向一端移動(dòng),然后直接清除掉端邊界以外的內(nèi)存泪蔫。
分代收集(Generational Collection)算法
- 新生代中棒旗,每次垃圾收集時(shí)都有大批對象死去,只有少量存活鸥滨,就選用復(fù)制算法嗦哆,只需要付出少量存活對象的復(fù)制成本就可以完成收集;
- 老年代中婿滓,其存活率較高老速、沒有額外空間對它進(jìn)行分配擔(dān)保,就應(yīng)該使用“標(biāo)記-整理”或“標(biāo)記-清除”算法進(jìn)行回收凸主。
一些收集器
Serial收集器
單線程收集器橘券,表示在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程,直到它收集結(jié)束旁舰。"Stop The World".
ParNew收集器
實(shí)際就是Serial收集器的多線程版本锋华。
- 并發(fā)(Parallel):指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍然處于等待狀態(tài)箭窜;
- 并行(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行毯焕,用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)CPU上磺樱。
Parallel Scavenge收集器
該收集器比較關(guān)注吞吐量(Throughout)(CPU用于用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值)纳猫,保證吞吐量在一個(gè)可控的范圍內(nèi)。
CMS(Concurrent Mark Sweep)收集器
CMS收集器是一種以獲得最短停頓時(shí)間為目標(biāo)的收集器竹捉。
G1(Garbage First)收集器
從JDK1.7 Update 14之后的HotSpot虛擬機(jī)正式提供了商用的G1收集器芜辕,與其他收集器相比,它具有如下優(yōu)點(diǎn):并行與并發(fā)块差;分代收集侵续;空間整合;可預(yù)測的停頓等憨闰。
新生代收集器使用的收集器:Serial状蜗、ParNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old起趾、Parallel Old诗舰、CMS
Java性能優(yōu)化
大多數(shù)針對內(nèi)存的調(diào)優(yōu)警儒,都是針對特定情況的训裆。但是實(shí)際中,調(diào)優(yōu)很難與Java運(yùn)行動(dòng)態(tài)特性的實(shí)際情況和工作負(fù)載保持一致蜀铲。也就是說边琉,幾乎不可能通過單純的調(diào)優(yōu)來消除GC的目的。
寫程序的時(shí)候應(yīng)該注意的點(diǎn):
- 減少new對象记劝。每次new對象之后变姨,都要開辟新的內(nèi)存空間。這些對象不被引用之后厌丑,還要回收掉定欧。因此,如果最大限度地合理重用對象怒竿,或者使用基本數(shù)據(jù)類型替代對象砍鸠,都有助于節(jié)省內(nèi)存。
- 多使用局部變量耕驰,減少使用靜態(tài)變量爷辱。局部變量被創(chuàng)建在棧中,存取速度快。靜態(tài)變量則是存儲(chǔ)在堆內(nèi)存中饭弓。
- 避免使用finalize双饥,該方法會(huì)給GC增添很大的負(fù)擔(dān)
- 如果是單線程,盡量使用非多線程安全的弟断,因?yàn)榫€程安全來自于同步機(jī)制咏花,同步機(jī)制會(huì)降低性能。例如阀趴,單線程程序迟螺,能使用HashMap,就不要使用HashTabl舍咖。同理矩父,盡量減少使用synchronized。
- 用移位符號替代乘除號排霉。比如:a*8應(yīng)該寫作a<<3
- 對于經(jīng)常反復(fù)使用的對象使用緩存窍株。
- 盡量使用基本類型而不是包裝類型,盡量使用一維數(shù)組而不是二維數(shù)組
- 盡量使用final修飾符攻柠,final表示不可修改球订,訪問效率高
- 單線程下(或者是針對于局部變量),字符串盡量使用StringBuilder,比StringBuffer要快
- 盡量使用StringBuffer來連接字符串瑰钮。這里需要注意的是冒滩,StringBuffer的默認(rèn)緩存容量是16個(gè)字符,如果超過16浪谴,append犯法調(diào)用私有的expandCapacity()方法开睡,來保證足夠的緩存容量。因此苟耻,如果可以預(yù)設(shè)StringBuffer的容量篇恒,避免append再去擴(kuò)展容量。
參考資料:
1.《深入理解Java虛擬機(jī)-JVM高級特性與最佳實(shí)踐》
2.橙子wj的博客 (強(qiáng)推)