JVM学习笔记之-拉圾回收概述,垃圾回收相关算法
拉圾回收概述
什么是垃圾
垃圾收集,不是Java語(yǔ)言的伴生產(chǎn)物。早在1960年,第一門(mén)開(kāi)始使用內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)的Lisp語(yǔ)言誕生。
關(guān)于垃圾收集有三個(gè)經(jīng)典問(wèn)題:
哪些內(nèi)存需要回收?
什么時(shí)候回收?
如何回收?
垃圾收集機(jī)制是Java的招牌能力,極大地提高了開(kāi)發(fā)效率。如今,垃圾收集幾乎成為現(xiàn)代語(yǔ)言的標(biāo)配,即使經(jīng)過(guò)如此長(zhǎng)時(shí)間的發(fā)展,Java的垃圾收集機(jī)制仍然在不斷的演進(jìn)中,不同大小的設(shè)備、不同特征的應(yīng)用場(chǎng)景,對(duì)垃圾收集提出了新的挑戰(zhàn),這當(dāng)然也是面試的熱點(diǎn)。
什么是垃圾( Garbage)呢?
垃圾是指在運(yùn)行程序中沒(méi)有任何指針指向的對(duì)象,這個(gè)對(duì)象就是需要被回收的垃圾。
外文: An object is considered garbage when it can no longer be reached from any pointer in the runningprogram.
如果不及時(shí)對(duì)內(nèi)存中的垃圾進(jìn)行清理,那么,這些垃圾對(duì)象所占的內(nèi)存空間會(huì)一直保留到應(yīng)用程序結(jié)束,被保留的空間無(wú)法被其他對(duì)象使用。甚至可能導(dǎo)致內(nèi)存溢出。
磁盤(pán)碎片整理的日子
為什么需要GC
想要學(xué)習(xí)GC,首先需要理解為什么需要GC?
對(duì)于高級(jí)語(yǔ)言來(lái)說(shuō),一個(gè)基本認(rèn)知是如果不進(jìn)行垃圾回收,內(nèi)存遲早都會(huì)被消耗完,因?yàn)椴粩嗟胤峙鋬?nèi)存空間而不進(jìn)行回收,就好像不停地生產(chǎn)生活垃圾而從來(lái)不打掃一樣。
除了釋放沒(méi)用的對(duì)象,垃圾回收也可以清除內(nèi)存里的記錄碎片。碎片整理將所占用的堆內(nèi)存移到堆的一端,以便JVM將整理出的內(nèi)存分配給新的對(duì)象。
隨著應(yīng)用程序所應(yīng)付的業(yè)務(wù)越來(lái)越龐大、復(fù)雜,用戶(hù)越來(lái)越多,沒(méi)有GC就不能保證應(yīng)用程序的正常進(jìn)行。而經(jīng)常造成STW的GC又跟不上實(shí)際的需求,所以才會(huì)不斷地嘗試對(duì)GC進(jìn)行優(yōu)化。
早期垃圾回收
在早期的C/C++時(shí)代,垃圾回收基本上是手工進(jìn)行的。開(kāi)發(fā)人員可以使用new關(guān)鍵字進(jìn)行內(nèi)存申請(qǐng),并使用delete關(guān)鍵字進(jìn)行內(nèi)存釋放。比如以下代碼:
這種方式可以靈活控制內(nèi)存釋放的時(shí)間,但是會(huì)給開(kāi)發(fā)人員帶來(lái)頻繁申請(qǐng)和釋放內(nèi)存的管理負(fù)擔(dān)。倘若有一處內(nèi)存區(qū)間由于程序員編碼的問(wèn)題忘記被回收,那么就會(huì)產(chǎn)生內(nèi)存泄漏,垃圾對(duì)象永遠(yuǎn)無(wú)法被清除,隨著系統(tǒng)運(yùn)行時(shí)間的不斷增長(zhǎng),垃圾對(duì)象所耗內(nèi)存可能持續(xù)上升,直到出現(xiàn)內(nèi)存溢出并造成應(yīng)用程序崩潰。
在有了垃圾回收機(jī)制后,上述代碼塊極有可能變成這樣:
現(xiàn)在,除了Java以外,C#、Python、Ruby等語(yǔ)言都使用了自動(dòng)垃圾回收的思想,也是未來(lái)發(fā)展趨勢(shì)。可以說(shuō),這種自動(dòng)化的內(nèi)存分配和垃圾回收的方式己經(jīng)成為現(xiàn)代開(kāi)發(fā)語(yǔ)言必備的標(biāo)準(zhǔn)。
Java垃圾回收機(jī)制
自動(dòng)內(nèi)存管理,無(wú)需開(kāi)發(fā)人員手動(dòng)參與內(nèi)存的分配與回收,這樣降低內(nèi)存泄漏和內(nèi)存溢出的風(fēng)險(xiǎn)
沒(méi)有垃圾回收器,java也會(huì)和cpp一樣,各種懸垂指針,野指針,泄露問(wèn)題
讓你頭疼不已。
自動(dòng)內(nèi)存管理機(jī)制,將程序員從繁重的內(nèi)存管理中釋放出來(lái),可以更專(zhuān)心地專(zhuān)注于業(yè)務(wù)開(kāi)發(fā)
擔(dān)憂(yōu)(對(duì)于java開(kāi)發(fā)人員)
對(duì)于Java開(kāi)發(fā)人員而言,自動(dòng)內(nèi)存管理就像是一個(gè)黑匣子,如果過(guò)度依賴(lài)于“自動(dòng)”,那么這將會(huì)是一場(chǎng)災(zāi)難,最嚴(yán)重的就會(huì)弱化Java開(kāi)發(fā)人員在程序出現(xiàn)內(nèi)存溢出時(shí)定位問(wèn)題和解決問(wèn)題的能力。
此時(shí),了解JVM的自動(dòng)內(nèi)存分配和內(nèi)存回收原理就顯得非常重要,只有在真正了解JVM是如何管理內(nèi)存后,我們才能夠在遇見(jiàn)outofMemoryError時(shí),快速地根據(jù)錯(cuò)誤異常日志定位問(wèn)題和解決問(wèn)題。
當(dāng)需要排查各種內(nèi)存溢出、內(nèi)存泄漏問(wèn)題時(shí),當(dāng)垃圾收集成為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí),我們就必須對(duì)這些“自動(dòng)化”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)。
垃圾回收器可以對(duì)年輕代回收,也可以對(duì)老年代回收,甚至是全堆和方法區(qū)的回收。
其中,Java堆是垃圾收集器的工作重點(diǎn)。
從次數(shù)上講:
頻繁收集Young區(qū)
較少收集old區(qū)
基本不動(dòng)Perm區(qū)(或者元空間)
垃圾回收相關(guān)算法
標(biāo)記階段:引用計(jì)數(shù)算法
垃圾標(biāo)記階段:對(duì)象存活判斷
在堆里存放著幾乎所有的Java對(duì)象實(shí)例,在GC執(zhí)行垃圾回收之前,首先需要區(qū)分出內(nèi)存中哪些是存活對(duì)象,哪些是已經(jīng)死亡的對(duì)象。只有被標(biāo)記為己經(jīng)死亡的對(duì)象,GC才會(huì)在執(zhí)行垃圾回收時(shí),釋放掉其所占用的內(nèi)存空間,因此這個(gè)過(guò)程我們可以稱(chēng)為垃圾標(biāo)記階段。
那么在JVM中究竟是如何標(biāo)記一個(gè)死亡對(duì)象呢?簡(jiǎn)單來(lái)說(shuō),當(dāng)一個(gè)對(duì)象已經(jīng)不再被任何的存活對(duì)象繼續(xù)引用時(shí),就可以宣判為已經(jīng)死亡。
判斷對(duì)象存活一般有兩種方式:引用計(jì)數(shù)算法和可達(dá)性分析算法。
方式一:引用計(jì)數(shù)算法(java不使用這類(lèi)算法)
引用計(jì)數(shù)算法(Reference Counting)比較簡(jiǎn)單,對(duì)每個(gè)對(duì)象保存一個(gè)整型的 引用計(jì)數(shù)器屬性。用于記錄對(duì)象被引用的情況。
原理:
對(duì)于一個(gè)對(duì)象A,只要有任何一個(gè)對(duì)象引用了A,則A的引用計(jì)數(shù)器就加1;當(dāng)引用失效時(shí),引用計(jì)數(shù)器就減1。只要對(duì)象A的引用計(jì)數(shù)器的值為0,即表示對(duì)象A不可能再被使用,可進(jìn)行回收。
優(yōu)點(diǎn):
實(shí)現(xiàn)簡(jiǎn)單,垃圾對(duì)象便于辨識(shí);判定效率高,回收沒(méi)有延遲性。
缺點(diǎn):
它需要單獨(dú)的字段存儲(chǔ)計(jì)數(shù)器,這樣的做法增加了存儲(chǔ)空間的開(kāi)銷(xiāo)。
每次賦值都需要更新計(jì)數(shù)器,伴隨著加法和減法操作,這增加了時(shí)間開(kāi)銷(xiāo)。
引用計(jì)數(shù)器有一個(gè)嚴(yán)重的問(wèn)題,即無(wú)法處理循環(huán)引用的情況。這是一條致命缺陷,導(dǎo)致在Java的垃圾回收器中沒(méi)有使用這類(lèi)算法。
循環(huán)引用
java由于不用引用計(jì)數(shù)算法,所以不會(huì)出現(xiàn)下面這類(lèi)的內(nèi)存泄露情況
java代碼解釋 沒(méi)有使用引用計(jì)數(shù)算法
運(yùn)行結(jié)果
結(jié)論:
由GC打印信息的,代碼中的循環(huán)依耐的對(duì)象被GC回收了,反向說(shuō)明java不是使用的引用計(jì)數(shù)算法來(lái)判斷對(duì)象存活
圖示
如果不下小心直接把Obj1-reference和obj2-reference置null。則在Java堆當(dāng)中的兩塊內(nèi)存依然保持著互相引用,無(wú)法回收。
小結(jié)
引用計(jì)數(shù)算法,是很多語(yǔ)言的資源回收選擇,例如因人工智能而更加火熱的Python,它更是同時(shí)支持引用計(jì)數(shù)和垃圾收集機(jī)制。
具體哪種最優(yōu)是要看場(chǎng)景的,業(yè)界有大規(guī)模實(shí)踐中僅保留引用計(jì)數(shù)機(jī)制,以提高吞吐量的嘗試。
Java并沒(méi)有選擇引用計(jì)數(shù),是因?yàn)槠浯嬖谝粋€(gè)基本的難題,也就是很難處理循環(huán)引用關(guān)系。
Python如何解決循環(huán)引用?
手動(dòng)解除:很好理解,就是在合適的時(shí)機(jī),解除引用關(guān)系。
使用弱引用weakref, weakref是Python提供的標(biāo)準(zhǔn)庫(kù),旨在解決循環(huán)引用。
標(biāo)記階段:可達(dá)性分析算法
方式二∶可達(dá)性分析(或根搜索算法、追蹤性垃圾收集)
相對(duì)于引用計(jì)數(shù)算法而言,可達(dá)性分析算法不僅同樣具備實(shí)現(xiàn)簡(jiǎn)單和執(zhí)行高效等特點(diǎn),更重要的是該算法可以有效地解決在引用計(jì)數(shù)算法中循環(huán)引用的問(wèn)題,防止內(nèi)存泄漏的發(fā)生。
相較于引用計(jì)數(shù)算法,這里的可達(dá)性分析就是Java、C#選擇的。這種類(lèi)型的垃圾收集通常也叫作追蹤性垃圾收集( Tracing Garbagecollection) 。
所謂"GC Roots"根集合就是一組必須活躍的引用。
基本思路:
可達(dá)性分析算法是以根對(duì)象集合(GC Roots)為起始點(diǎn),按照從上全下的方式搜索被根對(duì)象集合所連接的目標(biāo)對(duì)象是否可達(dá)。
使用可達(dá)性分析算法后,內(nèi)存中的存活對(duì)象都會(huì)被根對(duì)象集合直接或間接連接著,搜索所走過(guò)的路徑稱(chēng)為引用鏈(Reference Chain)
如果目標(biāo)對(duì)象沒(méi)有任何引用鏈相連,則是不可達(dá)的,就意味著該對(duì)象己經(jīng)死亡,可以標(biāo)記為垃圾對(duì)象。
在可達(dá)性分析算法中,只有能夠被根對(duì)象集合直接或者間接連接的對(duì)象才是存活對(duì)象。
GC Roots
在Java 語(yǔ)言中,GC Roots包括以下幾類(lèi)元素:
虛擬機(jī)棧中引用的對(duì)象
比如:各個(gè)線(xiàn)程被調(diào)用的方法中使用到的參數(shù)、局部變量等。
本地方法棧內(nèi)JNI(通常說(shuō)的本地方法)引用的對(duì)象
方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象
比如: Java類(lèi)的引用類(lèi)型靜態(tài)變量
方法區(qū)中常量引用的對(duì)象
比如:字符串常量池((string Table)里的引用
所有被同步鎖synchronized持有的對(duì)象
Java虛擬機(jī)內(nèi)部的引用。
基本數(shù)據(jù)類(lèi)型對(duì)應(yīng)的class對(duì)象,一些常駐的異常對(duì)象(如:NullPointerException、outofMemoryError),系統(tǒng)類(lèi)加載器
反映java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。
除了這些固定的GC Roots集合以外,根據(jù)用戶(hù)所選用的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域不同,還可以有其他對(duì)象“臨時(shí)性”地加入,共同構(gòu)成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)。
如果只針對(duì)Java堆中的某一塊區(qū)域進(jìn)行垃圾回收(比如:典型的只針
對(duì)新生代),必須考慮到內(nèi)存區(qū)域是虛擬機(jī)自己的實(shí)現(xiàn)細(xì)節(jié),更不是孤立封閉的,這個(gè)區(qū)域的對(duì)象完全有可能被其他區(qū)域的對(duì)象所引用,這時(shí)候就需要一并將關(guān)聯(lián)的區(qū)域?qū)ο笠布尤隚C Roots集合中去考慮,才能保證可達(dá)性分析的準(zhǔn)確性。
小技巧:
由于Root采用棧方式存放變量和指針,所以如果一個(gè)指針,它保存了堆內(nèi)存里面的對(duì)象,但是自己又不存放在堆內(nèi)存里面,那它就是一個(gè)Root
注意:
如果要使用可達(dá)性分析算法來(lái)判斷內(nèi)存是否可回收,那么分析工作必須在一個(gè)能保障一致性的快照中進(jìn)行。這點(diǎn)不滿(mǎn)足的話(huà)分析結(jié)果的準(zhǔn)確性就無(wú)法保證。
這點(diǎn)也是導(dǎo)致GC進(jìn)行時(shí)必須"stop The world"的一個(gè)重要原因。
即使是號(hào)稱(chēng)(幾乎)不會(huì)發(fā)生停頓的CMS收集器中,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。
對(duì)象的finalization機(jī)制
Java語(yǔ)言提供了對(duì)象終止(finalization)機(jī)制來(lái)允許開(kāi)發(fā)人員提供對(duì)象被銷(xiāo)毀之前的自定義處理邏輯。
當(dāng)垃圾回收器發(fā)現(xiàn)沒(méi)有引用指向一個(gè)對(duì)象,即:垃圾回收此對(duì)象之前,總會(huì)先調(diào)用這個(gè)對(duì)象的finalize()方法。
finalize()方法允許在子類(lèi)中被重寫(xiě),**用于在對(duì)象被回收時(shí)進(jìn)行資源釋放。**通常在這個(gè)方法中進(jìn)行一些資源釋放和清理的工作,比如關(guān)閉文件、套接字和數(shù)據(jù)庫(kù)連接等。
永遠(yuǎn)不要主動(dòng)調(diào)用某個(gè)對(duì)象的finalize ()方法,應(yīng)該交給垃圾回收機(jī)制調(diào)用.理由包括下面三點(diǎn):
在finalize ()時(shí)可能會(huì)導(dǎo)致對(duì)象復(fù)活。
finalize ()方法的執(zhí)行時(shí)間是沒(méi)有保障的,它完全由GC線(xiàn)程決定,極端情況下,若不發(fā)生GC,則finalize ()方法將沒(méi)有執(zhí)行機(jī)會(huì)。
一個(gè)糟糕的finalize ()會(huì)嚴(yán)重影響Gc的性能。
從功能上來(lái)說(shuō),finalize()方法與C++中的析構(gòu)函數(shù)比較相似,但是Java采用的是基于垃圾回收器的自動(dòng)內(nèi)存管理機(jī)制,所以finalize ()方法在本質(zhì)上不同于c++中的析構(gòu)函數(shù)。
由于finalize ()方法的存在,虛擬機(jī)中的對(duì)象一般處于三種可能的狀態(tài)。
虛擬機(jī)中的對(duì)象一般處于三種可能的狀態(tài)。
如果從所有的根節(jié)點(diǎn)都無(wú)法訪(fǎng)問(wèn)到某個(gè)對(duì)象,說(shuō)明對(duì)象己經(jīng)不再使用了。一般來(lái)說(shuō),此對(duì)象需要被回收。但事實(shí)上,也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑”階段。一個(gè)無(wú)法觸及的對(duì)象有可能在某一個(gè)條件下“復(fù)活”自己,如果這樣,那么對(duì)它的回收就是不合理的,為此,定義虛擬機(jī)中的對(duì)象可能的三種狀態(tài)。如下:
可觸及的:從根節(jié)點(diǎn)開(kāi)始,可以到達(dá)這個(gè)對(duì)象。
可復(fù)活的:對(duì)象的所有引用都被釋放,但是對(duì)象有可能在finalize ()中復(fù)活。
不可觸及的:對(duì)象的finalize ()被調(diào)用,并且沒(méi)有復(fù)活,那么就會(huì)進(jìn)入不可觸及狀態(tài)。不可觸及的對(duì)象不可能被復(fù)活,因?yàn)?strong>finalize()只會(huì)被調(diào)用一次。
以上3種狀態(tài)中,是由于finalize ()方法的存在,進(jìn)行的區(qū)分。只有在對(duì)象不可觸及時(shí)才可以被回收。
具體過(guò)程
判定一個(gè)對(duì)象objA是否可回收,至少要經(jīng)歷兩次標(biāo)記過(guò)程:
1.如果對(duì)象objA到 GC Roots沒(méi)有引用鏈,則進(jìn)行第一次標(biāo)記。
2.進(jìn)行篩選,判斷此對(duì)象是否有必要執(zhí)行finalize ()方法
① 如果對(duì)象objA沒(méi)有重寫(xiě)finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),則虛擬機(jī)視為“沒(méi)有必要執(zhí)行”,objA被判定為不可觸及的。
② 如果對(duì)象objA重寫(xiě)了finalize()方法,且還未執(zhí)行過(guò),那么objA會(huì)被插入到F-Queue隊(duì)列中,由一個(gè)虛擬機(jī)自動(dòng)創(chuàng)建的、低優(yōu)先級(jí)的Finalizer線(xiàn)程觸發(fā)其finalize()方法執(zhí)行。
③ finalize()方法是對(duì)象逃脫死亡的最后機(jī)會(huì),稍后cc會(huì)對(duì)F-Queue隊(duì)列中的對(duì)象進(jìn)行第二次標(biāo)記。如果objA在finalize()方法中與引用鏈上的任何一個(gè)對(duì)象建立了聯(lián)系,那么在第二次標(biāo)記時(shí),objA會(huì)被移出“即將回收”集合。之后,對(duì)象會(huì)再次出現(xiàn)沒(méi)有引用存在的情況。在這個(gè)情況下,finalize方法不會(huì)被再次調(diào)用,對(duì)象會(huì)直接變成不可觸及的狀態(tài),也就是說(shuō),一個(gè)對(duì)象的finalize方法只會(huì)被調(diào)用一次。
代碼演示具體過(guò)程
package com.fs.demo01;/***測(cè)試object類(lèi)中finalize()方法,測(cè)試對(duì)象的finalizeion機(jī)制*/ public class CanReliveObj {public static CanReliveObj obj;//類(lèi)變量@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調(diào)用當(dāng)前類(lèi)重寫(xiě)的finalize()方法");obj = this;}public static void main(String[] args) {try {//對(duì)象第一次成功拯救自己obj = new CanReliveObj();obj = null;//調(diào)用垃圾回收器System.gc();System.out.println("第一次 GC");//因?yàn)镕inalizer線(xiàn)程優(yōu)先級(jí)很低,暫停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println( "obj is dead" );}else {System.out.println( "obj is still alive" );}System.out.println("第2次gc");//下面這段代碼與上面的完全相同,但是這次自救卻失敗了obj = null;System.gc();//因?yàn)镕inalizer線(xiàn)程優(yōu)先級(jí)很低,暫停2秒,以等待它Thread.sleep( 2000) ;if (obj == null) {System.out.println("obj is dead" );} else {System.out.println( "obj is still alive" );}}catch (Exception e){e.printStackTrace();}}}/* 測(cè)試一 先注釋掉重寫(xiě)的 finalize()運(yùn)行得到的結(jié)果:第一次 GCobj is dead第2次gcobj is dead測(cè)試二 放開(kāi)重寫(xiě)的 finalize()運(yùn)行結(jié)果調(diào)用當(dāng)前類(lèi)重寫(xiě)的finalize()方法第一次 GCobj is still alive第2次gcobj is still alive*/MAT與JProfiler的GC Roots溯源
MAT
MAT是Memory Analyzer的簡(jiǎn)稱(chēng),它是一款功能強(qiáng)大的Java堆內(nèi)存分析器。用于查找內(nèi)存泄漏以及查看內(nèi)存消耗情況。
MAT是基于Eclipse開(kāi)發(fā)的,是一款免費(fèi)的性能分析工具。
大家可以在http://www.eclipse.org/mat/下載并使用MAT。
獲取dump文件
方式1:命令行使用jmap
方式2:使用JVisualVM導(dǎo)出
捕獲的heap dump文件是一個(gè)臨時(shí)文件,關(guān)閉JVisualVM后自動(dòng)刪除,若要保留,需要將其另存為文件。
可通過(guò)以下方法捕獲heap dump:
在左側(cè)“Application"(應(yīng)用程序)子窗口中右擊相應(yīng)的應(yīng)用程序,選擇Heap Dump(堆Dump) 。
在Monitor(監(jiān)視)子標(biāo)簽頁(yè)中點(diǎn)擊Heap Dump(堆Dump)按鈕。
本地應(yīng)用程序的Heap dumps作為應(yīng)用程序標(biāo)簽頁(yè)的一個(gè)子標(biāo)簽頁(yè)打開(kāi)。同時(shí),heap dump在左側(cè)的Application(應(yīng)用程序)欄中對(duì)應(yīng)一個(gè)含有時(shí)間戳的節(jié)點(diǎn)。右擊這個(gè)節(jié)點(diǎn)選擇save as (另存為)即可將heap dump保存到本地。
MAT打開(kāi)另存為的dump文件
JProfiler
idea 安裝JProfiler 請(qǐng)自行百度
使用JProfiler 查看OOM
-XX:+HeapDumpOnOutOfMemoryError 這個(gè)參數(shù)的意思就是當(dāng)出現(xiàn)OOM的時(shí)候,就會(huì)生成一個(gè)heap的dump文件
package com.fs.demo01;import java.util.ArrayList;/*** 使用JProfiler 查看OOM* -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError** -XX:+HeapDumpOnOutOfMemoryError 這個(gè)參數(shù)的意思就是當(dāng)出現(xiàn)OOM的時(shí)候,就會(huì)生成一個(gè)heap的dump文件*/ public class HeapOOMTest {byte[] buffer = new byte[1 * 1024 * 1024];//1MBpublic static void main(String[] args) {ArrayList<HeapOOMTest> list = new ArrayList<HeapOOMTest>();int count = 0;try{while(true){list.add(new HeapOOMTest());count++;}}catch ( Throwable e){System.out.println( "count = " + count) ;e.printStackTrace();}} }運(yùn)行上面代碼
分析
發(fā)現(xiàn)大對(duì)象
是那個(gè)線(xiàn)程發(fā)生OOM呢
清除階段:標(biāo)記-清除算法
當(dāng)成功區(qū)分出內(nèi)存中存活對(duì)象和死亡對(duì)象后,GC接下來(lái)的任務(wù)就是執(zhí)行垃圾回收,釋放掉無(wú)用對(duì)象所占用的內(nèi)存空間,以便有足夠的可用內(nèi)存空間為新對(duì)象分配內(nèi)存。
目前在JVM中比較常見(jiàn)的三種垃圾收集算法是標(biāo)記一清除算法((Mark-Sweep )、復(fù)制算法( copying )、標(biāo)記–壓縮算法( Mark-compact ) 。
標(biāo)記-清除(Mark - Sweep )算法
背景:
標(biāo)記–清除算法( Mark-Sweep )是一種非常基礎(chǔ)和常見(jiàn)的垃圾收集算法(優(yōu)點(diǎn)),該算法被J.McCarthy等人在1960年提出并并應(yīng)用于Lisp語(yǔ)言。
執(zhí)行過(guò)程:
當(dāng)堆中的有效內(nèi)存空間(available memory)被耗盡的時(shí)候,就會(huì)停止整個(gè)程序(也被稱(chēng)為stop the world),然后進(jìn)行兩項(xiàng)工作,第一項(xiàng)則是標(biāo)記,第二項(xiàng)則是清除。
標(biāo)記: collector從引用根節(jié)點(diǎn)開(kāi)始遍歷,標(biāo)記所有被引用的對(duì)象。一般是在對(duì)象的Header中記錄為可達(dá)對(duì)象。
清除:collector對(duì)堆內(nèi)存從頭到尾進(jìn)行線(xiàn)性的遍歷,如果發(fā)現(xiàn)某個(gè)對(duì)象在其Header中沒(méi)有標(biāo)記為可達(dá)對(duì)象,則將其回收。
缺點(diǎn)
效率不算高
在進(jìn)行GC的時(shí)候,需要停止整個(gè)應(yīng)用程序,導(dǎo)致用戶(hù)體驗(yàn)差
這種方式清理出來(lái)的空閑內(nèi)存是不連續(xù)的,產(chǎn)生內(nèi)存碎片。需要維護(hù)一個(gè)空閑列表
注意:何為清除?
這里所謂的清除并不是真的置空,而是把需要清除的對(duì)象地址保存在空閑的地址列表里。下次有新對(duì)象需要加載時(shí),判斷垃圾的位置空間是否夠,如果夠,就存放。
清除階段:復(fù)制算法
背景:
為了解決標(biāo)記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年發(fā)表了著名的論文,“使用雙存儲(chǔ)區(qū)的Lisp語(yǔ)言垃圾收集器CALISP Garbage collector Algorithm Using serialSecondary storage ) ”。M.L.Minsky在該論文中描述的算法被人們稱(chēng)為復(fù)制(copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp語(yǔ)言的一個(gè)實(shí)現(xiàn)版本中。
核心思想:
將活著的內(nèi)存空間分為兩塊,每次只使用其中一塊,在垃圾回收時(shí)將正在使用的內(nèi)存中的存活對(duì)象復(fù)制到未被使用的內(nèi)存塊中,之后清除正在使用的內(nèi)存塊中的所有對(duì)象,交換兩個(gè)內(nèi)存的角色,最后完成垃圾回收。
復(fù)制算法的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
·沒(méi)有標(biāo)記和清除過(guò)程,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效
·復(fù)制過(guò)去以后保證空間的連續(xù)性,不會(huì)出現(xiàn)“碎片”問(wèn)題。
缺點(diǎn):
此算法的缺點(diǎn)也是很明顯的,就是需要兩倍的內(nèi)存空間。
對(duì)于G1這種分拆成為大量region的Gc,復(fù)制而不是移動(dòng),意味著cc需要維護(hù)region之間對(duì)象引用關(guān)系,不管是內(nèi)存占用或者時(shí)間開(kāi)銷(xiāo)也不小。
特別的:
·如果系統(tǒng)中的垃圾對(duì)象很多,復(fù)制算法就不會(huì)很理想,因?yàn)閺?fù)制算法需要復(fù)制的存活對(duì)象數(shù)量并不會(huì)太大,或者說(shuō)非常低才行。
復(fù)制算法應(yīng)用場(chǎng)景:
在新生代,對(duì)常規(guī)應(yīng)用的垃圾回收,一次通常可以回收70%-99%的內(nèi)存空間。回收性?xún)r(jià)比很高。所以現(xiàn)在的商業(yè)虛擬機(jī)都是用這種收集算法回收新生代。
清除階段:標(biāo)記-壓縮算法(或標(biāo)記-整理、Mark - Compact )算法
背景:
復(fù)制算法的高效性是建立在存活對(duì)象少、垃圾對(duì)象多的前提下的。這種情況在新生代經(jīng)常發(fā)生,但是在老年代,更常見(jiàn)的情況是大部分對(duì)象都是存活對(duì)象。如果依然使用復(fù)制算法,由于存活對(duì)象較多,復(fù)制的成本也將很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
標(biāo)記一清除算法的確可以應(yīng)用在老年代中,但是該算法不僅執(zhí)行效率低下,而且在執(zhí)行完內(nèi)存回收后還會(huì)產(chǎn)生內(nèi)存碎片,所以JVM的設(shè)計(jì)者需要在此基礎(chǔ)之上進(jìn)行改進(jìn)。標(biāo)記–壓縮(Mark - Compact)算法由此誕生。
1970 年前后,G. L. Steele 、C. J. Chene和D.S. Wise 等研究者發(fā)布標(biāo)記-壓縮算法。在許多現(xiàn)代的垃圾收集器中,人們都使用了標(biāo)記-壓縮算法或其改進(jìn)版本。
執(zhí)行過(guò)程:
第一階段和標(biāo)記清除算法一樣,從根節(jié)點(diǎn)開(kāi)始標(biāo)記所有被引用對(duì)象
第二階段將所有的存活對(duì)象壓縮到內(nèi)存的一端,按順序排放。
之后,清理邊界外所有的空間。
標(biāo)記-壓縮算法的最終效果等同于標(biāo)記-清除算法執(zhí)行完成后,再進(jìn)行一次內(nèi)存碎片整理,因此,也可以把它稱(chēng)為標(biāo)記-清除-壓縮(Mark-Sweep-Compact)算法。
二者的本質(zhì)差異在于標(biāo)記-清除算法是一種非移動(dòng)式的回收算法,標(biāo)記-壓縮是移動(dòng)式的。是否移動(dòng)回收后的存活對(duì)象是一項(xiàng)優(yōu)缺點(diǎn)并存的風(fēng)險(xiǎn)決策。
可以看到,標(biāo)記的存活對(duì)象將會(huì)被整理,按照內(nèi)存地址依次排列,而未被標(biāo)記的內(nèi)存會(huì)被清理掉。如此一來(lái),當(dāng)我們需要給新對(duì)象分配內(nèi)存時(shí)JVM只需要持有一個(gè)內(nèi)存的起始地址即可,這比維護(hù)一個(gè)空閑列表顯然少了許多開(kāi)銷(xiāo)。
指針碰撞(Bump the Pointer)
指針碰撞(Bump the Pointer)
如果內(nèi)存空間以規(guī)整和有序的方式分布,即已用和未用的內(nèi)存都各自一邊,彼此之間維系著一個(gè)記錄下一次分配起始點(diǎn)的標(biāo)記指針,當(dāng)為新對(duì)象分配內(nèi)存時(shí),只需要通過(guò)修改指針的偏移量將新對(duì)象分配在第一個(gè)空閑內(nèi)存位置上,這種分配方式就叫做指針碰撞(Bump the Pointer) 。
標(biāo)記-壓縮算法的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
消除了標(biāo)記-清除算法當(dāng)中,內(nèi)存區(qū)域分散的缺點(diǎn),我們需要給新對(duì)象分配內(nèi)存時(shí),JVM只需要持有一個(gè)內(nèi)存的起始地址即可。
消除了復(fù)制算法當(dāng)中,內(nèi)存減半的高額代價(jià)。
缺點(diǎn):
從效率上來(lái)說(shuō),標(biāo)記-整理算法要低于復(fù)制算法。
移動(dòng)對(duì)象的同時(shí),如果對(duì)象被其他對(duì)象引用,則還需要調(diào)整引用的地址。
移動(dòng)過(guò)程中,需要全程暫停用戶(hù)應(yīng)用程序。即:STW
小結(jié)
對(duì)比三種算法
效率上來(lái)說(shuō),復(fù)制算法是當(dāng)之無(wú)愧的老大,但是卻浪費(fèi)了太多內(nèi)存。
而為了盡量兼顧上面提到的三個(gè)指標(biāo),標(biāo)記-整理算法相對(duì)來(lái)說(shuō)更平滑一些,但是效率上不盡如人意,它比復(fù)制算法多了一個(gè)標(biāo)記的階段,比標(biāo)記-清除多了一個(gè)整理內(nèi)存的階段。
分代收集算法
難道就沒(méi)有一種最優(yōu)的算法嗎?
回答:無(wú),沒(méi)有最好的算法,只有最合適的算法。
分代收集算法概念
前面所有這些算法中,并沒(méi)有一種算法可以完全替代其他算法,它們都具有自己獨(dú)特的優(yōu)勢(shì)和特點(diǎn)。分代收集算法應(yīng)運(yùn)而生。
分代收集算法,是基于這樣一個(gè)事實(shí):不同的對(duì)象的生命周期是不一樣的。因此,不同生命周期的對(duì)象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)使用不同的回收算法,以提高垃圾回收的效率。
在Java程序運(yùn)行的過(guò)程中,會(huì)產(chǎn)生大量的對(duì)象,其中有些對(duì)象是與業(yè)務(wù)信息相關(guān),比如Http請(qǐng)求中的session對(duì)象、線(xiàn)程、Socket連接,這類(lèi)對(duì)象跟業(yè)務(wù)直接掛鉤,因此生命周期比較長(zhǎng)。但是還有一些對(duì)象,主要是程序運(yùn)行過(guò)程中生成的臨時(shí)變量,這些對(duì)象生命周期會(huì)比較短,比如: string對(duì)象,由于其不變類(lèi)的特性,系統(tǒng)會(huì)產(chǎn)生大量的這些對(duì)象,有些對(duì)象甚至只用一次即可回收。
目前幾乎所有的cc都是采用分代收集(Generational Collecting)算法執(zhí)行垃圾回收的。
在HotSpot中,基于分代的概念,GC所使用的內(nèi)存回收算法必須結(jié)合年輕代和老年代各自的特點(diǎn)。
年輕代(Young Gen)
年輕代特點(diǎn):區(qū)域相對(duì)老年代較小,對(duì)象生命周期短、存活率低,回收頻繁。
這種情況復(fù)制算法的回收整理,速度是最快的。復(fù)制算法的效率只和當(dāng)前存活對(duì)象大小有關(guān),因此很適用于年輕代的回收。而復(fù)制算法內(nèi)存利用率不高的問(wèn)題,通過(guò)hotspot中的兩個(gè)survivor(s0,s1或者from to區(qū))的設(shè)計(jì)得到緩解。
老年代(Tenured Gen)
老年代特點(diǎn):區(qū)域較大,對(duì)象生命周期長(zhǎng)、存活率高,回收不及年輕代頻繁。
這種情況存在大量存活率高的對(duì)象,復(fù)制算法明顯變得不合適。一般是由標(biāo)記-清除或者是標(biāo)記-清除與標(biāo)記-整理的混合實(shí)現(xiàn)。
Mark階段的開(kāi)銷(xiāo)與存活對(duì)象的數(shù)量成正比。
Sweep階段的開(kāi)銷(xiāo)與所管理區(qū)域的大小成正相關(guān)。
Compact階段的開(kāi)銷(xiāo)與存活對(duì)象的數(shù)據(jù)成正比。
以HotSpot中的CMS回收器為例,CMS是基于Mark-Sweep實(shí)現(xiàn)的,對(duì)于對(duì)象的回收效率很高。而對(duì)于碎片問(wèn)題,CMS采用基于Mark-Compact算法的serial old回收器作為補(bǔ)償措施:當(dāng)內(nèi)存回收不佳(碎片導(dǎo)致的Concurrent Mode Failure時(shí))﹐將采用serial old執(zhí)行Full cc以達(dá)到對(duì)老年代內(nèi)存的整理。
分代的思想被現(xiàn)有的虛擬機(jī)廣泛使用。幾乎所有的垃圾回收器都區(qū)分新生代和老年代。
增量收集算法、分區(qū)算法
增量收集算法
上述現(xiàn)有的算法,在垃圾回收過(guò)程中,應(yīng)用軟件將處于一種stop the world的狀態(tài)。在stop the world狀態(tài)下,應(yīng)用程序所有的線(xiàn)程都會(huì)掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時(shí)間過(guò)長(zhǎng),應(yīng)用程序會(huì)被掛起很久,將嚴(yán)重影響用戶(hù)體驗(yàn)或者系統(tǒng)的穩(wěn)定性。為了解決這個(gè)問(wèn)題,即對(duì)實(shí)時(shí)垃圾收集算法的研究直接導(dǎo)致了增量收集(Incremental collecting)算法的誕生。
基本思想
如果一次性將所有的垃圾進(jìn)行處理,需要造成系統(tǒng)長(zhǎng)時(shí)間的停頓,那么就可以讓垃圾收集線(xiàn)程和應(yīng)用程序線(xiàn)程交替執(zhí)行。每次,垃圾收集線(xiàn)程只收集一小片區(qū)域的內(nèi)存空間,接著切換到應(yīng)用程序線(xiàn)程。依次反復(fù),直到垃圾收集完成。
總的來(lái)說(shuō),增量收集算法的基礎(chǔ)仍是傳統(tǒng)的標(biāo)記-清除和復(fù)制算法。增量收集算法通過(guò)對(duì)線(xiàn)程間沖突的妥善處理,允許垃圾收集線(xiàn)程以分階段的方式完成標(biāo)記、清理或復(fù)制工作。
缺點(diǎn):
使用這種方式,由于在垃圾回收過(guò)程中,間斷性地還執(zhí)行了應(yīng)用程序代碼,所以能減少系統(tǒng)的停頓時(shí)間。但是,因?yàn)榫€(xiàn)程切換和上下文轉(zhuǎn)換的消耗會(huì)使得垃圾回收的總體成本上升,造成系統(tǒng)吞吐量的下降。
分區(qū)算法
一般來(lái)說(shuō),在相同條件下,堆空間越大,一次Gc時(shí)所需要的時(shí)間就越長(zhǎng),有關(guān)GC產(chǎn)生的停頓也越長(zhǎng)。為了更好地控制GC產(chǎn)生的停頓時(shí)間,將一塊大的內(nèi)存區(qū)域分割成多個(gè)小塊,根據(jù)目標(biāo)的停頓時(shí)間,每次合理地回收若干個(gè)小區(qū)間,而不是整個(gè)堆空間,從而減少一次cc所產(chǎn)生的停頓。
分代算法將按照對(duì)象的生命周期長(zhǎng)短劃分成兩個(gè)部分,分區(qū)算法將整個(gè)堆空間劃分成連續(xù)的不同小區(qū)間。
每一個(gè)小區(qū)間都獨(dú)立使用,獨(dú)立回收。這種算法的好處是可以控制一次回收多少個(gè)小區(qū)間。
寫(xiě)在最后∶
注意,這些只是基本的算法思路,實(shí)際cc實(shí)現(xiàn)過(guò)程要復(fù)雜的多,目前還在發(fā)展中的前沿cc都是復(fù)合算法,并且并行和并發(fā)兼?zhèn)洹?/p>
總結(jié)
以上是生活随笔為你收集整理的JVM学习笔记之-拉圾回收概述,垃圾回收相关算法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JVM学习笔记之-StringTable
- 下一篇: JVM学习笔记之-垃圾回收相关概念 Sy