這篇文章將為大家詳細(xì)講解有關(guān)java虛擬機(jī)垃圾收集器的由來(lái),小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、成都微信小程序、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了合作免費(fèi)建站歡迎大家使用!
1.垃圾收集器的由來(lái)
為什么要有垃圾收集器???不知道有沒(méi)有想過(guò)這個(gè)問(wèn)題,你說(shuō)我運(yùn)行一個(gè)程序要什么垃圾收集器?。?/p>
隨意看一下下面兩行代碼:
User user = new User("root","123456") user = new User("lisi","123123")
簡(jiǎn)單畫一下內(nèi)存圖,可以看到user這個(gè)局部變量本來(lái)是指向root這個(gè)對(duì)象,現(xiàn)在改為指向lisi這個(gè)對(duì)象,那么此時(shí)這個(gè)root對(duì)象沒(méi)有人用,假如類似root這樣的對(duì)象非常多的話,那么jvm性能就會(huì)越來(lái)越低,直至最后創(chuàng)建個(gè)對(duì)象可能都要十幾秒,而且堆內(nèi)存總有一天會(huì)裝滿就會(huì)報(bào)內(nèi)存溢出異常;
所以我們就要想辦法把類似root這種對(duì)象給清理掉,這樣才能保證jvm高效的運(yùn)行;
假如虛擬機(jī)沒(méi)有提供gc你覺(jué)得會(huì)怎么樣?其實(shí)也行,只不過(guò)你每次需要你用代碼手動(dòng)釋放不需要的對(duì)象,關(guān)于這點(diǎn)有好處有壞處,好處就是有利于我們對(duì)堆內(nèi)存的控制,壞處就是我們?cè)谝恍┍容^復(fù)雜的程序之中由于手動(dòng)釋放內(nèi)存難免會(huì)出錯(cuò),但是這中錯(cuò)誤還不怎么明顯,可能要你去慢慢調(diào)試好久才能看到!
所以java就把這種工作自己處理了,讓一個(gè)gc線程一直在后臺(tái)運(yùn)行,隨時(shí)準(zhǔn)備清理不需要用的對(duì)象,雖然相當(dāng)程度上會(huì)對(duì)jvm性能造成一些影響,但是由于gc太好用了,我們不用再人為的去關(guān)心垃圾對(duì)象的釋放,簡(jiǎn)化了我們編寫程序的難度,所以這種影響程度完全可以接受!
這里順便一提兩個(gè)基本概念,內(nèi)存泄漏和內(nèi)存溢出:
內(nèi)存溢出(Memory Overflow)比較好理解,就是我們保存對(duì)象需要的空間太大了,但是申請(qǐng)內(nèi)存比較小,于是裝不下,于是就會(huì)報(bào)內(nèi)存溢出異常,比如說(shuō)你申請(qǐng)了一個(gè)integer,但給它存了long才能存下的數(shù),那就是內(nèi)存溢出;專業(yè)點(diǎn)的說(shuō)法就是:你要求分配的內(nèi)存超出了系統(tǒng)能給你的,系統(tǒng)不能滿足需求,于是產(chǎn)生溢出。
內(nèi)存泄漏(Memory Leak)指的就是我們new出來(lái)的對(duì)象保存在堆中但是沒(méi)有釋放,于是堆中內(nèi)存會(huì)越來(lái)越少,會(huì)導(dǎo)致系統(tǒng)運(yùn)行速度減慢,嚴(yán)重情況會(huì)使程序卡死;專業(yè)點(diǎn)的說(shuō)法就是:你用malloc或new申請(qǐng)了一塊內(nèi)存,但是沒(méi)有通過(guò)free或delete將內(nèi)存釋放,導(dǎo)致這塊內(nèi)存一直處于占用狀態(tài)。
對(duì)于我們jvm來(lái)說(shuō),通常情況下我們不用擔(dān)心內(nèi)存泄漏,因?yàn)橛幸粋€(gè)強(qiáng)大的gc在我們程序的背后默默地為我們清理,但是也會(huì)有特殊情況,比如當(dāng)被分配的對(duì)象可達(dá)但已無(wú)用(未對(duì)作廢數(shù)據(jù)內(nèi)存單元的賦值null)即會(huì)引起,至于這個(gè)可達(dá)是什么意思,后面會(huì)慢慢說(shuō)到;
相對(duì)而言內(nèi)存溢出我們比較常見,還有g(shù)c只會(huì)對(duì)堆內(nèi)存進(jìn)行回收,所以靜態(tài)變量是不會(huì)回收的;
再順便提一下另外兩個(gè)小概念,非守護(hù)線程(也叫用戶線程)和守護(hù)線程,看下面這個(gè)丑陋的程序運(yùn)行會(huì)有幾個(gè)線程?。?/p>
public class User{ public static void main(String[] args){ System.out.println("我是java小新人"); } }
兩個(gè)線程,一個(gè)是執(zhí)行main方法的線程,后臺(tái)還有g(shù)c執(zhí)行g(shù)c的線程,在這里,用戶線程就是執(zhí)行main方法的那個(gè)線程,執(zhí)行g(shù)c的線程就是守護(hù)線程,默默地守護(hù)者jvm,假如jvm是雅典娜,那么守護(hù)線程就是黃金圣斗士;
當(dāng)用戶線程停止之后整個(gè)程序直接停止,守護(hù)線程也會(huì)終止;但是黃金圣斗士掛了雅典娜還是可以好好活著的繼續(xù)愉快的玩耍的;
2.堆內(nèi)存結(jié)構(gòu)
哎,內(nèi)存中的結(jié)構(gòu)如果真的要通過(guò)源代碼去看,簡(jiǎn)直讓人崩潰,除了專業(yè)搞這方面的不然真的很難懂,本來(lái)我想自己畫一下草圖了,發(fā)現(xiàn)太丑陋了,于是去順手借了一張圖:
途中可以很清楚的看到,整塊堆內(nèi)存分為年輕人聚集的地方和老年人聚集的地方,年輕人比較少趨勢(shì)占用1/3空間(新生代),老年人比較多就占用2/3的空間(老年代),然而啊,年輕人又要分分類,分別是Eden區(qū)占新生代8/10,F(xiàn)rom Survivor區(qū)占新生代1/10,To Survivor區(qū)占新生代1/10,emmm。。。我特意查了一下百度翻譯,Eden---->樂(lè)園,Survivor----->幸存者;哦~~~我感覺(jué)我仿佛明白了命名人的意圖!
那么新生代和老年代到底是干什么的呢?我們創(chuàng)建的對(duì)象是放在哪里?。?/p>
新生代:java對(duì)象申請(qǐng)內(nèi)存和存放對(duì)象的地方,而且存放的對(duì)象都是那種死的比較快的對(duì)象,很多時(shí)候創(chuàng)建沒(méi)多久就清理掉了,那些活的時(shí)間比較長(zhǎng)的對(duì)象都被移動(dòng)到了老年代。
老年代:存大對(duì)象比如長(zhǎng)字符串、數(shù)組由于需要大量連續(xù)的內(nèi)存空間,可以直接進(jìn)入老年代;還有長(zhǎng)期存活的對(duì)象也會(huì)進(jìn)入老年代,具體是多長(zhǎng)時(shí)間呢,其實(shí)默認(rèn)就是經(jīng)過(guò)15 對(duì)新生代的清理(Minor Gc)還能活著的對(duì)象。
而垃圾收集器對(duì)這兩塊內(nèi)存有兩種行為,一種是對(duì)新生代的清理,叫做Minor Gc,另外一種是對(duì)老年代的清理被叫做Major Gc。
順便提一點(diǎn):很多博客中都把Major GC和Full GC說(shuō)成是一種,其實(shí)還是有區(qū)別的,因?yàn)楹芏鄇ava虛擬機(jī)的實(shí)現(xiàn)不一樣,所以就有各種各樣的名稱,比如Minor Gc又叫做Young GC,Major GC也可以叫做Old GC,但是Full GC卻有點(diǎn)不同,F(xiàn)ull GC 是清理整個(gè)堆空間 —— 包括年輕代、老年代和永久代(也叫做方法區(qū))。因此 Full GC 可以說(shuō)是 Minor GC 和 Major GC 的結(jié)合。當(dāng)然在我們這里,為了好理解我們也就把Full GC當(dāng)作Major GC就可以了?!?/p>
3.篩選清理對(duì)象
GC要工作的話,必須首先知道哪些對(duì)象要被清理,你想一下,在新生代和老年代有這么多對(duì)象,怎么篩選會(huì)又快又省事呢?可以有以下兩種方法
1.引用計(jì)數(shù)算法,相當(dāng)于給你創(chuàng)建的對(duì)象偷偷的添加一個(gè)計(jì)數(shù)器,每引用一次這個(gè)對(duì)象,計(jì)數(shù)器就加一,引用失效就減一,當(dāng)這個(gè)計(jì)數(shù)器為0的時(shí)候,說(shuō)明這個(gè)對(duì)象沒(méi)有變量引用了,于是我們就可以說(shuō)這個(gè)對(duì)象可以被清理了
2.根搜索算法(jvm用的就是這個(gè)),這個(gè)怎么理解呢?你可以想象現(xiàn)在有一個(gè)數(shù)組,這個(gè)數(shù)組里面包含了一些東西的引用,我們將這個(gè)數(shù)組叫做”GC Root“,然后我們根據(jù)這個(gè)數(shù)組中的引用去找到對(duì)應(yīng)的對(duì)象,看看這個(gè)對(duì)象中又引用了哪些對(duì)象,一直往下找,這樣就形成了很多線路,在這個(gè)線路上的對(duì)象就叫做”可達(dá)對(duì)象“,不在這個(gè)線路上的對(duì)象就是不可達(dá)對(duì)象,而不可達(dá)對(duì)象也就是我們要清理的對(duì)象;
其中可以作為GC Root的對(duì)象:
(1).類中的靜態(tài)變量,當(dāng)它持有一個(gè)指向一個(gè)對(duì)象的引用時(shí),它就作為root
(2).活動(dòng)著的線程,可以作為root
(3).一個(gè)Java方法的參數(shù)或者該方法中的局部變量,這兩種對(duì)象可以作為root
(4).JNI方法中的局部變量或者參數(shù),這兩種對(duì)象可以作為root
(5).其它。
關(guān)于這個(gè)根搜索算法專業(yè)一點(diǎn)的說(shuō)法就是:通過(guò)一系列的名為“GC Root”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所有走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Root沒(méi)有任何引用鏈相連時(shí)(用圖論來(lái)說(shuō)就是GC Root到這個(gè)對(duì)象不可達(dá)時(shí)),證明該對(duì)象是可以被回收的。
4.進(jìn)行垃圾回收
前面已經(jīng)篩選出了我們要清理的對(duì)象,但是怎么清理比較快呢?難道要一個(gè)一個(gè)對(duì)象慢慢刪除嘛?就好像你要清理手機(jī)中的垃圾,你會(huì)一個(gè)應(yīng)用一個(gè)應(yīng)用去慢慢清理數(shù)據(jù)嗎?當(dāng)然不可能,這也太浪費(fèi)時(shí)間了!我們當(dāng)然是用手機(jī)管家或者360管家先把要清理的東西給收集起來(lái)放在一起,然后我們一清理就是全部,一個(gè)字,爽!
ok,在這里也一樣,我們要想辦法把所有的要清理的對(duì)象給放在一起清理,有什么辦法呢?
1.標(biāo)記-----清除算法:這種方法分為兩步,先標(biāo)記然后清除,其實(shí)就是需要回收的對(duì)象標(biāo)記一下,然后就是把有標(biāo)記的對(duì)象全部清理即可;這種方式比較適合對(duì)象比較少的內(nèi)存,假如對(duì)象太多標(biāo)記都要好半天,更別說(shuō)清除了,而且用這種方法清除的內(nèi)存空間會(huì)東一塊西一塊,下次再創(chuàng)建一個(gè)大的對(duì)象可能會(huì)出問(wèn)題1
2.復(fù)制算法:按內(nèi)存容量將內(nèi)存劃分為等大小的兩塊。每次只使用其中一塊,當(dāng)這一塊內(nèi)存滿后將尚存活的對(duì)象復(fù)制到另一塊上去,把已經(jīng)使用的那塊內(nèi)存直接全部清理掉;這種方法最大的缺陷就是耗內(nèi)存啊,只能用總內(nèi)存的一半,而且如果對(duì)象很多復(fù)制都要花很多時(shí)間。
3.標(biāo)記----整理算法:結(jié)合以上兩種方法優(yōu)缺點(diǎn)進(jìn)行改良的一種方法,標(biāo)記和第一種方法一樣把要清理的對(duì)象做好標(biāo)記,然后把所有標(biāo)記的對(duì)象移動(dòng)到本內(nèi)存的一個(gè)小角落,最后集中力量對(duì)那個(gè)小角落進(jìn)行消滅
4.分代收集算法:這是集中了上面三種方法的優(yōu)點(diǎn)所實(shí)現(xiàn)的一種最好的方法,是目前大部分JVM所采用的方法,這種算法的核心思想是根據(jù)對(duì)象存活的時(shí)間不同將內(nèi)存劃分為不同的域,一般情況下將GC堆劃分為新生代和老年代;新生代的特點(diǎn)是每次垃圾回收時(shí)都有大量垃圾需要被回收,少數(shù)對(duì)象存活,因此可以使用復(fù)制算法;老年代的特點(diǎn)是每次垃圾回收時(shí)只有少量對(duì)象需要被回收,可以選用”標(biāo)記--清除方法“”或者標(biāo)記--整理算法“
所以目前大部分JVM的GC都是使用分代收集算法。
5.執(zhí)行GC的步驟
前面說(shuō)了這么多無(wú)非是介紹堆的內(nèi)部結(jié)構(gòu),然后怎么找到要被清理的對(duì)象,然后為了提高效率怎么清理最快!
現(xiàn)在我們就大概說(shuō)說(shuō)GC的清理步驟(詳細(xì)版):
1.我們創(chuàng)建對(duì)象的時(shí)候會(huì)進(jìn)行一個(gè)判斷,極少數(shù)很大的對(duì)象直接放進(jìn)老年代中,除此之外所有新創(chuàng)建的對(duì)象都放進(jìn)新生代的Eden區(qū)中;
2.此時(shí)新生代中只有Eden區(qū)中有對(duì)象,兩個(gè)Survivor區(qū)中是空的;當(dāng)我們創(chuàng)建了很多對(duì)象,使得Eden區(qū)快滿的時(shí)候第一次GC發(fā)生(就是執(zhí)行了一次Minior GC),Eden區(qū)和”From“區(qū)(此時(shí)“From”區(qū)是空的)存活的對(duì)象將會(huì)被移動(dòng)到Surviver區(qū)的“To”區(qū),并且為每個(gè)對(duì)象設(shè)置一個(gè)計(jì)數(shù)器記錄年齡,初始值為1;每進(jìn)行一次GC,會(huì)給那些存活的對(duì)象設(shè)置一個(gè)年齡+1 的操作,默認(rèn)是當(dāng)年齡達(dá)到15歲,下次GC就會(huì)直接把這種”老油條“丟到老年代中。
3.Minior GC之后,會(huì)進(jìn)行一個(gè)比較厲害的操作,就是將”To“區(qū)和”From“換個(gè)名字,沒(méi)錯(cuò),就是換個(gè)名字,然后進(jìn)行下一次Minior GC。
4.由于又創(chuàng)建了很多對(duì)象使得Eden區(qū)要滿了,于是又一次Minior GC,Eden區(qū)還存活的對(duì)象會(huì)直接移動(dòng)到Surviver區(qū)的“To”區(qū),此時(shí)”From“區(qū)(這里就是交換名字之前的”To“區(qū))中的對(duì)象有兩個(gè)地方可以去,要么年齡滿15歲了去老年代,要么就移動(dòng)到”To“區(qū)
5.此時(shí)我們看一下,只有”To“區(qū)的對(duì)象是活著的,Eden區(qū)都是垃圾對(duì)象可以直接全部清理,”From“區(qū)是空的;不管怎樣,在進(jìn)行下一次Minior GC之前保證名為”To“的Survivor區(qū)域是空的就ok了
6.當(dāng)老年代中快要裝滿之后,就會(huì)進(jìn)行一次Major GC,這個(gè)清理事件很慢,至少比Minior GC慢十幾倍,甚至更多,所以我們盡量要少執(zhí)行Major GC
注意:如果在移動(dòng)過(guò)程中”To“ 區(qū)被填滿了,剩余的對(duì)象會(huì)被直接移動(dòng)到老年代中。還有在每次Minior GC之前會(huì)先進(jìn)性判斷,只要老年代里面的連續(xù)空間大于新生代對(duì)象總大小或者歷次晉升的平均大小進(jìn)行Minor GC,否則進(jìn)行Major GC。
簡(jiǎn)化版:
(1)Eden 區(qū)活著的對(duì)象 + From Survivor 存儲(chǔ)的對(duì)象被復(fù)制到 To Survivor ;
(2)清空 Eden 和 From Survivor ;
(3)顛倒 From Survivor 和 To Survivor 的邏輯關(guān)系: From 變 To , To 變 From 。
(4)老年代的Major GC執(zhí)行時(shí)間很長(zhǎng),盡量少執(zhí)行
只有在Eden空間快滿的時(shí)候才會(huì)觸發(fā) Minor GC 。而 Eden 空間占新生代的絕大部分,所以 Minor GC 的頻率得以降低。當(dāng)然,使用兩個(gè) Survivor 這種方式我們也付出了一定的代價(jià),如 10% 的空間浪費(fèi)、復(fù)制對(duì)象的開銷等。
6.知識(shí)點(diǎn)補(bǔ)充
通過(guò)查看了很多大佬的博客看到的很多有關(guān)的東西還是挺有趣的,于是簡(jiǎn)單做個(gè)小筆記:
6.1.新創(chuàng)建的對(duì)象是在堆中的新生代的Eden區(qū),由于堆中內(nèi)存是所有線程共享,所以在堆中分配內(nèi)存需要加鎖。而Sun JDK為提升效率,會(huì)為每個(gè)新建的線程在Eden上分配一塊獨(dú)立的空間由該線程獨(dú)享,這塊空間稱為TLAB(Thread Local Allocation Buffer)。在TLAB上分配內(nèi)存不需要加鎖,因此JVM在給線程中的對(duì)象分配內(nèi)存時(shí)會(huì)盡量在TLAB上分配。如果對(duì)象過(guò)大或TLAB用完,則仍然在堆上Eden區(qū)或者老年代進(jìn)行分配。如果Eden區(qū)內(nèi)存也用完了,則會(huì)進(jìn)行一次Minor GC(young GC)。
6.2.很多人認(rèn)為方法區(qū)(或者HotSpot虛擬機(jī)中的永久代)是沒(méi)有垃圾收集的,Java虛擬機(jī)規(guī)范中確實(shí)說(shuō)過(guò)可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾收集,而且在方法區(qū)進(jìn)行垃圾收集的“性價(jià)比”一般比較低:在堆中,尤其是在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠(yuǎn)低于此。
6.3對(duì)象調(diào)用.finalize方法被調(diào)用后,對(duì)象一定會(huì)被回收嗎?
在經(jīng)過(guò)可達(dá)性分析后,到GC Roots不可達(dá)的對(duì)象可以被回收(但并不是一定會(huì)被回收,至少要經(jīng)過(guò)兩次標(biāo)記),此時(shí)對(duì)象被第一次標(biāo)記,并進(jìn)行一次判斷,如果該對(duì)象沒(méi)有調(diào)用過(guò)或者沒(méi)有重寫finalize()方法,那么在第二次標(biāo)記后可以被回收了;否則,該對(duì)象會(huì)進(jìn)入一個(gè)FQueue中,稍后由JVM建立的一個(gè)Finalizer線程中去執(zhí)行回收,此時(shí)若對(duì)象中finalize中“自救”,即和引用鏈上的任意一個(gè)對(duì)象建立引用關(guān)系,到GC Roots又可達(dá)了,在第二次標(biāo)記時(shí)它會(huì)被移除“即將回收”的集合;如果finalize中沒(méi)有逃脫,那就面臨被回收。因此finalize方法被調(diào)用后,對(duì)象不一定會(huì)被回收。
6.4.如果在Survivor空間中相同年齡所有對(duì)象大小總和大于Survivor空間的一半,年齡大于或者等于該年齡的對(duì)象直接進(jìn)入老年代。不需要等到15歲。
關(guān)于java虛擬機(jī)垃圾收集器的由來(lái)就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。