垃圾回收相關算法
標記階段
對象存活判斷
●?在堆里存放著幾乎所有的Java對象實例录语,在GC執(zhí)行垃圾回收之前睁壁,首先需要區(qū)分出內存中哪些是存活對象剑勾,哪些是已經死亡的對象床估。只有被標記為己經死亡的對象弯囊,GC才會在執(zhí)行垃圾回收時痰哨,釋放掉其所占用的內存空間,因此這個過程我們可以稱為垃圾標記階段匾嘱。
●?那么在JVM中究竟是如何標記-一個死亡對象呢?簡單來說斤斧,當一個對象已經不再被任何的存活對象繼續(xù)引用時,就可以宣判為已經死亡霎烙。
●?判斷對象存活一般有兩種方式:引用計數算法和可達性分析算法撬讽。
引用計數算法
●?引用計數算法(Reference Counting) 比較簡單,對每個對象保存一個整型的引用計數器屬性悬垃。用于記錄對象被引用的情況游昼。
●?對于一個對象A,只要有任何一個對象引用了A尝蠕,則A的引用計數器就加1烘豌;當引用失效時,引用計數器就減1看彼。只要對象A的引用計數器的值為0廊佩,即表示對象A不可能再被使用昧捷,可進行回收。
●?優(yōu)點:實現(xiàn)簡單罐寨,垃圾對象便于辨識;判定效率高序矩,回收沒有延遲性鸯绿。
●?缺點:
???它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷簸淀。
???每次賦值都需要更新計數器瓶蝴,伴隨著加法和減法操作,這增加了時間開銷租幕。
???引用計數器有一個嚴重的問題舷手,即無法處理循環(huán)引用的情況。這是一條致命缺陷,導致在Java的垃圾回收器中沒有使用這類算法劲绪。
循環(huán)引用
小結
●?引用計數算法男窟,是很多語言的資源回收選擇,例如因人工智能而更加火熱的Python贾富,它更是同時支持引用計數和垃圾收集機制歉眷。
●?具體哪種最優(yōu)是要看場景的,業(yè)界有大規(guī)模實踐中僅保留引用計數機制颤枪,以提高吞吐量的嘗試汗捡。
●?Java并沒有選擇引用計數,是因為其存在一個基本的難題畏纲,也就是很難處理循環(huán)引用關系扇住。
●?Python如何解決循環(huán)引用?
???手動解除:很好理解,就是在合適的時機盗胀,解除引用關系艘蹋。
???使用弱引用weakref,weakref 是Python提供的標準庫票灰,旨在解決循環(huán)引用簿训。
可達性分析算法
●?相對于引用計數算法而言,可達性分析算法不僅同樣具備實現(xiàn)簡單和執(zhí)行高效等特點米间,更重要的是該算法可以有效地解決在引用計數算法中循環(huán)引用的問題强品,防止內存泄漏的發(fā)生。
●?相較于引用計數算法屈糊,這里的可達性分析就是Java的榛、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Ga rbage Collection)逻锐。
●?所謂"GC Roots"根集合就是一組必須活躍的引用夫晌。
●?基本思路:
???可達性分析算法是以根對象集合(GC Roots) 為起始點雕薪,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達。
???使用可達性分析算法后晓淀,內存中的存活對象都會被根對象集合直接或間接連接著所袁,搜索所走過的路徑稱為引用鏈(Reference Chain)。
???如果目標對象沒有任何引用鏈相連凶掰,則是不可達的燥爷,就意味著該對象己經死亡,可以標記為垃圾對象懦窘。
???在可達性分析算法中前翎,只有能夠被根對象集合直接或者間接連接的對象才是存活對象。
GC Roots
在Java語言中畅涂,GC Roots包括以下幾類元素
●?虛擬機棧中引用的對象
???比如:各個線程被調用的方法中使用到的參數港华、局部變量等。
●?本地方 法棧內JNI (通常說的本地方法)引用的對象
●?方法區(qū)中類靜態(tài)屬性引用的對象
???比如:Java類的引用類型靜態(tài)變量
●?方法區(qū)中常量引用的對象
???比如:字符串常量池(String Table)里的引用
●?所有被同步鎖synchronized持有的對象
●?Java虛擬機內部的引用午衰。
???基本數據類型對應的Class對象立宜,一些常駐的異常對象(如:NullPointerException、OutOfMemoryError) 臊岸,系統(tǒng)類加載器赘理。
●?反映java虛擬機內部情況的JMXBean、JVMTI中注冊的回調扇单、本地代碼緩存等商模。
●?除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的內存區(qū)域不同蜘澜,還可以有其他對象“臨時性”地加入畸裳,共同構成完整GC Roots集合舀射。比如:分代收集和局部回收(Partial GC)国撵。
???如果只針對Java堆中的某一塊區(qū) 域進行垃圾回收(比如:典型的只針對新生代)瓤的,必須考慮到內存區(qū)域是虛擬機自己的實現(xiàn)細節(jié),更不是孤立封閉的装诡,這個區(qū)域的對象完全有可能被其他區(qū)域的對象所引用银受,這時候就需要一并將關聯(lián)的區(qū)域對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性鸦采。
●?小技巧:
由于Root采用棧方式存放變量和指針宾巍,所以如果一個指針,它保存了堆內存里面的對象渔伯,但是自己又不存放在堆內存里面顶霞,那它就是一一個Root。
注意
●?如果要使用可達性分析算法來判斷內存是否可回收I那么分析工作必須在一個能保障一致性的快照中進行锣吼。這點不滿足的話分析結果的準確性就無法保證选浑。
●?這點也是導致GC進行時必須"Stop The World"的-一個重要原因蓝厌。
???即使是號稱(幾乎)不會發(fā)生停頓的CMS收集器中,枚舉根節(jié)點時也是必須要停頓的古徒。
對象的finalization機制
●?Java語言提供了對象終止(finalization)機制來允許開發(fā)人員提供對象皸銷毀之前的自定義處理邏輯拓提。
●?當垃圾回收器發(fā)現(xiàn)沒有引用指向一個對象,即:垃圾回收此對象之前隧膘,總會先調用這個對象的finalize()方法代态。
●?finalize()方法允許在子類中被重寫,用于在對象被回收時進行資源釋放舀寓。通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件肌蜻、套接字和數據庫連接等互墓。
●?永遠不要主動調用某個對象的finalize()方法,應該交給垃圾回收機制調用蒋搜。理由包括下面三點:
???在finalize() 時可能會導致對象復活篡撵。
???finalize() 方法的執(zhí)行時間是沒有保障的,它完全由GC線程決定豆挽,極端情況下育谬,若不發(fā)生GC,則finalize()方法將沒有執(zhí)行機會帮哈。
???一個糟糕的finalize ()會嚴重影響GC的性能膛檀。
●?從功能上來說,finalize()方法與C+ +中的析構函數比較相似娘侍,但是Java采用的是基于垃圾回收器的自動內存管理機制咖刃,所以finalize()方法在本質上不同于C++中的析構函數。
●?由于finalize()方法的存在憾筏,虛擬機中的對象一般處于三種可能的狀態(tài)嚎杨。
生存還是死亡?
●?如果從所有的根節(jié)點都無法訪問到某個對象氧腰,說明對象已經不再使用了枫浙。一般來說,此對象需要被回收古拴。但事實上箩帚,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段黄痪。一個無法觸及的對象有可能在某一個條件下“復活”自己膏潮,如果這樣,那么對它的回收就是不合理的满力,為此焕参,定義虛擬機中的對象可能的三種狀態(tài)轻纪。如下:
???可觸及的:從根節(jié)點開始,可以到達這個對象叠纷。
???可復活的:對象的所有引用都被釋放刻帚,但是對象有可能在finalize()中復活。
???不可觸及的:對象的finalize()被調用涩嚣,并且沒有復活崇众,那么就會進入不可觸及狀態(tài)。不可觸及的對 象不可能被復活航厚,因為finalize()只會被調用一次顷歌。
●?以上3種狀態(tài)中,是由于finalize ()方法的存在幔睬,進行的區(qū)分眯漩。只有在對象不可觸及時才可以被回收。
具體過程
●?判定一個對象objA是否可回收麻顶,至少要經歷兩次標記過程:
?1赦抖、如果對象objA到GC Roots沒有引用鏈,則進行第一次標記辅肾。
?2队萤、進行篩選,判斷此對象是否有必要執(zhí)行finalize()方法
??①如果對象objA沒有 重寫finalize()方法矫钓,或者finalize()方法已經被虛擬機調用過要尔,則虛擬機視為“沒有必要執(zhí)行”,objA被判定為不可觸及的新娜。
??②如果對象objA重寫 了finalize()方法盈电,且還未執(zhí)行過,那么objA會被插入到F-Queue隊列中杯活,由一個虛擬機自動創(chuàng)建的匆帚、低優(yōu)先級的Finalizer線程觸發(fā)其finalize()方
法執(zhí)行。
??③finalize()方法是對象逃脫死亡的最后機會旁钧,稍后Gc會對F-Queue隊列中的對象進行第二次標記吸重。如果objA在finalize()方法中與引用鏈上的任何一個對象建立了聯(lián)系,那么在第二次標記時歪今,objA會被移出“即將回收”集合嚎幸。之后,對象會再次出現(xiàn)沒有引用存在的情況寄猩。在這個情況下嫉晶,finalize方法不會被再次調用,對象會直接變成不可觸及的狀態(tài),也就是說替废,一個對象的finalize方法只會被調用一次箍铭。
MAT與JProfiler的GC Roots溯源
MAT是Memory Anallyzer的簡稱,它是一款功能強大的Java堆內存分析器椎镣。用于查找內存泄漏以及查看內存消耗情況诈火。
MAT是基于Eclipse開發(fā)的,是一款免費的性能分析工具状答。
可以在http://www.eclipse.org/mat/下載并使用MAT冷守。
獲取dump文件
方式一:命令行使用jmap
方式二:使用JVi sualVM導出
●?捕獲的heap dump文件是一個臨時文件,關閉JVisualVM后自動刪除惊科,若要保留拍摇,需要將其另存為文件。
●?可通過以下方法捕獲heap dump:
???在左側“Application"(應用程序)子窗口中右擊相應的應用程序馆截,選擇Heap Dump(堆Dump)充活。
???在Monitor(監(jiān)視)子標簽頁中點擊Heap Dump(堆Dump)按鈕。
●?本地應用程序的Heap dumps作為應用程序標簽頁的一一個子標簽頁打開孙咪。同時堪唐,heap dump在 左側的Application (應用程序)欄中對應一個含有時間戳的節(jié)點巡语。右擊這個節(jié)點選擇save as (另存為)即可將heap dump保存 到本地翎蹈。
清除階段
當成功區(qū)分出內存中存活對象和死亡對象后,GC接下來的任務就是執(zhí)行垃圾回收男公,釋放掉無用對象所占用的內存空間荤堪,以便有足夠的可用內存空間為新對象分配內存。
目前在JVM中比較常見的三種垃圾收集算法是標記 - 清除算法(Mark - Sweep)枢赔、復制算法(Copying)澄阳、標記 - 壓縮算法(Mark - Compact)。
標記 - 清除算法
背景:
標記 - 清除算法(Mark - Sweep)是一種非程ぐ荩基礎和常見的垃圾收集算法碎赢,該算法被J.McCarthy等人在1960年提出并并應用于Lisp語言。
執(zhí)行過程:
當堆中的有效內存空間(available memory) 被耗盡的時候速梗,就會停止整個程序(也被稱為stop the world) 肮塞,然后進行兩項工作,第一項則是標記姻锁,第二項則是清除枕赵。
●?標記:Collector從引用根節(jié)點開始遍歷,標記所有被引用的對象(可達對象-非辣雞對象)位隶。一般是在對象的Header中記錄為可達對象拷窜。
●?清除:Collector對 堆內存從頭到尾進行線性的遍歷,如果發(fā)現(xiàn)某個對象在其Header中沒有標記為可達對象,則將其回收篮昧。
缺點
???效率不算高
???在進行GC的時候赋荆,需要停止整個應用程序,導致用戶體驗差
???這種方式清理出來的空閑內存是不連續(xù)的恋谭,產生內存碎片糠睡。需要維護一個空閑列表
注意:何為清除?
???這里所謂的清除并不是真的置空疚颊,而是把需要清除的對象地址保存在空閑的地址列表里狈孔。下次有新對象需要加載時,判斷垃圾的位置空間是否夠材义,如果夠均抽,就存放。
復制算法
背景:
為了解決標記-清除算法在垃圾收集效率方面的缺陷其掂,M.L.Minsky于1963年發(fā)表了著名的論文油挥,“ 使用雙存儲區(qū)的Li sp語言垃圾收集器CALISP Garbage Collector Algorithm Using Serial Secondary storage )”。M.L.Minsky 在該論文中描述的算法被人們稱為復制(Copying)算法款熬,它也被M. L.Minsky本人成功地引入到了
Lisp語言的一個實現(xiàn)版本中深寥。
核心思想:
將活著的內存空間分為兩塊,每次只使用其中一塊贤牛,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中惋鹅,之后清除正在使用的內存塊中的所有對象,交換兩個內存的角色殉簸,最后完成垃圾回收闰集。
優(yōu)點:
●?沒有標記和清除過程,實現(xiàn)簡單般卑,運行高效
●?復制過去以后保證空間的連續(xù)性武鲁,不會出現(xiàn)“碎片”問題。
缺點:
●?此算法的缺點也是很明顯的蝠检,就是需要兩倍的內存空間沐鼠。
●?對于G1這種分拆成為大量region的GC,復制而不是移動叹谁,意味著GC需要維護region之間對象引用關系饲梭,不管是內存占用或者時間開銷也不小。
特別的:
●?如果系統(tǒng)中的垃圾對象很多本慕,復制算法就不會太理想排拷,因為復制算法需要復制的存活對象數量并不會太大,或者說非常低才行锅尘。
應用場景:
在新生代监氢,對常規(guī)應用的垃圾回收布蔗,一次通常可以回收70%-99%的內存空間浪腐∽葑幔回收性價比很高。所以現(xiàn)在的商業(yè)虛擬機都是用這種收集算法回收新生代议街。
標記 - 壓縮(整理)算法
背景:
復制算法的高效性是建立在存活對象少泽谨、垃圾對象多的前提下的。這種情況在新生代經常發(fā)生特漩,但是在老年代吧雹,更常見的情況是大部分對象都是存活對象。如果依然使用復制算法涂身,由于存活對象較多雄卷,復制的成本也將很高。因此蛤售,基于老年代垃圾回收的特性丁鹉,需要使用其他的算法。
標記 - 清除算法的確可以應用在老年代中悴能,但是該算法不僅執(zhí)行效率低下揣钦,而且在執(zhí)行完內存回收后還會產生內存碎片,所以JVM的設計者需要在此基礎之上進行改進漠酿。標記 - 壓縮(Mark - Compact) 算法由此誕生冯凹。
1970年前后,G.L.Steele 记靡、C.J.Chene 和D.S.Wise等研究者發(fā)布標記 - 壓縮算法谈竿。在許多現(xiàn)代的垃圾收集器中团驱,人們都使用了標記 - 壓縮算法或其改進版本摸吠。
執(zhí)行過程:
第一階段和標記清除算法一樣,從根節(jié)點開始標記所有被引用對象
第二階段將所有的存活對象壓縮到內存的一端嚎花,按順序排放寸痢。
之后,清理邊界外所有的空間紊选。
標記 - 壓縮算法的最終效果等同于標記 - 清除算法執(zhí)行完成后啼止,再進行一次內存碎片整理,因此兵罢,也可以把它稱為標記 - 清除 - 壓縮(Mark - Sweep - Compact)算法献烦。
二者的本質差異在于標記-清除算法是一種非移動式的回收算法,標記 - 壓縮是移動式的卖词。是否移動回收后的存活對象是- -項優(yōu)缺點并存的風險決策巩那。
可以看到,標記的存活對象將會被整理,按照內存地址依次排列即横,而未被標記的內存會被清理掉噪生。如此一來,當我們需要給新對象分配內存時东囚,JVM只需要持有一個內存的起始地址即可跺嗽,這比維護一個空閑列表顯然少了許多開銷。
優(yōu)點:
●?消除了標記 - 清除算法當中页藻,內存區(qū)域分散的缺點桨嫁,我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可份帐。
●?消除了復制算法當中瞧甩,內存減半的高額代價。
缺點:
●?從效率上來說弥鹦,標記一整理算法要低于復制算法肚逸。
●?移動對象的同時,如果對象被其他對象引用彬坏,則還需要調整引用的地址朦促。
●?移動過程中,需要全程暫停用戶應用程序栓始。即:STW
小結
效率上來說务冕,復制算法是當之無愧的老大,但是卻浪費了太多內存幻赚。
而為了盡量兼顧上面提到的三個指標禀忆,標記 - 整理算法相對來說更平滑一些,但是效率上不盡如人意落恼,它比復制算法多了一個標記的階段箩退,比標記 - 清除多了一個整理內存的階段。
分代收集算法
前面所有這些算法中佳谦,并沒有一種算法可以完全替代其他算法戴涝,它們都具有自己獨特的優(yōu)勢和特點。分代收集算法應運而生钻蔑。
分代收集算法啥刻,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此咪笑,不同生命周期的對象可以采取不同的收集方式可帽,以便提高回收效率。一般是把Java堆分為新生代和老年代窗怒,這樣就可以根據各個年代的特點使用不同的回收算法映跟,以提高垃圾回收的效率钝满。
在Java程序運行的過程中,會產生大量的對象申窘,其中有些對象是與業(yè)務信息相關弯蚜,比如Http請求中的Session對象、線程剃法、Socket連接碎捺, 這類對象跟業(yè)務直接掛鉤,因此生命周期比較長贷洲。但是還有一些對象收厨,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短优构,比如:String對象诵叁, 由于其不變類的特性,系統(tǒng)會產生大量的這些對象钦椭,有些對象甚至只用一次即可回收拧额。
目前幾乎所有的GC都是采用分代收集(Generational Collecting) 算法執(zhí)行垃圾回收的。
在HotSpot中彪腔,基于分代的概念侥锦,GC所使用的內存回收算法必須結合年輕代和老年代各自的特點。
●?年輕代(Young Gen)
?年輕代特點:區(qū)域相對老年代較小德挣,對象生命周期短恭垦、存活率低,回收頻繁格嗅。
?這種情況復制算法的回收整理番挺,速度是最快的。復制算法的效率只和當前存活對象大小有關屯掖,因此很適用于年輕代的回收玄柏。而復制算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解懂扼。
●老年代(Tenured Gen)
?老年代特點:區(qū)域較大禁荸,對象生命周期長右蒲、存活率高阀湿,回收不及年輕代頻繁。
?這種情況存在大量存活率高的對象瑰妄,復制算法明顯變得不合適陷嘴。一般是由標記-清除或者是標記 - 清除與標記 - 整理的混合實現(xiàn)。
???Mark階 段的開銷與存活對象的數量成正比间坐。
???Sweep階 段的開銷與所管理區(qū)域的大小成正相關灾挨。
???Compact階段的開銷與存活對象的數據成正比邑退。
以HotSpot中的CMS回收器為例,CMS是基于Mark- Sweep實現(xiàn)的劳澄,對于對象的回收效率很高地技。而對于碎片問題,CMS采用基于Mark - Compact算法的Serial 0ld回收器作為補償措施:當內存回收不佳(碎片導致的Concurrent Mode Failure時)秒拔,將采用Serial old執(zhí)行Full GC以達到對老年代內存的整理莫矗。
分代的思想被現(xiàn)有的虛擬機廣泛使用。幾乎所有的垃圾回收器都區(qū)分新生代和老年代砂缩。
增量收集算法
上述現(xiàn)有的算法作谚,在垃圾回收過程中,應用軟件將處于一種stop the World的狀態(tài)庵芭。在Stop the World狀態(tài)下妹懒,應用程序所有的線程都會掛起,暫停一切正常的工作双吆,等待垃圾回收的完成眨唬。如果垃圾回收時間過長,應用程序會被掛起很久好乐,將嚴重影響用戶體驗或者系統(tǒng)的穩(wěn)定性单绑。為了解決這個問題,即對實時垃圾收集算法的研究直接導致了增量收集(Incremental Collecting) 算法的誕生曹宴。
基本思想
如果一次性將所有的垃圾進行處理搂橙,需要造成系統(tǒng)長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執(zhí)行笛坦。每次区转,垃圾收集線程只收集一小片區(qū)域的內存空間,接著切換到應用程序線程版扩。依次反復废离,直到垃圾收集完成。
總的來說礁芦,增量收集算法的基礎仍是傳統(tǒng)的標記 - 清除和復制算法蜻韭。增量收集算法通過對線程間沖突的妥善處理,允許垃圾收集線程以分階段的方式完成標記柿扣、清理或復制工作肖方。
缺點:
使用這種方式,由于在垃圾回收過程中未状,間斷性地還執(zhí)行了應用程序代碼俯画,所以能減少系統(tǒng)的停頓時間。但是司草,因為線程切換和上下文轉換的消耗艰垂,會使得垃圾回收的總體成本上升泡仗,造成系統(tǒng)吞吐量的下降。
分區(qū)算法
一般來說猜憎,在相同條件下娩怎,堆空間越大,一次GC時所需要的時間就越長胰柑,有關GC產生的停頓也越長峦树。為了更好地控制GC產生的停頓時間,將一塊大的內存區(qū)域分割成多個小塊旦事,根據目標的停頓時間魁巩,每次合理地回收若干個小區(qū)間,而不是整個堆空間姐浮,從而減少一次GC所產生的停頓谷遂。
分代算法將按照對象的生命周期長短劃分成兩個部分,分區(qū)算法將整個堆空間劃分成連續(xù)的不同小區(qū)間region卖鲤。
每一個小區(qū)間都獨立使用肾扰,獨立回收。這種算法的好處是可以控制一 次回收多少個小區(qū)間蛋逾。
后記
寫在最后:
注意集晚,這些只是基本的算法思路,實際GC實現(xiàn)過程要復雜的区匣,目前還在發(fā)展中的前沿GC都是復合算法偷拔,并且并行和并發(fā)兼?zhèn)洹?/p>
指針碰撞
如果內存空間以規(guī)整和有序的方式分布,即已用和未用的內存都各自一邊亏钩,彼此之間維系著一個記錄下一次分配起始點的標記指針莲绰,當為新對象分配內存時,只需要通過修改指針的偏移量將新對象分配在第一一個空閑內存位置上姑丑,這種分配方式就叫做指針碰撞(Bump thle Pointer)