java與c++之間有一堵由動態(tài)分配和垃圾收集技術(shù)所圍成的高墻同规,墻內(nèi)的想出去循狰,墻外的人卻想進來。
然后言歸正傳券勺,我這篇文章是想把垃圾收集和內(nèi)存分配提出來講一下的绪钥。重申下個人觀點:我說的都是別人講過的東西,老生常談关炼,然后會加一下自己的理解和看法程腹,甚至有的時候有些東西也是自己的猜測∪宸鳎可能會有錯的寸潦,歡迎指出,有異議的也歡迎交流社痛。然后我之所以寫出來一來是我覺得單獨看一遍書其實理解的不深见转,自己打一遍,而且為了讓文章言之有物我還要自己推敲琢磨蒜哀,這樣對我自己的理解本身是一種好處斩箫。另外就是如果讀者看到了,覺得有幫助凡怎,那也算是一件好事校焦。畢竟我是打在筆記本里自己看和打在簡書里大家看,對我而言是差不多的统倒!還有寨典!我也是在學習階段,我也是小白房匆,剛剛也說了也會有錯誤耸成。但是我不覺得一定要特別特別厲害才有資格寫文章(最近有人問我整天寫文章能掙錢咋的,所以針對這種看法我做一下解釋井氢。)
背景簡介
因為我這次看的是書弦追,所以介紹的比較全面劲件,不再是視頻那種單純的知識點講解。所以有一些背景簡介零远,這里也和大家絮叨絮叨。
首先牵辣,垃圾收集(Garbage Collection,簡稱GC)其實并不是java的半生產(chǎn)物纬向。1960年的Lisp就使用了。目前java的內(nèi)存的動態(tài)分配和內(nèi)存回收技術(shù)已經(jīng)比較成熟逾条,但是M栋!薇缅!當需要排查各種內(nèi)存溢出,內(nèi)存泄露的時候泳桦,當垃圾手機成為系統(tǒng)達到更高并發(fā)量的瓶頸的時候,我們還是要對內(nèi)存是是必要的監(jiān)控和調(diào)節(jié)灸撰。
上一篇文章我們說了內(nèi)存分為:堆,棧浮毯,方法區(qū),程序計數(shù)器债蓝。其中堆,方法區(qū)的隨著jvm而生饰迹,所有線程共享芳誓。而虛擬機棧和本地棧統(tǒng)稱為棧余舶,和程序計數(shù)器是線程隔離的。隨著線程而生锹淌,也隨線程而滅匿值。棧中的棧幀隨著方法的進入和退出執(zhí)行入棧和出棧。因為棧幀分配的內(nèi)存是編譯期就已知的赂摆,所以這個區(qū)域的回收具有確定性挟憔,我們不需要過多的考慮回收的問題。因為方法或者線程結(jié)束的時候烟号,內(nèi)存就自然跟著回收了曲楚。
而java堆則不一樣,我們只有在程序運行時才知道會創(chuàng)建什么對象褥符,這部分的內(nèi)存分配和回收都是動態(tài)的,我們所說的垃圾收集器關(guān)注的也是這部分內(nèi)存抚垃。
對象已死么喷楣?
java堆中存放的是所有對象的實例。垃圾收集器回收之前鹤树,要判斷這個對象哪些還活著铣焊,哪些死了(就是不會再被使用的對象)。
引用計數(shù)法
通俗來講罕伯,就是判斷對象是否存活的一個算法:給對象中添加一個引用計數(shù)器(這里要區(qū)別于程序計數(shù)器)曲伊。每當有一個地方引用它時,計數(shù)器的值+1.引用失效時追他,計數(shù)器的值-1.如果說一個對象的計數(shù)器的值是0坟募,就說明這個對象是不可用的。
客觀來說這個即用計數(shù)算法實現(xiàn)簡單邑狸,而且效果也不錯懈糯,理解起來很容易。但是很多主流的jvm虛擬機并沒有選用引用計數(shù)算法來管理內(nèi)存单雾。因為它有個弊端赚哗,或者說漏洞:那就是它很難解決對象之間的相互循環(huán)引用的問題。舉個例子:兩對象A,B互相引用屿储,除此之外沒有任何別的引用渐逃。實際上這個兩個對象已經(jīng)不可能再被訪問了茄菊。但是計數(shù)器的值不是0助赞,導(dǎo)致GC不能回收他們雹食。
可達性分析算法
據(jù)說主流的商用程序語言是主流實現(xiàn)都是通過可達性分析來判斷對象是否存活的群叶。這個算法的基本思路就是通過一系列的成為”GC Roots“的對象作為起始點街立,從這些節(jié)點開始向下搜索埠通,搜索走過的路徑稱為引用鏈端辱,當一個對象到”GC Roots“沒有任何引用鏈相連,則這個對象是不可用的荣病。(也叫這個根節(jié)點到對象不可達)个盆。
這個根節(jié)點是可能是:棧中引用的對象朵栖。方法區(qū)中靜態(tài)屬性引用的對象陨溅。方法區(qū)中常量引用的對象等等。
其實我覺得這個也還算好理解狠鸳。我們可以把所謂的根節(jié)點看作是一個樹根件舵,想判斷一個葉子是不是屬于這棵樹的就是從樹根開始順著樹干铅祸,樹枝查找。最后發(fā)現(xiàn)能連上就說明這個樹葉是這棵樹的涡扼,但是要是發(fā)現(xiàn)樹根到這個葉子連不上吃沪,則說明這個樹葉不是這棵樹的什猖,也就是不可達不狮。而在jvm虛擬機中這種不可達代表著不可用。(書上的圖不錯推掸,我盡量畫一下讓大家可以直觀的看下谅畅。)
再談引用
無論是通過引用計數(shù)法,還是可達性分析臼婆,我們判斷對象是否或者都和引用有關(guān)。在JDK1.2以后(之前的太遙遠就不說了)java堆引用的概念進行了擴充:將引用分為強引用,軟引用,弱引用无拗,虛引用四種英染。這四種引用強度一次減弱被饿。
強引用:類似Object o = new Object()這種狭握,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象哎垦。
軟引用:描述一些還有用但是非必須的對象漏设。在系統(tǒng)將要發(fā)生內(nèi)存溢出之前,才會把軟引用關(guān)聯(lián)的對象列進回收范圍之中進行二次回收损俭。
弱引用:也是描述非必須的對象的杆兵。但是它比軟引用更弱一些仔夺。當垃圾收集器工作時缸兔,會回收掉只被弱引用關(guān)聯(lián)的對象。
虛引用:是最弱的一種引用關(guān)系昂拂。是否有虛引用完全不會對其生存時間有影響格侯。也無法通過虛引用來獲取一個對象實例联四。
其實我們可以用自己的手機來聯(lián)想這幾種引用關(guān)系撑教。
虛引用就是內(nèi)存垃圾伟姐,我們用智能機的小伙伴都知道,手機自己呆著呆著就總莫名其妙的出現(xiàn)垃圾倒戏,這個時候一般有提示:您的手機已經(jīng)有XXg的垃圾杜跷,請及時清理。然后你點一個清理就沒了憋槐!你都不一定知道系統(tǒng)都清理了什么玩意兒阳仔。
弱引用就是微信QQ的聊天記錄啊扣泊,咋說呢延蟹,有時候顯得沒事了,覺得聊天記錄也沒大作用斥杜,刪了就刪了吧蔗喂。然后一鍵清理好幾G的內(nèi)存缰儿,感覺自己萌萌噠散址。
軟引用這個就比較有意思了,它屬于你挺有用的义起,比如好幾個G的電影师崎。作為精神食糧犁罩,沒事就看看床估,還是很喜歡滴丐巫!但是真的到了內(nèi)存不足,干啥都提示沒空間了的時候碑韵,也還是能下定決心把一些你懂得的資源刪除的祝闻,這個就是軟引用遗菠。關(guān)鍵時刻可以舍棄的東西辙纬。
至于強引用,差不多就是手機里的微信堤框,qq蜈抓,支付寶了吧昂儒。要手機的目的不就是這些么?刪除是不可能刪除的腊嗡,換手機都不能刪除的這種拾酝。
生存還是死亡
這是一個問題蒿囤,比今天吃啥還不好解決。即使在可達性分析中不可達的對象底挫,也不是立刻就殺死了建邓,他們還有個“緩刑”的極端睁枕。這個就涉及到了對象的finalize()方法了,在effective java第三版中也有專門的一章講解這個方法注簿,這里知識簡單的說一下,如果感興趣的同學可以自己區(qū)搜索finalize()栅隐。
如果一個對象被判定不可達租悄,這個時候我們就會調(diào)用它的finalize()方法(如果對象的finalize()方法沒有覆蓋或者finalize()已經(jīng)被調(diào)用過了則系統(tǒng)判斷沒有必要調(diào)用了泣棋。然后finalize()方法只會被系統(tǒng)自動調(diào)用一次)畔塔,finalize()方法是一個對昂逃脫死亡的最后一次機會澈吨。如果在這個方法里成功的和外界建立關(guān)聯(lián)了。就是把自己變成可用了修赞。就不會死了柏副。如果調(diào)用finalize()方法還是沒讓自己變得有用割择,那么就真的被回收了萎河。(我超級喜歡這段話,感覺就是電視劇里面演的换可,人被敵人抓住了厦幅,趕緊透漏情況确憨,說的情報有用可以多活一會,啥也不知道直接就被滅口了)吞歼。
然后其實這個finalize()方法篙骡,建議大家避免使用它的丈甸。因為不確定的因素太多了睦擂。這個就不詳細說明了,如果我有機會把effective java也寫出來淘正,那里會有詳細的說明為啥不推薦使用終結(jié),清理方法述呐。
回收方法區(qū)
昨天已經(jīng)說過了方法區(qū)也有人叫做“永久代”。但是這個不嚴謹缤谎。雖然java虛擬機規(guī)范中確實說過了可以不在方法區(qū)進行垃圾收集,而且在方法區(qū)進行垃圾收集的性價比比較低项郊。但是該有還是要有的啊着降。
永久代的垃圾收集主要是兩部分內(nèi)容:廢棄常量和無用的類蓄喇。
廢棄常量:這個很好理解,比如常量池有一個字符串”abc“钱骂,但是沒有任何string對象引用指向這個”abc“,也沒有地方用了或者字面量。那么這個“abc”就是廢棄的肮蛹。必要的話這個“abc”就會被系統(tǒng)清理出常量池。
無用的類:這個要同時滿足三個條件:
- jaca堆中不存在這個類的實例
- 加載該類的ClassLoader已經(jīng)被回收
- 無法在任何地方通過反射訪問該類的方法。也就是該類對應(yīng)的java.lang.Class對象也沒有被引用。
虛擬機可以對滿足上述三個條件的類進行回收脓匿。但是這里僅僅是”可以“。是否對類進行回收還是看要虛擬機的配置的。
垃圾收集算法
終于要講到正題了妙色,由于垃圾收集算法的實現(xiàn)比較復(fù)雜,而且各個平臺的虛擬機操作內(nèi)存的方法也不一樣,所以這里就是介紹幾種算法的思想萧落。
標記-清除算法:這個是最基礎(chǔ)的收集算法了。如它的名字一樣,主要是兩個階段:標記蜜唾,清除。首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象掩完。之所以說他基礎(chǔ),是因為后續(xù)的收集算法都是基于這種思路并對其不足進行改進得到的。它主要的問題有兩個:
- 效率問題存淫,標記和清除的效率都不高。
-
標記清除之后會產(chǎn)生大量的不連續(xù)的內(nèi)存碎片荚虚“媸觯空間碎片太多導(dǎo)致內(nèi)存空間利用率不高。
標記-清理
如上圖做完一波清理操作,該清除的倒是清除了,但是如果這個時候我們要存入一個6個空間位大小的數(shù)據(jù)场斑,內(nèi)存是夠用的,但是沒有哪一塊能放下,這不就是空間的浪費了么脖隶?
復(fù)制算法:復(fù)制算法是為了解決效率問題块仆。他將可用內(nèi)存劃分為一樣一樣的兩塊悔据。每次只是用一塊。在一塊內(nèi)存用完了,就把其中活著的對象復(fù)制到另一塊上面(從頭開始連著放,不會產(chǎn)生空間碎片)期吓。然后把之前滿了那塊空間都刪除了蛀醉。實現(xiàn)起來簡單,運行起來也高效。但是缺點就是將內(nèi)存縮小為原來的一半帚桩。代價不小啊儡蔓。
據(jù)說現(xiàn)在流行的商用虛擬機都是采用這種算法的喂江,而且有一點:新生代中的98%的對象朝生晚死,死的賊快卵惦,所以不需要1:1分配內(nèi)存的。上一篇說到的Eden赴邻,Survivor就順勢而出了。一般默認Eden和Survivor的空間比8:1.。內(nèi)存劃分為兩個Survivor和一個Eden墨榄。然后每次是Eden和使用中的Survivor中的活著的復(fù)制到另一個Survivor中逢并,然后清理掉Eden和Survivor的空間砍聊。接下來使用Eden和剛剛被復(fù)制過來的Survivor。這樣做相當于每次只有一個Survivor空間被浪費。按照默認比例:8:1:1.也就是每次只浪費百分之十的內(nèi)存竹椒。
當然了翘贮,有時候Survivor可能空間不夠了扯再,可以暫時先放老年代的內(nèi)存。(重點是暫時。反正就是有借要有還的那種钾军。)
標記-整理算法:看名字都能看出來袖扛,就是標記清除算法的進階版蛆封。標記就是標記清除中的標記過程,只不過這個不是標記完了直接清除就完事了,而是所有活著的對象都向一端移動簿寂。其實就是把活著的都聚在前面的一堆兒。最后把死了的都清除了。
分代收集算法:這個其實就是一個思想跃赚。跟咱們?nèi)怂频模瑒偵鰜淼男『喝菀壮霈F(xiàn)各種意外,所以我們小心又全面的呵護著。但是青年中年一般很少莫名其妙無緣無故吃個飯噎死,洗個臉淹死啥的少孝。就不用那么呵護了柴底。我們對小孩子的照顧要是對成年人也那么照顧,人家愿不愿意不說鸿脓,單說照顧的這個人都容易累死在塔。所以代碼里也是拨黔。你剛創(chuàng)建可以用完就沒用了蓉驹,這個時候容易朝生晚死,生命比較短甜刻。叫做新生代章贞。但是很少你一直用著的東西突然就不用了。這種東西用的次數(shù)多了反而不太容易死了,就叫做老年代。一般波動就不大了。
上面講的復(fù)制算法一般就是新生代的垃圾收集使用的族吻。而標記-整理算法一般就是老年代使用的垃圾收集算法超歌。其實仔細想想就能明白為什么懊悯。
垃圾收集器
其實剛剛說的那些算法都是內(nèi)存回收的方法論桃焕,而垃圾收集器就是內(nèi)存回收的具體實現(xiàn)。java虛擬機規(guī)范中沒有明確規(guī)定垃圾收集器。所以不同廠商因篇,不同版本的收集器是不同的虽界。這里討論的是比較常用的一些。
首先收集器是分代的,這個上文提到過。有的新生代收集器和老年代收集器可以共同工作,但是有的就不可以。而且沒有最好的收集器。只有最合適。接下來具體介紹一下:
Serial收集器:它是最基本的,也是歷史最老的收集器精续。他的兩個特點:
- 單線程工作
- 垃圾收集的時候停止任何別的線程工作凫乖。
其實我看了書中介紹的這段真的很有意思,生動而且形象(下面的引用是書中的原話,我用鍵盤敲了出來而已)。
Serial收集器是JAVA虛擬機中最基本、歷史最悠久的收集器,在JDK 1.3.1之前是JAVA虛擬機新生代收集的唯一選擇。Serial收集器是一個,但它的“單線程”的意義并不僅僅是說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作沾谜,更重要的是在它進行垃圾收集時媳否,必須暫停其他所有的工作線程力图,直到它收集結(jié)束赘那。這項工作實際上是由虛擬機在后臺自動發(fā)起和自動完成的,在用戶不可見的情況下把用戶的正常工作的線程全部停掉,這對很多應(yīng)用來說都是難以接受的。
對于這種惡劣的體驗,虛擬機的設(shè)計者表示完全理解宏蛉,但也表示非常委屈:“你媽媽在給你打掃房間的時候嗅义,肯定也會讓你老老實實地在椅子上或房間外待著,如果她一邊打掃式塌,你一邊亂扔紙屑尾菇,這房間還能打掃完嗎?”這確實是一個合情合理的矛盾缆八,雖然垃圾收集這項工作聽起來和打掃房間屬于一個性質(zhì)奖恰,但實際上可能肯定還要比打掃房間復(fù)雜得多啊。
其實它雖然存在自己的不足错负,但它依然是虛擬機運行在Client模式下但默認新生代收集器油航。它有著優(yōu)于其他收集器的地方:簡單而高效,對于限定單個CPU的環(huán)境來說奠伪,Serial收集器由于沒有線程交互的開銷脸狸,專心做垃圾收集自然可以獲得最高的單線程收集效率卿啡。在用戶的桌面應(yīng)用場景中,分配給虛擬機管理的內(nèi)存一般來說不會很大,停頓時間完全可以控制在幾十毫秒最多一百多毫秒以內(nèi)清酥,只要不是頻繁發(fā)生,這點停頓還是可以接受的。所以,Serial收集器對于運行在Client模式下的虛擬機來說是一個很好的選擇忆某。
ParNew收集器:ParNew收集器其實就Serial的多線程版本棒坏。書中這里介紹了一下各個版本的配合和使用情況测暗。但是因為這里是只是想簡單的懂得收集器作用稚字,原理昌讲。所以這一段就不寫了礼搁。除了能多線程剩下與Serial收集器是差不多的。
注意兩個概念:
- 在單核中ParNew收集器沒有Serial收集器效率高负拟,因為有線程切換的開銷厨姚。
- 在雙核中ParNew收集器和Serial收集器效率差不多拭抬。
- 在3核及以上ParNew收集器才比Serial收集器效率高贬派。
Parallel Scavenge收集器”:它是一個新生代的收集器镐躲,也是使用復(fù)制算法的收集器裆熙。又是并行的多線程收集器入录。但是它不關(guān)注縮短垃圾收集的時間僚稿。更關(guān)注系統(tǒng)的吞吐量蟀伸。
因此啊掏,Parallel Scavenge收集器也被稱為“吞吐量優(yōu)先”收集器。
吞吐量計算公式:
吞吐量 = cpu運行程序時間/cpu運行程序時間+GC時間
Serial Old收集器:Serial收集器的老年代版本谢肾÷瑁看名字就能看出來微姊。使用的是標記-整理算法兢交。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本。是多線程和標記-整理算法酪穿。一般在注重吞吐量以及cpu資源敏感的場合被济,可以優(yōu)先考慮Parallel Scavenge+Parallel Old涧团。
CMS收集器:CMS垃圾回收器的全稱是Concurrent Mark-Sweep Collector,從名字上可以看出兩點钮追,一個是使用的是并發(fā)收集,第二個是使用的收集算法是Mark-Sweep(標記-清除算法)轧叽。
它的運作過程相對于前面幾種收集器來說要更復(fù)雜一些犹芹,整個過程分為6個步驟鞠绰,包括(我的書是第二版蜈膨,只有四步翁巍。但是我查閱的資料都說六步休雌,這里按六步寫了):
- 初始標記(CMS initial mark)
- 并發(fā)標記(CMS concurrent mark)
- 并發(fā)預(yù)清理(CMS-concurrent-preclean)
- 重新標記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
- 并發(fā)重置(CMS-concurrent-reset)杈曲。
因為這個介紹的篇幅比較多恰响,我就用我個人的話來理解吧涌献。
它只有初始標記和重新標記是需要暫停別的線程的燕垃。而且這兩個行為的時間都很快。至于并發(fā)標記和并發(fā)清除都可以和用戶線程一起工作您旁,不用”stop the world“被冒。這就讓用戶用起來很舒服。但是也有一些缺點:
- 對cpu資源敏感昨悼。
- 無法處理浮動垃圾率触。
- 采用標記-清除方法,會產(chǎn)生內(nèi)存空間碎片穴张。
G1收集器
這個是當今收集器技術(shù)發(fā)展最前沿的成果之一皂甘。反正聽起來就高大上有木有悼凑?
G1的特點:
- 并行與并發(fā)。
- 分代收集(之前的收集器都是只針對年輕代或者老年代渐夸。G1是都可以使用)
- 空間整合性好渔欢。
-
可預(yù)測的停頓奥额。
在G1之前,cms是很火的态坦,所以這里好多都是G1和cms的對比棒拂。然后我看的書可能比較老,那時候還沒有什么G1的實際使用數(shù)據(jù)(我估計現(xiàn)在有了吧谜诫,但是沒有特意去查攻旦。)
收集器之間搭配關(guān)系
內(nèi)存分配與回收策略
我們之前一直說什么新生代牢屋,老年代的槽袄。這些到底什么區(qū)別遍尺?怎么劃分的涮拗?
首先一般創(chuàng)建一個對象三热,在新生代Eden中分配內(nèi)存。如果Eden中沒有足夠的空間呐能,虛擬機將發(fā)起一次Minor GC(新生代GC从藤,指發(fā)生在新生代的垃圾收集動作,所有的Minor GC都會觸發(fā)全世界的暫停(stop-the-world),停止應(yīng)用程序的線程悯搔,不過這個過程非常短暫舌仍。)
如果這個對象需要大量連續(xù)內(nèi)存空間铸豁,我們可以通過設(shè)置,讓大對象直接進入老年代在刺。這樣做的目的是為了防止大對象在Eden和Survicor來回來去復(fù)制头镊。
虛擬機采用了分代收集的思想來管理內(nèi)存相艇,那么內(nèi)存回收的時候必須能分辨哪些是新生對象,哪些是老年對象坛芽。
為了做到這一點,虛擬機給每個對象定義了一個年齡計數(shù)器获讳∨夂浚活過一次Minor GC就漲一歲。當年齡到了一定程度(默認15歲侠畔,這個閾值可以改)损晤,就可以晉升到老年代了尤勋。
好了,這章關(guān)于垃圾收集器和內(nèi)存分配就到這里了最冰。一下午四十頁書暖哨,而且感覺比自己單純的看一遍效果要好得多篇裁。然后全文手打,我也是正在學習的過程中达布,有問題或者不同的意見歡迎指出共同交流學習。
全文手打不易躺苦,如果你覺得有幫到你或者有點用分冈,別吝嗇的點個喜歡和點個關(guān)注哦~~