您好,登錄后才能下訂單哦!
今天小編給大家分享一下Java垃圾收集器與內存分配的方法是什么的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
Java內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的(盡管在運行期會由即時編譯器進行一些優化,但在基于概念模型的討論里,大體上可以認為是編譯期可知的),因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮如何回收的問題,當方法結束或者線程結束時,內存自然就跟隨著
回收了。
引用計數算法
在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的。
單純的引用計數就很難解決對象之間相互循環引用的問題。
可達性分析算法
這個算法的基本思路就是通過一系列稱為“GC Roots”的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。
為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2版之后提供了PhantomReference類來實現虛引用。
虛引用場景?
即使在可達性分析算法中判定為不可達的對象,也不是“非死不可”的,這時候它們暫時還處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那么虛擬機將這兩種情況都視為“沒有必要執行”。
如果這個對象被判定為確有必要執行finalize()方法,那么該對象將會被放置在一個名為F-Queue的隊列之中,并在稍后由一條由虛擬機自動建立的、低調度優先級的Finalizer線程去執行它們的finalize()方法。
這樣做的原因是,如果某個對象的finalize()方法執行緩慢,或者更極端地發生了死循環,將很可能導致F-Queue隊列中的其他對象永久處于等待,甚至導致整個內存回收子系統的崩潰。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后收集器將對F-Queue中的對象進行第二次小規模的標記,如果對
象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。
這是因為任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了。
finalize()它的運行代價高昂,不確定性大,無法保證各個對象的調用順序,如今已被官方明確聲明為不推薦使用的語法。有些教材中描述它適合做“關閉外部資源”之類的清理性工作,這完全是對finalize()方法用途的一種自我安慰。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時,所以筆者建議大家完全可以忘掉Java語言里面的這個方法。
回收方法區
《Java虛擬機規范》中提到過可以不要求虛擬機在方法區中實現垃圾收集,事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK 11時期的ZGC收集器就不支持類卸載)
方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的類型。回收廢棄常量與回收Java堆中的對象非常類似。舉個常量池中字面量回收的例子,假如一個字符串“java”曾經進入常量池中,但是當前系統又沒有任何一個字符串對象的值是“java”,換句話說,已經沒有任何字符串對象引用常量池中的“java”常量,且虛擬機中也沒有其他地方引用這個字面量。如果在這時發生內存回收,而且垃圾收集器判斷確有必要的話,這個“java”常量就將會被系統清理出常量池。常量池中其他類(接口)、方法、字段的符號引用也與此類似。
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
·該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
·加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
·該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關于是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數需要FastDebug版 [1] 的虛擬機支持。
在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。
垃圾收集算法
從如何判定對象消亡的角度出發,垃圾收集算法可以劃分為“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作“直接垃圾收集”和“間接垃圾收集”。
分代收集理論
1)弱分代假說(Weak Generational Hypothesis):絕大多數對象都是朝生夕滅的。
2)強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。
3)跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對于同代引用來說僅占極少數。
這其實是可根據前兩條假說邏輯推理得出的隱含推論:存在互相引用關系的兩個對象,是應該傾向于同時生存或者同時消亡的。舉個例子,如果某個新生代對象存在跨代引用,由于老年代對象難以消亡,該引用會使得新生代對象在收集時同樣得以存活,進而在年齡增長之后晉升到老年代中,這時跨代引用也隨即被消除了。
分代收集并非只是簡單劃分一下內存區域那么容易,它至少存在一個明顯的困難:對象不是孤立的,對象之間會存在跨代引用。
假如要現在進行一次只局限于新生代區域內的收集(Minor GC),但新生代中的對象是完全有可能被老年代所引用的,為了找出該區域中的存活對象,不得不在固定的GC Roots之外,再額外遍歷整個老年代中所有對象來確保可達性分析結果的正確性,反過來也是一樣 [3] 。遍歷整個老年代所有對象的方案雖然理論上可行,但無疑會為內存回收帶來很大的性能負擔。為了解決這個問題,就需要對分代收集理論添加第三條經驗法則: 跨代引用假說。
依據這條假說,我們就不應再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,只需在新生代上建立一個全局的數據結構(該結構被稱為“記憶集”,Remembered Set),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊內存會存在跨代引用。此后當發生Minor GC時,只有包含了跨代引用的小塊內存里的對象才會被加入到GCRoots進行掃描。雖然這種方法需要在對象改變引用關系(如將自己或者某個屬性賦值)時維護記錄數據的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是劃算的。
部分收集(Partial GC 怕羞):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:
■新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
■老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。實際上除了CMS收集器,其他都不存在只針對老年代的收集。
■混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
■整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。
標記-清除算法
它的主要缺點有兩個:第一個是執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執行效率都隨對象數量增長而降低;第二個是內存空間的碎片化問題,標記、清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致當以后在程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
標記-復制算法
為了解決標記-清除算法面對大量可回收對象時執行效率低的問題。現在的商用Java虛擬機大多都優先采用了這種收集算法去回收新生代。
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。如果內存中多數對象都是存活的,這種算法將會產生大量的內存間復制的開銷,但對于多數對象都是可回收的情況,算法需要復制的就是占少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指針,按順序分配即可。
這種復制回收算法的代價是將可用內存縮小為了原來的一半,空間浪費未免太多了一點。
Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾搜集時,將Eden和Survivor中仍然存活的對象一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內存空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被“浪費”的。當Survivor空間不足以容納一次Minor GC之后存活的對象時,就需要依賴其他內存區域(實際上大多就是老年代)進行分配擔保(Handle Promotion)。
標記-整理算法
針對老年代對象的存亡特征,1974年Edward Lueders提出了另外一種有針對性的“標記-整理”(Mark-Compact)算法,其中的標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然后直接清理掉邊界以外的內存。
標記-清除算法與標記-整理算法的本質差異在于前者是一種非移動式的回收算法,而后者是移動式的。是否移動回收后的存活對象是一項優缺點并存的風險決策:
如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行 ,這就更加讓使用者不得不小心翼翼地權衡其弊端了,像這樣的停頓被最初的虛擬機設計者形象地描述為“Stop The World”。
[1] 最新的ZGC和Shenandoah收集器使用讀屏障(Read Barrier)技術實現了整理過程與用戶線程的并發執行,稍后將會介紹這種收集器的工作原理。
[2] 通常標記-清除算法也是需要停頓用戶線程來標記、清理可回收對象的,只是停頓時間相對而言要來的短而已。
但如果跟標記-清除算法那樣完全不考慮移動和整理存活對象的話,彌散于堆中的存活對象導致的空間碎片化問題就只能依賴更為復雜的內存分配器和內存訪問器來解決。譬如通過“分區空閑分配鏈表”來解決內存分配問題(計算機硬盤存儲大文件就不要求物理連續的磁盤空間,能夠在碎片化的硬盤上存儲和訪問就是通過硬盤分區表實現的)。內存的訪問是用戶程序最頻繁的操作,甚至都沒有之一,假如在這個環節上增加了額外的負擔,勢必會直接影響應用程序的吞吐量。
基于以上兩點,是否移動對象都存在弊端,移動則內存回收時會更復雜,不移動則內存分配時會更復雜。從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓,但是從整個程序的吞吐量來看,移動對象會更劃算。此語境中,吞吐量的實質是賦值器(Mutator,可以理解為使用垃圾收集的用戶程序,本書為便于理解,多數地方用“用戶程序”或“用戶線程”代替)與收集器的效率總和。
HotSpot虛擬機里面關注吞吐量的Parallel Scavenge收集器是基于標記-整理算法的,而關注延遲的CMS收集器則是基于標記-清除算法的,這也從側面印證這點。
另外,還有一種“和稀泥式”解決方案可以不在內存分配和訪問上增加太大額外負擔,做法是讓虛擬機平時多數時間都采用標記-清除算法,暫時容忍內存碎片的存在,直到內存空間的碎片化程度已經大到影響對象分配時,再采用標記-整理算法收集一次,以獲得規整的內存空間。前面提到的基于標記-清除算法的CMS收集器面臨空間碎片過多時采用的就是這種處理辦法。
以上就是“Java垃圾收集器與內存分配的方法是什么”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。