前提介紹
很多小伙伴赋元,都跟我反饋忘蟹,說自己總是對JVM這一塊的學(xué)習(xí)和認識不夠扎實也不夠成熟,因為JVM的一些特性以及運作機制總是混淆以及不確定搁凸,導(dǎo)致面試和工作實戰(zhàn)中出現(xiàn)了很多的紕漏和短板媚值,解決廣大小伙伴痛點,我寫了本篇文章护糖,希望可以幫助大家夯實基礎(chǔ)和鍛造JVM技術(shù)功底褥芒。
什么是垃圾收集(GC)
在JVM領(lǐng)域中GC(Garbage Collection)翻譯為 “垃圾收集“,Garbage Collector翻譯為 “垃圾收集器”嫡良。
分代模型(Generational Model)
我們都知道在JVM中锰扶,執(zhí)行垃圾收集需要停止整個應(yīng)用(STW)。對象越多則收集所有垃圾消耗的時間就越長寝受。程序中的大多數(shù)可回收的內(nèi)存可歸為兩類:
- 大部分對象很快就不再使用
- 還有一部分不會立即無用坷牛,但也不會持續(xù)(太)長時間
這形成了分代數(shù)據(jù)模型『艹危基于這一結(jié)構(gòu), VM中的內(nèi)存被分為年輕代(Young Generation)和老年代(Old Generation)京闰,老年代有時候也稱為年老區(qū)(Tenured)。如下所示甩苛。
從上圖可以看出拆分為這樣兩個可清理的單獨區(qū)域蹂楣,允許采用不同的算法來大幅提高GC的性能。
分代模型出現(xiàn)問題
在不同分代中的對象可能會互相引用, 在收集某一個分代時就會成為 “事實上的” GC root浪藻。當(dāng)然捐迫,要著重強調(diào)的是,分代假設(shè)并不適用于所有程序爱葵。
分代模型適合場景
GC算法專門針對“總體生命周期較短”施戴,“總體生命周期較長” 這類特征的對象來進行優(yōu)化, JVM對收集那種存活時間半長不長的對象就顯得非常尷尬了反浓,如下圖對象分布。
堆內(nèi)存中的內(nèi)存池劃分也是類似的赞哗。不太容易理解的地方在于各個內(nèi)存池中的垃圾收集是如何運行的雷则。
新生代(Eden,伊甸園)
Eden是內(nèi)存中的一個區(qū)域, 用來分配新創(chuàng)建的對象肪笋。通常會有多個線程同時創(chuàng)建多個對象月劈,所以Eden區(qū)被劃分為多個線程本地分配緩沖區(qū)(Thread Local Allocation Buffer, 簡稱TLAB)。通過這種緩沖區(qū)劃分藤乙,大部分對象直接由JVM 在對應(yīng)線程的TLAB中分配, 避免與其他線程的同步操作猜揪。
如果 TLAB 中沒有足夠的內(nèi)存空間, 就會在共享Eden區(qū)(shared Eden space)之中分配。如果共享Eden區(qū)也沒有足夠的空間, 就會觸發(fā)一次 年輕代GC 來釋放內(nèi)存空間坛梁。如果GC之后 Eden 區(qū)依然沒有足夠的空閑內(nèi)存區(qū)域, 則對象就會被分配到老年代空間(Old Generation)而姐。
當(dāng)Eden區(qū)進行垃圾收集時,GC將所有從root可達的對象過一遍, 并標(biāo)記為存活對象划咐。
對象間可能會有跨代的引用拴念,所以需要一種方法來標(biāo)記從其他分代中指向Eden的所有引用。這樣做又會遭遇各個分代之間一遍又一遍的引用褐缠。JVM在實現(xiàn)時采用了卡片標(biāo)記(card-marking)政鼠。
卡片標(biāo)記
JVM只需要記住Eden區(qū)中 “臟”對象的粗略位置,可能有老年代的對象引用指向這部分區(qū)間队魏。
存活區(qū)(Survivor Spaces)
Eden區(qū)的旁邊是兩個存活區(qū), 稱為 from 空間和 to 空間公般。需要著重強調(diào)的的是, 任意時刻總有一個存活區(qū)是空的(empty)。
空的那個存活區(qū)用于在下一次年輕代GC時存放收集的對象器躏。年輕代中所有的存活對象(包括Edenq區(qū)和非空的那個 “from” 存活區(qū))都會被復(fù)制到 ”to“ 存活區(qū)俐载。GC過程完成后, ”to“ 區(qū)有對象,而 ‘from’ 區(qū)里沒有對象蟹略。兩者的角色進行正好切換 登失。
存活的對象會在兩個存活區(qū)之間復(fù)制多次,直到某些對象的存活時間達到一定的閥值挖炬。分代理論假設(shè), 存活超過一定時間的對象很可能會繼續(xù)存活更長時間揽浙。
這類“ 年老” 的對象因此被提升(promoted )到老年代。提升的時候意敛, 存活區(qū)的對象不再是復(fù)制到另一個存活區(qū),而是遷移到老年代, 并在老年代一直駐留, 直到變?yōu)椴豢蛇_對象馅巷。
此外GC會跟蹤記錄每個存活區(qū)對象存活的次數(shù),每次分代GC完成后草姻,存活對象的年齡就會+1钓猬。當(dāng)年齡超過提升閾值(tenuring threshold),就會被提升到老年代區(qū)域撩独。
MaxTenuringThreshold的判定
具體的提升閾值由JVM動態(tài)調(diào)整,但也可以用參數(shù) -XX:+MaxTenuringThreshold
來指定上限敞曹。如果設(shè)置 -XX:+MaxTenuringThreshold=0
, 則GC時存活對象不在存活區(qū)之間復(fù)制账月,直接提升到老年代。現(xiàn)代 JVM 中這個閾值默認設(shè)置為15個GC周期澳迫。這也是HotSpot中的最大值局齿。
老年代(Old Generation)
老年代內(nèi)存空間一般情況下,里面的對象是垃圾的概率也更小橄登。
老年代GC發(fā)生的頻率比年輕代小很多抓歼。同時, 因為預(yù)期老年代中的對象大部分是存活的, 所以不再使用標(biāo)記和復(fù)制(Mark and Copy)算法。而是采用移動對象的方式來實現(xiàn)最小化內(nèi)存碎片拢锹。老年代空間的清理算法通常是建立在不同的基礎(chǔ)上的谣妻。原則上,會執(zhí)行以下這些步驟:
- 通過標(biāo)志位(marked bit),標(biāo)記所有通過 GC roots 可達的對象.
- 刪除所有不可達對象
- 整理老年代空間中的內(nèi)容,方法是將所有的存活對象復(fù)制,從老年代空間開始的地方,依次存放卒稳。
通過上面的描述可知, 老年代GC必須明確地進行整理,以避免內(nèi)存碎片過多拌禾。
永久代(PermGen)
Java8之前有一個特殊的空間,稱為“永久代”(Permanent Generation)展哭。
它存儲元數(shù)據(jù)(metadata)的地方,比如 class 信息等湃窍。此外,這個區(qū)域中也保存有其他的數(shù)據(jù)和信息, 包括內(nèi)部化的字符串(internalized strings)等等。
元數(shù)據(jù)區(qū)(Metaspace)
Java 8直接刪除了永久代(Permanent Generation)匪傍,改用Metaspace您市。將靜態(tài)變量和字符串常量都放到其中。像類定義(class definitions)之類的信息會被加載到Metaspace 中役衡。
元數(shù)據(jù)區(qū)位于本地內(nèi)存(native memory)茵休,不再影響到普通的Java對象。默認情況下, Metaspace的大小只受限于Java進程可用的本地內(nèi)存手蝎。
常見的垃圾回收思想的誤區(qū)
在我們的日常生活中垃圾收集主要就是找到垃圾并進行清理榕莺,這與我們JVM的運作機制恰恰相反,JVM中的垃圾收集器跟蹤和標(biāo)記所有正在使用的對象棵介,并把其余部分的對象當(dāng)做垃圾對象钉鸯。
所以這里一定要區(qū)分清楚,我們這里的標(biāo)記:是指標(biāo)記可用對象邮辽,而不是垃圾對象唠雕。常常會有人吧這兩者理解錯誤和混亂。
記住這一點以后吨述,我們再深入講解內(nèi)存自動回收的原理岩睁,探究JVM中垃圾收集的具體實現(xiàn)。先從基礎(chǔ)開始, 介紹垃圾收集的一般特征揣云、核心概念以及實現(xiàn)算法捕儒。
常見的垃圾回收類型
垃圾回收類型主要是通過回收的范圍進行界定和劃分。具體的JVM回收區(qū)域如下圖所示邓夕。
Java8之前
Java8之后
垃圾收集(Garbage Collection)通常分為:Minor GC - Major GC - Full GC 刘莹。接下來介紹這些事件及其區(qū)別亿笤,然后你會發(fā)現(xiàn)這些區(qū)別也不是特別清晰。
- Minor GC:年輕代垃圾回收機制栋猖,屬于輕量級GC净薛,主要面向于年輕代區(qū)域的垃圾對象進行回收。
- Major GC:老年代垃圾回收機制蒲拉,屬于重量級GC肃拜,主要面向于老年代區(qū)域的垃圾對象進行回收。
- Full GC:完全化GC雌团,屬于全量極GC燃领,大致角度而言Major GC和Full GC差不多,其實具體分析锦援,F(xiàn)ullGC的范圍是面向于整體的Heap堆內(nèi)存猛蔽。
GC的優(yōu)點和缺點(GC Benefits/Cost)
好處
- 提高系統(tǒng)的可靠性和穩(wěn)定性
- 內(nèi)存管理與程序設(shè)計的解耦
- 調(diào)試內(nèi)存錯誤所花費的時間更少
- 懸掛程序點/內(nèi)存泄漏不會發(fā)生
注意:Java程序沒有內(nèi)存泄漏;“不意味著對象存儲地址”更準(zhǔn)確)
壞處
- GC暫停的時間長度
- CPU/內(nèi)存利用率
Minor GC
年輕代內(nèi)存的垃圾收集稱為Minor GC灵寺。那什么時候會觸發(fā)MinorG以及出發(fā)MinorGC得我條件是什么曼库?
觸發(fā)MinorGC的時機
當(dāng)JVM無法為新對象分配Eden區(qū)的內(nèi)存空間時/達到了Eden存放閾值的時候會觸發(fā) Minor GC,所以新對象分配頻率越高略板,Minor GC的頻率就越高毁枯。并且Minor GC每次都會引起全線停頓(stop-the-world ),暫停所有的應(yīng)用線程叮称,對大多數(shù)程序而言,暫停時長基本上是可以忽略不計的种玛。
MinorGC回收的瓶頸
Eden區(qū)的對象基本上都是垃圾,也不怎么復(fù)制到Survior區(qū)/老年代瓤檐。如果情況不是這樣, 大部分新創(chuàng)建的對象不能被垃圾回收清理掉赂韵,則 Minor GC的停頓就會持續(xù)更長的時間。
MinorGC回收的范圍
Minor GC實際上忽略了老年代挠蛉,主要面向的對象范圍有兩部分組成:
主要是面向于老年代到年輕代的所引用的對象范圍祭示,例如,它會將從老年代指向年輕代的引用都被認為是GC Root碌秸,(而從年輕代指向老年代的引用在標(biāo)記階段全部被忽略)绍移。
主要面向的是Survior區(qū)之間的相互引用,此種場景的生命周期較短讥电,屬于年輕代之內(nèi)的對象之間的引用關(guān)系。
所以轧抗,Minor GC的定義很簡單恩敌、清理的就是年輕代,如下圖所示横媚。
Major GC vs Full GC
從上面我們知道了Minor GC清理的是年輕代空間(Young space)纠炮,相應(yīng)的其他區(qū)域也有對應(yīng)的回收機制和策略月趟。
Major GC清理的是老年代空間(Old space),MajorGC是由Minor GC觸發(fā)的恢口,所以很多情況下這兩者是不可分離的孝宗,G1這樣的垃圾收集算法執(zhí)行的是部分區(qū)域垃圾回收。
Full GC清理的是整個堆耕肩,包括年輕代和老年代空間因妇。
Minor GC、MajorGC和FullGC執(zhí)行效果
大部分情況下猿诸,發(fā)生在年輕代的Minor GC次數(shù)會很多婚被,會引起STW,也就是全局化暫停執(zhí)行業(yè)務(wù)線程的行為梳虽,但是時間很短(幾乎可以忽略不計)址芯。而Major GC和Full GC也會造成全局化暫停的效果。所以一般情況下盡可能減少MajorGC和FullGC是什么必要的窜觉,但是也不能“一棒子打死一船人”谷炸。必要的時候還是需要觸發(fā)少量幾次Major GC以及FullGC,進而釋放一些RSS常駐內(nèi)存禀挫。
垃圾收集(GC)的原理
自動內(nèi)存管理(Automated Memory Management)
如果要顯式地聲明什么時候需要進行內(nèi)存管理淑廊,實現(xiàn)自動進行收集垃圾,那樣就太方便了特咆,開發(fā)者不再耗費腦細胞去考慮要在何處進行內(nèi)存清理季惩。運行時環(huán)境會自動算出哪些內(nèi)存不再使用,并將其釋放腻格,歷史上第一款垃圾收集器是1959年為Lisp語言開發(fā)的画拾。
引用計數(shù)(Reference Counting)
共享指針方式的引用計數(shù)法, 可以應(yīng)用到所有對象菜职。許多語言都采用這種方法青抛,包括 Perl、Python 和 PHP 等酬核。下圖很好地展示了這種方式:
上圖中所展示的GC ROOTS蜜另,表示程序正在使用的對象。主要(這里指的不是全部)集中在于當(dāng)前正在執(zhí)行的方法中的局部變量或者是靜態(tài)變量等嫡意。在這里主要我指的是Java肴盏。
- 藍色的圓圈表示可以引用到的對象源梭,里面的數(shù)字就是被引用計數(shù)器。
- 灰色的圓圈是各個作用域都不再引用的對象,可以被認為是垃圾母蛛,隨時會被垃圾收集器清理。
循環(huán)引用(detached cycle)的問題
引用計數(shù)器無法針對于循環(huán)引用這種場景進行正確的處理和探測。任何作用域中都沒有引用指向這些對象,但由于循環(huán)引用, 導(dǎo)致引用計數(shù)一直大于零忍些,如下圖所示。
- 紅色線路和紅色圓圈對象實際上屬于垃圾引用以及垃圾對象坎怪,但由于引用計數(shù)的局限罢坝,所以存在內(nèi)存泄漏,永遠都無法進行回收該區(qū)域的對象內(nèi)存搅窿。
循環(huán)引用(detached cycle)的解決方案
比如說可以針對于一些這種循環(huán)模式進行加入到 “弱引用”(‘weak’ references)的體系中嘁酿,所以即使無法進行解決循環(huán)引用計數(shù)的場景,也可以通過弱引用實現(xiàn)內(nèi)存回收戈钢。
精華推薦 | 【JVM深層系列】「GC底層調(diào)優(yōu)系列」一文帶你徹底加強夯實底層原理之GC垃圾回收技術(shù)的分析指南(GC算法分析)