面試的時候按照這個套路回答 Java GC 的相關(guān)問題一定能過!
Hello 大家好,我是鴨血粉絲,2020 注定是一個不平凡的一年,很多小伙伴在后臺和星球留言都說今年的工作不好找,也有應(yīng)屆生小伙伴給阿粉發(fā)消息問阿粉所在的公司今年是否招應(yīng)屆生,阿粉也只能幫小伙伴問問 HR,但是阿粉也做不了主。
剛好前幾天一個小伙伴在微信上問阿粉,說是面試一家公司被問到 Java GC 相關(guān)的東西,雖然平時也有準(zhǔn)備,但是回答起來總是零零散散,感覺沒有邏輯。其實阿粉也能理解,現(xiàn)在面試都是一種問答模式,一個隨便問問,一個死記硬背,很多時候面試前準(zhǔn)備的好好的一到面試的時候可能緊張就給忘了,事后感覺自己表現(xiàn)的不好。
這篇文章給大家舉個例子,在遇到一個問題或者知識點的時候要怎么去理解和學(xué)習(xí)。
Java GC
目標(biāo)
遇到一個問題或者一個知識點,我們要理解和明白是要解決什么問題的。說到 Java GC 那這個 GC 的目的是什么呢?很顯然是回收內(nèi)存,因為內(nèi)存是有限的,隨著程序中創(chuàng)建的對象越來越多,如果進(jìn)行回收就會導(dǎo)致內(nèi)存越來越大,最后程序就會出現(xiàn)異常。既然目的是為了回收內(nèi)存,那么新的問題來了,哪些對象可以被回收呢?什么時候進(jìn)行回收呢?怎么回收呢?
哪些對象可以被回收
簡單來說就是無用的對象可以被回收,那么換句話說,如果定義一個對象是無用的呢?這里主要有兩種方法,一個叫引用計數(shù)法,一個叫可達(dá)性分析法。
引用計數(shù)
引用計數(shù)說的是如果一個對象被別的對象進(jìn)行了一次引用,那么該對象會有一個引用計數(shù)器,這個計數(shù)器就會加一;如果被釋放一下,引用計數(shù)器就會減一。當(dāng)引用計數(shù)器的計數(shù)為 0 的時候就表示這個對象是無用的,此時就可以對這樣對象進(jìn)行回收了。表面上看好像挺合理的,實現(xiàn)起來也很方便,但是仔細(xì)一想就會發(fā)現(xiàn)有問題。既循環(huán)引用的問題,比如對象 A 引用了對象 B,但是對象 B 當(dāng)中也引用了對象 A,那么這個時候?qū)ο?A 和對象 B 的引用計數(shù)器的計數(shù)都不會是 0,但是這兩個對象都沒有被其他對象引用,理論上來說這兩個對象都是可以被回收的。
從上面看到,這種方案是有問題的會導(dǎo)致內(nèi)存泄露。隨之而來的就出現(xiàn)了另一種方案,可就是可達(dá)性分析。
可達(dá)性分析
可達(dá)性分析說的是從 GCRoots 的點作為起點,向下搜索,當(dāng)找不到任何引用鏈的時候表示該對象為垃圾對象。那么哪些對象可以被認(rèn)為是 Roots 節(jié)點呢?有 Java 棧中的對象,方法區(qū)的靜態(tài)屬性和常量以及本地方法棧中的對象。從這幾種對象依次向下搜索,如果沒有能達(dá)到 Roots 節(jié)點的對象就是垃圾對象,就說明可以被回收。
如下圖所有,對象 A,B,C都能找到與 Roots 節(jié)點的聯(lián)系,但是對象 D,E,F(xiàn) 三個并不能找到與 Roots 節(jié)點的聯(lián)系,也就是不可達(dá),所以 DEF 這三個對象就是垃圾對象。
什么時候回收
上面的兩種方案解決了哪些對象能被回收,那么下個問題,就是什么時候進(jìn)行垃圾回收呢?在排除人為調(diào)用的時候,垃圾回收都是發(fā)生在為新生對象進(jìn)行內(nèi)存分配的時候,這個時候如果內(nèi)存空間不足就會觸發(fā) GC 進(jìn)行垃圾回收。
怎么回收
上面我們知道了哪些對象可以被回收,也知道我們應(yīng)該什么時候進(jìn)行回收,那下面要解決的就是如何進(jìn)行垃圾回收了。垃圾回收根據(jù)實現(xiàn)的方式不同有多種不同的算法實現(xiàn)。比如有標(biāo)記清除算法,復(fù)制算法,標(biāo)記整理算法,分代回收算法,下面簡單介紹一下,想深入了解的可以自行去研究一下。
標(biāo)記清除算法
標(biāo)記清除算法很好理解,主要就是執(zhí)行兩個動作,一個是標(biāo)記,另一個是對進(jìn)行標(biāo)記的對象內(nèi)存進(jìn)行清除回收。這個算法有個問題就是會出現(xiàn)內(nèi)存碎片化嚴(yán)重。如下圖所示:
從上圖中可以看到,在進(jìn)行內(nèi)存回收后出現(xiàn)了嚴(yán)重的內(nèi)存碎片化,這就導(dǎo)致在分配某些大對象的時候仍然會出現(xiàn)內(nèi)存不夠的情況,但是總體內(nèi)存確是夠的。
復(fù)制算法
復(fù)制算法的實現(xiàn)方式比較簡潔明了,就是霸道的把內(nèi)存分成兩部分,在平時使用的時候只用其中的固定一份,在當(dāng)需要進(jìn)行 GC 的時候,把存活的對象復(fù)制到另一部分中,然后將已經(jīng)使用的內(nèi)存全部清理掉。如下圖:
從上圖可以看到解決了標(biāo)記清除的內(nèi)存碎片化問題,但是很明顯復(fù)制算法有另一個問題,那就是內(nèi)存的使用率大大下降,能使用的內(nèi)存只有原來的一半了。
標(biāo)記整理算法
既然標(biāo)記清除和復(fù)制算法各有優(yōu)缺點,那自然的我們就想到是否可以把這兩種算法結(jié)合起來,于是就出現(xiàn)了標(biāo)記整理算法。標(biāo)記階段是標(biāo)記清除算法一樣,先標(biāo)記出需要回收的部分,不過清除階段不是直接清除,而是把存活的對象往內(nèi)存的一端進(jìn)行移動,然后清除剩下的部分。如下圖:
標(biāo)記整理的算法雖然可以解決上面兩個算法的一些問題,但是還是需要先進(jìn)行標(biāo)記,然后進(jìn)行移動,整個效率還是偏低的。
分代回收算法
分代回收算法是目前使用較多的一種算法,這個不是一個新的算法,只是將內(nèi)存進(jìn)行的劃分,不同區(qū)域的內(nèi)存使用不同的算法。根據(jù)對象的存活時間將內(nèi)存的劃分為新生代和老年代,其中新生代包含 Eden 區(qū)和 S0,S1。在新生代中使用是復(fù)制算法,在進(jìn)行對象內(nèi)存分配的時候只會使用 Eden 和 S0 區(qū),當(dāng)發(fā)生 GC 的時候,會將存活的對象復(fù)制到 S1 區(qū),然后循環(huán)往復(fù)進(jìn)行復(fù)制。當(dāng)某個對象在進(jìn)行了 15 次GC 后依舊存活,那這個對象就會進(jìn)入老年代。老年代因為每次回收的對象都會比較少,因此使用的是標(biāo)記整理算法。
垃圾回收器
講完了垃圾回收算法,我們再看下垃圾回收器,每一種垃圾回收器都是不同時代的不同產(chǎn)物,都有其獨特性。
- Serial 垃圾收集器(單線程、復(fù)制算法)
- ParNew垃圾收集器(Serial+多線程)
- Parallel Scavenge 收集器(多線程復(fù)制算法、高效)
- SerialOld收集器(單線程標(biāo)記整理算法)
- ParallelOld收集器(多線程標(biāo)記整理算法)
- CMS收集器(多線程標(biāo)記清除算法)
- G1收集器
各個垃圾收集器的配合使用情況可以參考下圖,個人覺得對這么多的收集器沒有必要全部精通,可以注重關(guān)注一下 CMS 和 G1 就可以了。感興趣的小伙伴可以自己的研究一下。