JVM的垃圾回收与内存分配
? ?Java是一種內(nèi)存動態(tài)分配和垃圾回收技術(shù)的一種語言,不需要顯示的進(jìn)行對象內(nèi)存的分配,這一切操作都是由JVM來完成的,由于Java是“一切皆對象”的,所以對于內(nèi)存分配的優(yōu)化與速度非常的高效。在Java中一個對象在堆中的分配以及滅亡都是由JVM來完成的。JVM負(fù)責(zé)來垃圾回收與對象分配。
一 垃圾回收
? ?垃圾回收(Garbage Collection,GC),研究這個主要目的就是為了提升JVM的性能,在內(nèi)存泄露中能及時查缺問題所在。對于垃圾回收,GC必須要解決的問題包括三個:
? 1)哪些內(nèi)存可以回收?哪些對象可以回收? 這里主要就是判斷哪些對象的死活
? 2)什么時候回收? 一般就是當(dāng)內(nèi)存不夠或者設(shè)置垃圾收集器的時間間隔
? 3)如何回收? 對于已經(jīng)判斷為死的對象的回收算法以及實現(xiàn)這些算法的垃圾收集器
哪些內(nèi)存和對象可以回收?
? ?JVM中的五大內(nèi)存區(qū)域中,程序計數(shù)器、虛擬機棧和本地方法棧都是隨著線程而生,隨著線程而滅亡。棧中的大小基本上在類結(jié)構(gòu)確定下來的時候就已知了,這三個區(qū)域內(nèi)存分配與回收都具備確定性,所以當(dāng)方法結(jié)束或者線程結(jié)束時候,這三塊的內(nèi)存就隨著回收了。
? ?而Java堆和方法區(qū)中,存放了與類有關(guān)的信息以及類的實例對象,這些對象只會在具體運行期間才能創(chuàng)建,而這些對象創(chuàng)建與回收都是動態(tài)的,故垃圾回收需要考慮堆和方法區(qū)中的回收。
? ?明確了需要回收哪一塊的內(nèi)存后,就需要再次解決哪些對象可以回收?
? ?垃圾回收的對象,這里主要回收的就是那些已經(jīng)死去(即不再被任何途徑使用的對象)。既然知道要回收這些死的對象,那么接下來就要確定怎么來判斷一個對象的死活。
? ?在Java中使用的就是根搜索算法(GC Roots Tracing)來判斷對象是否存活,而不是利用引用計數(shù)。
? ?根搜索算法的基本思想:通過一系列名為“GC Roots”的對象作為起始點,從這些節(jié)點向下搜索,搜索所經(jīng)過的路徑稱為引用鏈,當(dāng)一個對象到GC Roots沒有引用鏈的時候,則可初次判定該對象不可用。圖論中表示就是從GC Roots到這個對象路徑不可達(dá)。
? ?Java中可以作為GC Roots的對象有虛擬機棧中的引用對象,方法區(qū)中的類靜態(tài)屬性引用的對象,方法區(qū)中的常量引用的對象,本地方法區(qū)中Native的引用的對象。
垃圾回收的起點?(基本思想的詳解)
? ?棧是真正進(jìn)行程序執(zhí)行地方,所以要獲取哪些對象正在被使用,則需要從Java棧開始。同時,一個棧是與一個線程對應(yīng)的,因此,如果有多個線程的話,則必須對這些線程對應(yīng)的所有的棧進(jìn)行檢查。同時,除了棧外,還有系統(tǒng)運行時的寄存器等,也是存儲程序運行數(shù)據(jù)的。這樣,以棧或寄存器中的引用為起點,我們可以找到堆中的對象,又從這些對象找到對堆中其他對象的引用,這種引用逐步擴(kuò)展,最終以null引用或者基本類型結(jié)束,這樣就形成了一顆以Java棧中引用所對應(yīng)的對象為根節(jié)點的一顆對象樹,如果棧中有多個引用,則最終會形成多顆對象樹。在這些對象樹上的對象,都是當(dāng)前系統(tǒng)運行所需要的對象,不能被垃圾回收。而其他剩余對象,則可以視為無法被引用到的對象,可以被當(dāng)做垃圾進(jìn)行回收。
因此,垃圾回收的起點是一些根對象(java棧, 靜態(tài)變量, 寄存器...)
? ?Java利用根搜索算法要經(jīng)歷兩次標(biāo)記過程來宣告一個對象死活:
? ?第一次標(biāo)記并篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當(dāng)對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過一次(因該方法只會被調(diào)用一次),虛擬機則判定對象沒有必要執(zhí)行finalize()。如果有必要執(zhí)行,將該對象放入F-Queue隊列,稍后由虛擬機自動創(chuàng)建優(yōu)先級低的Finalizer線程。第二次標(biāo)記就發(fā)生在F-Queue中,如果在Finalizer線程執(zhí)行時F-Queue中的對象與引用鏈上的對象建立了關(guān)聯(lián),第二次標(biāo)記時會將該對象移出F-Queue,隊列中剩下的經(jīng)過兩次標(biāo)記的對象就是可以回收的。
什么時候回收?
? ? 在Java中垃圾回收器啟動的時間是不固定的,它根據(jù)內(nèi)存的使用量而進(jìn)行動態(tài)的自適應(yīng)調(diào)整,來運行GC,在為內(nèi)存分配的過程中就會有GC的產(chǎn)生過程。
如何回收?
確定了哪些對象以及內(nèi)存需要回收后,此時就需要考慮采用什么樣的策略以及用什么來具體實現(xiàn)這些策略。
回收的算法:
? ?垃圾收集算法都是先用“根搜索算法”來判斷哪些需要回收的,然后進(jìn)行垃圾的回收處理,常用的垃圾收集算法的基本概況:
標(biāo)記-清除算法(Mark-Sweep):這種算法主要分為兩個階段,“標(biāo)記”和“清除”,標(biāo)記的過程就是采用的“根搜索算法”,首先標(biāo)記處所有被引用的對象,在標(biāo)記階段完成后,遍歷整個堆,對于未被被標(biāo)記的可回收的對象進(jìn)行統(tǒng)一的回收掉。優(yōu)點:MS收集器可以在存儲耗盡的時候啟動,實現(xiàn)起來簡單容易。缺點:工作的時候需要使得工作例程掛起等待很長時間,效率不高,會產(chǎn)生大量的不連續(xù)的內(nèi)存碎片,就會導(dǎo)致一些比較大的對象無法找到足夠連續(xù)內(nèi)存從而提前出發(fā)另一次垃圾收集動作。
復(fù)制算法(Copying):這是為了解決效率問題而出現(xiàn)的,主要用于堆中新生代的回收。它將可用內(nèi)存按容量大小劃分為大小相等的兩塊,每次只使用其中一塊,當(dāng)這一塊完了后,就將還存活的對象復(fù)制到另一塊上面,然后把剛已使用的內(nèi)存空間一次清除掉。優(yōu)點:提高了回收效率,回收后不會產(chǎn)生不連續(xù)的空間,工作開銷正比于存活的對象。缺點:將可用內(nèi)存縮小為原來的一般,當(dāng)對象存活率較高時候,就要執(zhí)行較多的復(fù)制操作,效率就會降低。
在新生代采用該算法的思路:將該算法用于新生代中,在現(xiàn)在一般并不是將內(nèi)存劃分為等大的兩塊,由于新生代的對象大多是朝生夕死的,在新生代中將內(nèi)存劃分為一個較大Eden空間和兩塊較小的Survivor,每次僅僅使用Eden和其中一個Survivor,當(dāng)回收時,將Eden和Survivor還存活的對象一次性拷貝到另一塊的Survivor中,最后清理掉Eden和剛才用過的Survivor空間。當(dāng)要拷貝至Survivor空間不夠容納還存活的對象時候,此時就需要用老年代來進(jìn)行分配擔(dān)保,即將這些存活的對象拷貝至老年代中。
標(biāo)記-整理算法(Mark-Compact):主要用于堆中的老年代回收。它依舊采用“標(biāo)記-清理”中的“標(biāo)記”方法,當(dāng)“標(biāo)記”階段完成中,它是將對于存活的對象即被引用的對象進(jìn)行標(biāo)記,在“整理”階段中,將標(biāo)記完成的對象進(jìn)行移動,使之與相鄰的活動對象連續(xù)分配,從而將所有存活的對象都移向了堆的一端,然后直接清理掉堆端邊界以外的所有內(nèi)存。“標(biāo)記-整理算法”克服在對象存活率較高中出現(xiàn)頻繁的復(fù)制操作,并且解決回收后的內(nèi)存出現(xiàn)不連續(xù)的空間,它是“標(biāo)記清理”和“復(fù)制”的有機結(jié)合。
分代收集算法(Generational Collecting):當(dāng)前垃圾收集都是采用這種算法。主要將Java堆分為年輕代和老年代,根據(jù)不同的年代采用不同的算法。在新生代中,由于只有少量的存活對象,此時就使用“復(fù)制”算法;在老年代中,由于存活對象比較長沒有額外空間進(jìn)行分配擔(dān)保,就使用“標(biāo)記-整理”或“標(biāo)記-清理”算法。之所以要進(jìn)行分代的原因是由于不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。
? JVM中分代的模型如下:
回收的具體實現(xiàn):
? 垃圾收集器就是具體實現(xiàn)這些垃圾收集算法的。在HotSpot中主要包括如下:
年輕代垃圾收集器:Young generation,在垃圾收集的過程中都會使得用戶線程等待。
? ? Serial收集器:一種單線程的收集器,采用“復(fù)制”收集算法,收集時候會暫停所有的工作線程,直到收集結(jié)束,一般虛擬機在Client模式下的默認(rèn)新生代收集器就是采用這個收集器。優(yōu)點:與單線程收集器比較簡單高效。對于單個CPU下,由于沒有多個線程的交互開銷,在堆比較小的時候,一般停頓比較短,可以采用。
? ? ParNew收集器:是Serial的一種多線程收集器,采用“復(fù)制”收集算法,它和Serial收集器除了多線程外其余行為都相同,即在收集的過程中會暫停所有的線程。它是運行在Server模式下的新生代收集器的首選。可以使用-XX:+UseConcMarkSweepGC選項后的默認(rèn)新生代收集器,或者使用-XX:+UseParNewGC選項來強制使用它。只能與CMS收集器配合使用。
? ? Parallel Scavenge收集器:是一種并行(多條垃圾收集線程并行工作,用戶線程依舊等待)的多線程收集器,采用“復(fù)制”算法來收集新生代。它所關(guān)注的是達(dá)到一個控制的吞吐量,就是CPU運行用戶代碼時間與CPU總耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。利用-XX:MaxGCPauseMillis可設(shè)置垃圾收集停頓時間,-XX:GCTimeRatio可設(shè)置垃圾收集占的總時間,是吞吐量的倒數(shù)。如果為19,則允許GC時間為5%(1/(1+19));這種收集器也有自適應(yīng)調(diào)節(jié)策略。
老年代垃圾收集器:Tenured generation
Serial Old收集器:是一種單線程收集器,是Serial的老年代版本,使用的是“標(biāo)記-整理”算法
主要是虛擬機在Client模式下的使用的收集器。它在工作中依舊需要暫停所有的用戶線程。主要用在作為CMS收集器的后備預(yù)案使用。
? ?Parallel Old收集器:是一種多線程收集器,是Parallel Scavenge的老年代版本,使用的是“標(biāo)記-整理”算法。它在工作中依舊需要暫停所有的用戶線程。一般是結(jié)合Parallel Scavenge來一起使用,用于在注重吞吐量和CPU資源敏感的場合。
? ?CMS收集器(Concurrent Mark Sweep):采用“標(biāo)記-清除”的算法,目標(biāo)是獲取最短回收停頓時間的。整個過程分為4個步驟:
? ?1 初始標(biāo)記(Stop the world) ?2 并發(fā)標(biāo)記
? ?3 重新標(biāo)記(Stop the world) ?4 并發(fā)清除
? 初始標(biāo)記,僅僅標(biāo)記一下GC Roots能關(guān)聯(lián)到的對象,速度非常快,需要停止用戶線程。
? 并發(fā)標(biāo)記,進(jìn)行GC Roots Tracing過程,可以與用戶線程一起工作,不需要停止用戶線程。
? 重新標(biāo)記,為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象,這個時間也是很短的,需要停止用戶線程。
? 并發(fā)清除,可以與用戶線程一起工作,不需要停止用戶線程。
? ?在CMS中,耗時最長的是并發(fā)標(biāo)記和并發(fā)清除,在這兩個過程中收集器線程都可以與用戶一起工作,所以CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)地執(zhí)行。
? ?缺點:CMS對CPU資源非常敏感,無法處理浮動垃圾,會產(chǎn)生碎片。
G1收集器:采用的是“標(biāo)記-整理”算法,可以非常精確的控制停頓,可以實現(xiàn)在基本上不犧牲吞吐量的前提下完成低停頓的內(nèi)存回收。
垃圾收集器中的并發(fā)與并行:
并行(Parallel):多條垃圾收集線程并行工作,此時用戶線程處于等待停止?fàn)顟B(tài)。
并發(fā)(Concurrent):用戶線程與垃圾收集線程同時執(zhí)行,即用戶程序繼續(xù)運行,而垃圾收集程序運行在另一個CPU中。
二 內(nèi)存分配
? 對象的內(nèi)存分配,就是在Java堆上分配的,對象主要分配在堆中的新生代的Eden區(qū)。
? 內(nèi)存分配的幾個原則:
? 對象優(yōu)先在Eden分配:大多數(shù)情況下,對于一個新的對象將會首先分配在新生代的Eden區(qū),只有Eden區(qū)沒有足夠的空間進(jìn)行分配的時候,虛擬機發(fā)起一次Minor GC,可以使用-verbose:gc -XX:+PrintGCDetails來打印內(nèi)存分配的狀態(tài)。當(dāng)分配的對象無法容納在Eden區(qū)的時候,首先會將Eden中存活的對象復(fù)制到另一個Survivor中,如果Survivor無法容納這些存活的對象,則只有通過分配擔(dān)保機制將這些存活對象提前移動到老年代中,然后將要分配的對象分配到Eden區(qū)中。
? 大對象直接進(jìn)入老年代:大對象就是需要連續(xù)的大量內(nèi)存空間,最典型的就是字符串或者數(shù)組。可以設(shè)置-XX:PretenureSizeThreshold參數(shù),當(dāng)大于這個值的對象直接會在老年代中分配,避免了在Eden區(qū)和兩個Survivor之間大量的拷貝。
? 長期存活的對象將進(jìn)入老年代:虛擬機為每個對象定義了對象年齡,當(dāng)對象在Eden出生經(jīng)過第一個Minor GC還存活著,并且能被Survivor容納,則移動到Survivor,年齡加1.每次熬過一次Minor GC,對象年齡就會加1.對象晉升到老年代的年齡閥值,可以通過設(shè)置
-XX:MaxTenuringThreshold。
? 動態(tài)對象年齡判定:不一定非得達(dá)到年齡閥值才會進(jìn)入老年代,如果在Survivor空間中相同年齡的所有對象大小的總和大于Survivor空間的一半,則年齡大于或等于該年齡的對象就會直接進(jìn)入老年代,無需等待MaxTenuringThreshold的閥值年齡。
? 空間分配擔(dān)保:在發(fā)生Minor GC時候,虛擬機會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大小,如果大于,則直接對于老年代進(jìn)行一個Full GC。如果小于,則查看HandlePermotionFailure設(shè)置是否允許進(jìn)行擔(dān)保失敗,如果允許,則在新生代進(jìn)行Minor GC;如果不允許,則在老年代進(jìn)行Full GC。
注意:Minor GC和Full GC的區(qū)別
? 新生代 GC(Minor GC):指發(fā)生在新生代的垃圾收集動作,因為 Java 對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
? 老年代 GC(Major GC ?/ Full GC):指發(fā)生在老年代的 GC,出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略里就有直接進(jìn)行 Major GC 的策略選擇過程)。MajorGC的速度一般會比Minor GC慢10倍以上。Major GC會觸發(fā)整個heap的回收,包括回收young generation。
轉(zhuǎn)載于:https://blog.51cto.com/computerdragon/1220373
總結(jié)
以上是生活随笔為你收集整理的JVM的垃圾回收与内存分配的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。