與其餘語言相比,例如c/c++,咱們都知道,java虛擬機對於程序中產生的垃圾,虛擬機是會自動幫咱們進行清除管理的,而像c/c++這些語言平臺則須要程序員本身手動對內存進行釋放。java
雖然這種自動幫咱們回收垃圾的策略少了必定的靈活性,但卻讓代碼編寫者省去了不少工做,同時也提升了不少安全性。(由於像C/C++假如你建立了大量的對象,但卻因爲本身的疏忽忘了將他們進行釋放,可能會形成內存溢出)。c++
剛纔說了,虛擬機會自動幫助咱們進行垃圾的清除,那什麼樣的對象咱們才能夠稱爲是垃圾對象呢?程序員
假如你建立了一個對象算法
Man m = new Man();
你用一個變量指向了這個對象,顯然對於這個對象,你能夠用變量m對這個對象進行利用,但過了一段時間,你執行了安全
m = null;
而且也並無新的變量來指向剛纔建立的對象。此時對於這個沒有任何變量指向的對象,你以爲它還有用處嗎?多線程
顯然,對於這種沒有被變量指向的對象,它是一點卵用也沒有的,它只能在堆隨風漂流。併發
所以,對於這樣的對象,咱們就能夠把它稱爲垃圾了,它遲早會被垃圾回收器給幹掉。優化
假如代碼是你本身編寫的,你可能知道這個對象啥時候應該被拋棄,你能夠隨時讓它成爲垃圾對象。spa
可是,你畢竟是你,虛擬機則沒那麼智能。那虛擬機是如何知道的呢?線程
上面已經說了,沒有變量引用這個對象時,它就是垃圾對象了,基於這個原理,咱們能夠這樣作啊:
咱們能夠爲這個對象設置一個計數器,初始值爲0,假若有一個變量指向它,那麼計數器就加1,若是這個變量不在指向它了,計數器就減1。那麼咱們就能夠判斷,若是這個計數器爲0的話,那它就是垃圾對象了,不然就是有用的對象。
對於這種方法,咱們稱之爲引用計數法。
好吧,咱們先來誇一誇引用計數法這種方法:
很差意思,接下來得說說它那個致命的缺點。
實際上,對於這種引用計數的方法,假如它遇到對象互相引用的話,是很難解決的。
先看一段代碼:
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個步驟,包括:
其中初始標記、從新標記這兩個步驟仍然須要暫停其餘線程。但另外兩個步驟能夠和其餘線程併發執行。初始標記僅僅只是標記一下GCRoots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程 (說白了就是把整個圖都遍歷了,找出沒有的對象),
而從新標記階段則是爲了修正併發標記期間,因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但遠比並發標記的時間短。
因爲整個過程當中耗時最長的併發標記和併發清除過程當中,收集器線程均可以與用戶線程一塊兒工做,因此整體上來講,CMS收集器的內存回收過程幾乎是與與用戶線程一塊兒併發地執行。
(5).G1收集器
這個估計是最牛的收集器了。該收集器具備以下特色:
它的執行過程大致以下:
這個流程和CMS很類似,它也是在初始標記和最終標記須要暫停其餘線程,但其餘兩個過程就能夠和其餘線程併發執行。
剛纔咱們說了G1收集器哪些優勢,例如可預測停頓,這也使得篩選回收,是能夠預測停頓垃圾回收的時間的,也就是說,停頓的時間是用戶本身能夠控制的,這也使得通常狀況下,在篩選回收的時候,咱們會暫停其餘線程的執行,把全部時間都用到篩選回收上。
本次講解到這裏。
完
關注公個人衆號: 苦逼的碼農,獲取更多原創文章,後臺回覆"禮包"送你一份特別的資源大禮包。