與其他語言相比,例如c/c++,我們都知道,java虛擬機對于程序中產生的垃圾,虛擬機是會自動幫我們進行清除管理的,而像c/c++這些語言平臺則需要程序員自己手動對內存進行釋放。
雖然這種自動幫我們回收垃圾的策略少了一定的靈活性,但卻讓代碼編寫者省去了很多工作,同時也提高了很多安全性。(因為像C/C++假如你創建了大量的對象,但卻由于自己的疏忽忘了將他們進行釋放,可能會造成內存溢出)。
何為垃圾?
剛才說了,虛擬機會自動幫助我們進行垃圾的清除,那什么樣的對象我們才可以稱為是垃圾對象呢?
假如你創建了一個對象
Man m = new Man();
你用一個變量指向了這個對象,顯然對于這個對象,你可以用變量m對這個對象進行利用,但過了一段時間,你執行了
m = null;
并且也并沒有新的變量來指向剛才創建的對象。此時對于這個沒有任何變量指向的對象,你覺得它還有用處嗎?
顯然,對于這種沒有被變量指向的對象,它是一點卵用也沒有的,它只能在堆隨風漂流。
因此,對于這樣的對象,我們就可以把它稱為垃圾了,它早晚會被垃圾回收器給干掉。
怎么知道它已經是垃圾對象了?
假如代碼是你自己編寫的,你可能知道這個對象啥時候應該被拋棄,你可以隨時讓它成為垃圾對象。
但是,你畢竟是你,虛擬機則沒那么智能。那虛擬機是如何知道的呢?
上面已經說了,沒有變量引用這個對象時,它就是垃圾對象了,基于這個原理,我們可以這樣做啊:
我們可以為這個對象設置一個計數器,初始值為0,假如有一個變量指向它,那么計數器就加1,如果這個變量不在指向它了,計數器就減1。那么我們就可以判斷,如果這個計數器為0的話,那它就是垃圾對象了,否則就是有用的對象。
對于這種方法,我們稱之為引用計數法。
好吧,我們先來夸一夸引用計數法這種方法:
- 實現簡單。
- 效率高(一個if語句就能解決的問題想不高效都難)。
不好意思,接下來得說說它那個致命的缺點。
實際上,對于這種引用計數的方法,假如它遇到對象互相引用的話,是很難解決的。
先看一段代碼:
Man m1 = new Man();
Man m2 = new Man();
//互相引用
m1.instance = m2;//假設Man有instance這個屬性
m2.instance = m1;m1 = null;
m2 = null;
System.gc();//按道理對象應該被回收
這段代碼m1和m2都指向null了,按道理兩個對象已經是無用對象,應該被回收,但是,兩個對象之間彼此有一個instance的屬性互相牽引的對方,導致兩個對象并沒有被回收。
這個缺點夠致命吧?
所以,虛擬機并沒有采用這種引用計數的方法。
可達性分析
除了這種方法,我們還有其他的方法嗎?
答案是有的,必須得有啊。這種方法就是傳說中的可達性分析,(我靠,聽名字是真的高級啊)。它的工作原理是這樣的:
在程序開始時,會建立一個引用根節點(GC Roots),并構建一個引用圖。當需要判斷誰是垃圾時,我們可以從這個根節點進行遍歷,如果沒有被遍歷到的節點則是垃圾對象,否則就是有用對象。如下圖:
這個方法可以解決循環相互引用的問題,但是這個方法并沒有引用計數法高效,畢竟要遍歷圖啊。
總結下判斷是否為垃圾對象的算法:
- 引用計數法。
- 可達性分析。
何時進行垃圾回收
可能有人會覺得這個問題很奇怪,覺得看到垃圾就回收不是很好。對于這個我只能說:
- 看到房間有一點垃圾你會馬上掃?還是等到某個時間點或者當垃圾積累到一定的數量再掃?
- 虛擬機可沒那么智能可以馬上識別這個對象是垃圾對象,它還得遍歷所有對象才能知道有哪些是垃圾對象。
所以說,你總不能幾秒(我們假設幾秒是賊短的時間)就讓虛擬機遍歷一下所有對象吧?
這里先說明一下,當垃圾回收器在進行垃圾回收的時候,為了保證垃圾回收不受干擾,是會暫停所有線程的,此時程序無法對外部的請求進行響應。(因為你想啊,當你在可達性分析的時候,那些引用關系還在不斷著變化,那不很難受)。
而且頻繁的垃圾回收,對于有一些程序,是很影響用戶體驗的,例如你在玩游戲,系統動不動就停頓一下,怕你是要把這游戲給刪了。
所以說,垃圾回收是會等到內存被使用了一定的比例的時候,才會觸發垃圾回收。至于這個比例是多少,這可能就是人為規定的了。
怎么回收?
當我們標記好了哪些是垃圾,想要進行回收的時候,該怎么回收比較好呢?
可能有一些人就覺得奇怪,這還不簡單,看見它是垃圾,直接回收不就得了。
其實這也不無道理,簡單粗暴,直接回收。
是的,確實有這樣的算法,看哪些是被我們標記的垃圾,看見了就直接回收。這種算法我們稱之為標記--清除算法。
標記-清除算法工作原理:就是先標記出所有需要回收的對象,然后在統一回收所有被標記過的對象。
不過,那些人你可別得意啊,因為這種方法雖然簡單暴力,但它有個致命的缺點就是:
標記清除過后,會產生大量的不連續內存碎片,如果不連續的碎片過多的話,,可能會導致有一些大的對象存不進去。這樣,會導致下面兩個問題:
- 有些內存浪費了。
- 對象存不進去,會又一次觸發垃圾回收。
復制算法
為了解決這種問題,另外一種算法出現了---復制算法。就是說,它會將可用的內存按容量劃分成兩塊。然后每次只使用其中的一塊,當這一塊快用完的時候,就會觸發垃圾回收,它會把還存活的對象全部復制到另外一塊內存中去,然后把這塊內存全部清理了。
這樣,就不會出現碎片問題了。
居然幫我們解決了我們必須夸一下它:不僅幫我們解決了問題,而且實現上也簡單、運行也高效。
但是(凡事都有個但是的),它也是有缺點的,缺點很明顯,發現了沒有。假如每次存活的對象都很少很少,那另外一塊內存不是幾乎沒有用到?所以說,這種方法有可能導致另外一半內存幾乎沒用了。內存那么寶貴,這可是很嚴重的問題。
優化策略:可以告訴你,有研究顯示,其實有98%的對象都是朝生夕死的,也就是說,每次存活的對象確實很少很少。既然我們都知道存活的對象很少很少了,那我們干嘛還1:1的比例來分配?所以說,HotShot虛擬機是默認按8:1的比例來分配的。這樣,就不會出現很多內存沒用到的問題了。
可能有人會說,萬一占比為1/9的內存不夠用了怎么辦?不就沒地方存那些活的對象?實際上,當內存不夠用時,可以向其他地方借些內存來使用,例如老年代里的內存。
這里說明一下新生代和老年代:說白了,新生代就是剛剛創建不久的對象,而老年代是已經活了挺久的對象。也就是說,有一些對象是確實活的比較久的,對于這種對象,我們另外給它分配內存來養老,而且垃圾回收時,我們不用每次都來這里查找有沒垃圾對象,因為這些對象是垃圾的幾率會比較小。
下面在簡單介紹另外兩種算法:
- 標記-整理算法:這種算法和標記-清除算法類似,不過它把垃圾清除了之后,會讓存活的對象往一個方向靠攏,以此來整理碎片。
- 分代收集算法:所謂分代就是把對象分成類似上面說的老年代和新生代,在新手代一般每次垃圾回收時死的對象一般都會比較多,而老年代會比較少,基于這種關系,我們就可以采取不同的算法來針對了。
總結下垃圾回收的幾種算法:
- 標記-清除算法。
- 復制算法。
- 標記-整理算法。
- 分代收集算法。
最后給大家幾種垃圾回收器
對于垃圾的回收,你是想一邊運行程序其他代碼一邊進行垃圾回收?還是想把垃圾全收好再來執行程序的其他代碼?雖然說最終使用cpu的時間是一樣,但兩種方式還是有區別的。
下面簡單介紹幾種垃圾回收器,看看他們都使用哪種方。
(1).Serial收集器
serial(串行),看這個英文單詞就知道這是一個單線程收集器。也就是說,它在進行垃圾回收時,必須暫停其他所有線程。顯然,有時垃圾回收停頓的比較久的話,這對于用戶來說是很難受的。
(2).ParNew
這個收集器和Serial很類似,進行垃圾回收的時候,也是得暫停其他所有線程,不過,它可以多條線程工作進行垃圾回收。
(3).Parallel Scavenge收集器
parallel,并行的意思。也是可以多線程進行垃圾回收處理,但是它與ParNew不同。它會嚴格控制垃圾回收的時間與執行其他代碼的時間之間的比例。我們來看一個名詞:吞吐量。
吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。
也就是說,Parallet Scavenge收集器會嚴格控制吞吐量,至于這個吞吐量是多少,這個可以人為設置。
下面兩個收集器重點介紹下
(4).CMS(Concurrent Mark Sweep)收集器
CMS收集器是基于“標記-清除”算法實現的,它的運作過程相對于前面幾種收集器來說要更復雜一些,整個過程分為4個步驟,包括:
- 初始標記(CMS initial mark)
- 并發標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟仍然需要暫停其他線程。但另外兩個步驟可以和其他線程并發執行。初始標記僅僅只是標記一下GCRoots能直接關聯到的對象,速度很快,并發標記階段就是進行GC Roots Tracing的過程 (說白了就是把整個圖都遍歷了,找出沒有的對象),
而重新標記階段則是為了修正并發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。
由于整個過程中耗時最長的并發標記和并發清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程幾乎是與與用戶線程一起并發地執行。
(5).G1收集器
這個估計是最牛的收集器了。該收集器具有如下特點:
- 并行與并發:G1能充分利用現代計算器多CPU,多核的硬件優勢,可以使用并發或并行的方式來縮短讓其他線程暫停的優勢。
- 分代收集:就是類似像分出新生代和老年代那樣處理。
- 空間整合:采用了復制算法+標記-整合算法的特點來回收垃圾。就是整體采用標記-整理算法,局部采用復制算法。
- 可預測停頓:這個就牛了,就是說,它能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不超過N毫秒。
它的執行過程大體如下:
- 初始標記。
- 并發標記。
- 最終標記。
- 篩選回收。
這個流程和CMS很相似,它也是在初始標記和最終標記需要暫停其他線程,但其他兩個過程就可以和其他線程并發執行。
剛才我們說了G1收集器哪些優點,例如可預測停頓,這也使得篩選回收,是可以預測停頓垃圾回收的時間的,也就是說,停頓的時間是用戶自己可以控制的,這也使得一般情況下,在篩選回收的時候,我們會暫停其他線程的執行,把所有時間都用到篩選回收上。
本次講解到這里。
完
關注公我的眾號:苦逼的碼農,獲取更多原創文章,后臺回復"禮包"送你一份特別的資源大禮包。