JVM——GC算法原理
摘要
本文將介紹JVM垃圾回收算法相關原理。垃圾收集機制是 Java 的招牌能力,極大地提高了開發效率。如今,垃圾收集幾乎成為現代語言的標配,即使經過如此長時間的發展,Java 的垃圾收集機制仍然在不斷的演進中,不同大小的設備、不同特征的應用場景,對垃圾收集提出了新的挑戰,這當然也是面試的熱點。關于垃圾收集有三個經典問題:哪些內存需要回收?什么時候回收?如何回收?
一、GC的背景
對于高級語言來說,一個基本認知是如果不進行垃圾回收,內存遲早都會被消耗完,因為不斷地分配內存空間而不進行回收,就好像不停地生產生活垃圾而從來不打掃一樣。除了釋放沒用的對象,垃圾回收也可以清除內存里的記錄碎片。碎片整理將所占用的堆內存移到堆的一端,以便JVM 將整理出的內存分配給新的對象。隨著應用程序所應付的業務越來越龐大、復雜,用戶越來越多,沒有 GC 就不能保證應用程序的正常進行。而經常造成 STW 的 GC 又跟不上實際的需求,所以才會不斷地嘗試對 GC 進行優化。
在早期的 C/C++時代,垃圾回收基本上是手工進行的。開發人員可以使用 new 關鍵字進行內存申請,并使用 delete 關鍵字進行內存釋放。比如以下代碼:
MibBridge *pBridge= new cmBaseGroupBridge(); //如果注冊失敗,使用Delete釋放該對象所占內存區域 if (pBridge->Register(kDestroy) != NO ERROR)delete pBridge;這種方式可以靈活控制內存釋放的時間,但是會給開發人員帶來頻繁申請和釋放內存的管理負擔。倘若有一處內存區間由于程序員編碼的問題忘記被回收,那么就會產生內存泄漏,垃圾對象永遠無法被清除,隨著系統運行時間的不斷增長,垃圾對象所耗內存可能持續上升,直到出現內存溢出并造成應用程序崩潰。
在有了垃圾回收機制后,上述代碼極有可能變成這樣
MibBridge *pBridge = new cmBaseGroupBridge(); pBridge->Register(kDestroy);現在,除了 Java 以外,C#、Python、Ruby 等語言都使用了自動垃圾回收的思想,也是未來發展趨勢,可以說這種自動化的內存分配和來及回收方式已經成為了線代開發語言必備的標準。
二、JVM中GC作用域
垃圾是指在運行程序中沒有任何指針指向的對象,這個對象就是需要被回收的垃圾。如果不及時對內存中的垃圾進行清理,那么,這些垃圾對象所占的內存空間會一直保留到應用程序的結束,被保留的空間無法被其它對象使用,甚至可能導致內存溢出。Java 垃圾回收機制是自動內存管理,無需開發人員手動參與內存的分配與回收,這樣降低內存泄漏和內存溢出的風險
自動內存管理機制,將程序員從繁重的內存管理中釋放出來,可以更專心地專注于業務開發。對于 Java 開發人員而言,自動內存管理就像是一個黑匣子,如果過度依賴于“自動”,那么這將會是一場災難,最嚴重的就會弱化 Java 開發人員在程序出現內存溢出時定位問題和解決問題的能力。此時,了解 JVM 的自動內存分配和內存回收原理就顯得非常重要,只有在真正了解 JVM 是如何管理內存后,我們才能夠在遇見 outofMemoryError 時,快速地根據錯誤異常日志定位問題和解決問題。當需要排查各種內存溢出、內存泄漏問題時,當垃圾收集成為系統達到更高并發量的瓶頸時,我們就必須對這些“自動化”的技術實施必要的監控和調節。GC 主要關注的區域:方法區和堆中的垃圾收集。垃圾收集器可以對年輕代回收,也可以對老年代回收,甚至是全棧和方法區的回收。其中,Java 堆是垃圾收集器的工作重點
- 頻繁收集 Young 區
- 較少收集 Old 區
- 基本不收集 Perm 區(元空間)
三、判斷對象是否是垃圾方法
在堆里存放著幾乎所有的 Java 對象實例,在 GC 執行垃圾回收之前,首先需要區分出內存中哪些是存活對象,哪些是已經死亡的對象。只有被標記為己經死亡的對象,GC 才會在執行垃圾回收時,釋放掉其所占用的內存空間,因此這個過程我們可以稱為垃圾標記階段。那么在 JVM 中究竟是如何標記一個死亡對象呢?簡單來說,當一個對象已經不再被任何的存活對象繼續引用時,就可以宣判為已經死亡。判斷對象存活一般有兩種方式:引用計數算法和可達性分析算法。
3.1 引用計數算法
引用計數算法(Reference Counting)比較簡單,對每個對象保存一個整型的引用計數器屬性。用于記錄對象被引用的情況。對于一個對象 A,只要有任何一個對象引用了 A,則 A 的引用計數器就加 1;當引用失效時,引用計數器就減 1。只要對象 A 的引用計數器的值為 0,即表示對象 A 不可能再被使用,可進行回收。
優點:實現簡單,垃圾對象便于辨識;判定效率高,回收沒有延遲性。
缺點:
- 它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷。
- 每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷。
- 引用計數器有一個嚴重的問題,即無法處理循環引用的情況。這是一條致命缺陷,導致在 Java 的垃圾回收器中沒有使用這類算法。
循環引用:當 p 的指針斷開的時候,內部的引用形成一個循環,這就是循環引用
public class RefCountGC {// 這個成員屬性的唯一作用就是占用一點內存private byte[] bigSize = new byte[5*1024*1024];// 引用Object reference = null;public static void main(String[] args) {RefCountGC obj1 = new RefCountGC();RefCountGC obj2 = new RefCountGC();obj1.reference = obj2;obj2.reference = obj1;obj1 = null;obj2 = null;// 顯示的執行垃圾收集行為// 這里發生GC,obj1和obj2是否被回收?System.gc();} } // 運行結果 PSYoungGen: 15490K->808K(76288K)] 15490K->816K(251392K)上述進行了GC收集的行為,所以可以證明JVM中采用的不是引用計數器的算法。
引用計數算法,是很多語言的資源回收選擇,例如因人工智能而更加火熱的 Python,它更是同時支持引用計數和垃圾收集機制。具體哪種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提高吞吐量的嘗試。Java并沒有選擇引用計數,是因為其存在一個基本的難題,也就是很難處理循環引用關系。Python 如何解決循環引用?手動解除:很好理解,就是在合適的時機,解除引用關系。 使用弱引用 weakref,weakref 是 Python 提供的標準庫,旨在解決循環引用。
3.2 可達性分析算法
可達性分析算法(根搜索算法、追蹤性垃圾收集)相對于引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。相較于引用計數算法,這里的可達性分析就是 Java、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Garbage Collection)所謂"GCRoots”根集合就是一組必須活躍的引用。
基本思路
- 可達性分析算法是以根對象集合(GCRoots)為起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達。
- 使用可達性分析算法后,內存中的存活對象都會被根對象集合直接或間接連接著,搜索所走過的路徑稱為引用鏈(Reference Chain)
- 如果目標對象沒有任何引用鏈相連,則是不可達的,就意味著該對象己經死亡,可以標記為垃圾對象。
- 在可達性分析算法中,只有能夠被根對象集合直接或者間接連接的對象才是存活對象。
在 Java 語言中,GC Roots 包括以下幾類元素:
- 虛擬機棧中引用的對象。比如:各個線程被調用的方法中使用到的參數、局部變量等。
- 本地方法棧內 JNI(通常說的本地方法)引用的對象
- 方法區中類靜態屬性引用的對象。比如:Java 類的引用類型靜態變量
- 方法區中常量引用的對象。比如:字符串常量池(String Table)里的引用
- 所有被同步鎖 synchronized 持有的對象
- Java 虛擬機內部的引用?;緮祿愋蛯?Class 對象,一些常駐的異常對象(如:NullPointerException、OutOfMemoryError),系統類加載器。
- 反映 java 虛擬機內部情況的 JMXBean、JVMTI 中注冊的回調、本地代碼緩存等。
- 除了這些固定的 GC Roots 集合以外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象“臨時性”地加入,共同構成完整 GC Roots 集合。比如:分代收集和局部回收(PartialGC)。
如果只針對 Java 堆中的某一塊區域進行垃圾回收(比如:典型的只針對新生代),必須考慮到內存區域是虛擬機自己的實現細節,更不是孤立封閉的,這個區域的對象完全有可能被其他區域的對象所引用,這時候就需要一并將關聯的區域對象也加入 GCRoots 集合中去考慮,才能保證可達性分析的準確性。
小技巧:由于 Root 采用棧方式存放變量和指針,所以如果一個指針,它保存了堆內存里面的對象,但是自己又不存放在堆內存里面,那它就是一個 Root。注意:如果要使用可達性分析算法來判斷內存是否可回收,那么分析工作必須在一個能保障一致性的快照中進行。這點不滿足的話分析結果的準確性就無法保證。這點也是導致 GC 進行時必須“stop The World”的一個重要原因。即使是號稱(幾乎)不會發生停頓的 CMS 收集器中,枚舉根節點時也是必須要停頓的。
3.3 對象的 finalization 機制
Java 語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷毀之前的自定義處理邏輯。當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調用這個對象的 finalize()方法。finalize() 方法允許在子類中被重寫,用于在對象被回收時進行資源釋放。通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件、套接字和數據庫連接等。
永遠不要主動調用某個對象的 finalize()方法 I 應該交給垃圾回收機制調用。理由包括下面三點:
- 在 finalize()時可能會導致對象復活。
- finalize()方法的執行時間是沒有保障的,它完全由 GC 線程決定,極端情況下,若不發生 GC,則 finalize()方法將沒有執行機會。
- 一個糟糕的 finalize()會嚴重影響 Gc 的性能。
從功能上來說,finalize()方法與 C++中的析構函數比較相似,但是 Java 采用的是基于垃圾回收器的自動內存管理機制,所以 finalize()方法在本質上不同于 C++中的析構函數。由于 finalize()方法的存在,虛擬機中的對象一般處于三種可能的狀態。
3.4 對象生存還是死亡
如果從所有的根節點都無法訪問到某個對象,說明對象己經不再使用了。一般來說,此對象需要被回收。但事實上,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段。一個無法觸及的對象有可能在某一個條件下“復活”自己,如果這樣,那么對它的回收就是不合理的,為此,定義虛擬機中的對象可能的三種狀態。如下:
- 可觸及的:從根節點開始,可以到達這個對象。
- 可復活的:對象的所有引用都被釋放,但是對象有可能在 finalize()中復活。
- 不可觸及的:對象的 finalize()被調用,并且沒有復活,那么就會進入不可觸及狀態。不可觸及的對象不可能被復活,因為finalize()只會被調用一次。
以上 3 種狀態中,是由于 inalize()方法的存在,進行的區分。只有在對象不可觸及時才可以被回收。
判定一個對象 objA 是否可回收,至少要經歷兩次標記過程:
在第一次 GC 時,執行了 finalize 方法,但 finalize()方法只會被調用一次,所以第二次該對象被 GC 標記并清除了。
public class CanReliveObj {// 類變量,屬于GC Roots的一部分public static CanReliveObj canReliveObj;@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調用當前類重寫的finalize()方法");canReliveObj = this;}public static void main(String[] args) throws InterruptedException {canReliveObj = new CanReliveObj();canReliveObj = null;System.gc();System.out.println("-----------------第一次gc操作------------");// 因為Finalizer線程的優先級比較低,暫停2秒,以等待它Thread.sleep(2000);if (canReliveObj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}System.out.println("-----------------第二次gc操作------------");canReliveObj = null;System.gc();// 下面代碼和上面代碼是一樣的,但是 canReliveObj卻自救失敗了Thread.sleep(2000);if (canReliveObj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}} } --------------------運行結果---------------------------------------------------第一次gc操作------------ 調用當前類重寫的finalize()方法 obj is still alive -----------------第二次gc操作------------ obj is dead四、GC算法原理
當成功區分出內存中存活對象和死亡對象后,GC 接下來的任務就是執行垃圾回收,釋放掉無用對象所占用的內存空間,以便有足夠的可用內存空間為新對象分配內存。目前在 JVM 中比較常見的三種垃圾收集算法是標記一清除算法(Mark-Sweep)、復制算法(copying)、標記-壓縮算法(Mark-Compact)
4.1 標記-清除算法
當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為 stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除
-
標記:Collector 從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的 Header 中記錄為可達對象。
-
清除:Collector 對堆內存從頭到尾進行線性的遍歷,如果發現某個對象在其 Header 中沒有標記為可達對象,則將其回收
?優點
- 沒有標記和清除過程,實現簡單,運行高效
缺點
- 標記清除算法的效率不算高
- 在進行 GC 的時候,需要停止整個應用程序,用戶體驗較差
- 這種方式清理出來的空閑內存是不連續的,產生內碎片,需要維護一個空閑列表
何為清除?這里所謂的清除并不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放覆蓋原有的地址。
4.2 復制算法
將活著的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之后清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最后完成垃圾回收。
優點
- 沒有標記和清除過程,實現簡單,運行高效
- 復制過去以后保證空間的連續性,不會出現“碎片”問題。
缺點
- 此算法的缺點也是很明顯的,就是需要兩倍的內存空間。
- 對于 G1 這種分拆成為大量 region 的 GC,復制而不是移動,意味著 GC 需要維護 region 之間對象引用關系,不管是內存占用或者時間開銷也不小
特別的:如果系統中的垃圾對象很多,復制算法需要復制的存活對象數量并不會太大,或者說非常低才行。應用場景比如在在新生代,對常規應用的垃圾回收,一次通常可以回收 70% - 99% 的內存空間?;厥招詢r比很高。所以現在的商業虛擬機都是用這種收集算法回收新生代。
4.3 標記-整理算法
復制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經常發生,但是在老年代,更常見的情況是大部分對象都是存活對象。如果依然使用復制算法,由于存活對象較多,復制的成本也將很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
執行過程
標記-壓縮算法的最終效果等同于標記-清除算法執行完成后,再進行一次內存碎片整理,因此,也可以把它稱為標記-清除-壓縮(Mark-Sweep-Compact)算法。二者的本質差異在于標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收后的存活對象是一項優缺點并存的風險決策??梢钥吹?#xff0c;標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM 只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。
4.4 指針碰撞(Bump the Pointer)
如果內存空間以規整和有序的方式分布,即已用和未用的內存都各自一邊,彼此之間維系著一個記錄下一次分配起始點的標記指針,當為新對象分配內存時,只需要通過修改指針的偏移量將新對象分配在第一個空閑內存位置上,這種分配方式就叫做指針碰撞(Bump tHe Pointer)。
優點
- 消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象分配內存時,JVM 只需要持有一個內存的起始地址即可。
- 消除了復制算法當中,內存減半的高額代價。
缺點
- 從效率上來說,標記-整理算法要低于復制算法。
- 移動對象的同時,如果對象被其他對象引用,則還需要調整引用的地址
- 移動過程中,需要全程暫停用戶應用程序。即:STW
效率上來說,復制算法是當之無愧的老大,但是卻浪費了太多內存。而為了盡量兼顧上面提到的三個指標,標記-整理算法相對來說更平滑一些,但是效率上不盡如人意,它比復制算法多了一個標記的階段,比標記-清除多了一個整理內存的階段,沒有最好的算法,只有最合適的算法。
4.5 分代收集算法
前面所有這些算法中,并沒有一種算法可以完全替代其他算法,它們都具有自己獨特的優勢和特點。分代收集算法應運而生。分代收集算法,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。在 Java 程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http 請求中的 Session 對象、線程、Socket 連接,這類對象跟業務直接掛鉤,因此生命周期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如:String 對象,由于其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。目前幾乎所有的 GC 都采用分代手機算法執行垃圾回收的。在 HotSpot 中,基于分代的概念,GC 所使用的內存回收算法必須結合年輕代和老年代各自的特點。
4.5.1 年輕代(Young Gen)
年輕代特點:區域相對老年代較小,對象生命周期短、存活率低,回收頻繁。
這種情況復制算法的回收整理,速度是最快的。復制算法的效率只和當前存活對象大小有關,因此很適用于年輕代的回收。而復制算法內存利用率不高的問題,通過 hotspot 中的兩個 survivor 的設計得到緩解。
4.5.2 老年代(Tenured Gen)
老年代特點:區域較大,對象生命周期長、存活率高,回收不及年輕代頻繁。
這種情況存在大量存活率高的對象,復制算法明顯變得不合適。一般是由標記-清除或者是標記-清除與標記-整理的混合實現。
- Mark 階段的開銷與存活對象的數量成正比。
- Sweep 階段的開銷與所管理區域的大小成正相關。
- Compact 階段的開銷與存活對象的數據成正比。
以 HotSpot 中的 CMS 回收器為例,CMS 是基于 Mark-Sweep 實現的,對于對象的回收效率很高。而對于碎片問題,CMS 采用基于 Mark-Compact 算法的 Serial Old 回收器作為補償措施:當內存回收不佳(碎片導致的 Concurrent Mode Failure 時),將采用 Serial Old 執行 Full GC 以達到對老年代內存的整理。
分代的思想被現有的虛擬機廣泛使用。幾乎所有的垃圾回收器都區分新生代和老年代。
4.6 增量收集算法、分區算法
4.6.1 增量收集算法
上述現有的算法,在垃圾回收過程中,應用軟件將處于一種 Stop the World 的狀態。在 Stop the World 狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。為了解決這個問題,即對實時垃圾收集算法的研究直接導致了增量收集(Incremental Collecting)算法誕生。
基本思想:如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程。依次反復,直到垃圾收集完成。
總的來說,增量收集算法的基礎仍是傳統的標記-清除和復制算法。增量收集算法通過對線程間沖突的妥善處理,允許垃圾收集線程以分階段的方式完成標記、清理或復制工作
缺點:使用這種方式,由于在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
4.6.2 分區算法
一般來說,在相同條件下,堆空間越大,一次 Gc 時所需要的時間就越長,有關 GC 產生的停頓也越長。為了更好地控制 GC 產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次 GC 所產生的停頓。
分代算法將按照對象的生命周期長短劃分成兩個部分,分區算法將整個堆空間劃分成連續的不同小區間。每一個小區間都獨立使用獨立回收。這種算法的好處可以控制一次回收多少個小區間。注意,這些只是基本的算法思路,實際 GC 實現過程要復雜的多,目前還在發展中的前沿 GC 都是復合算法,并且并行和并發兼備。
System.gc()的理解:在默認情況下,通過 system.gc()或者 Runtime.getRuntime().gc() 的調用,會顯式觸發 Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄對象占用的內存。
然而 System.gc() 調用附帶一個免責聲明,無法保證對垃圾收集器的調用。(不能確保立即生效)。JVM 實現者可以通過 System.gc() 調用來決定 JVM 的 GC 行為。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過于麻煩了。在一些特殊情況下,如我們正在編寫一個性能基準,我們可以在運行之間調用 System.gc()
public class SystemGCTest {public static void main(String[] args) {new SystemGCTest();System.gc();// 提醒JVM的垃圾回收器執行gc,但是不確定是否馬上執行gc// 與Runtime.getRuntime().gc();的作用一樣System.runFinalization();//強制執行使用引用的對象的finalize()方法}@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("SystemGCTest 重寫了finalize()");} }五、內存溢出與內存泄露
5.1 內存溢出(OOM)
內存溢出相對于內存泄漏來說,盡管更容易被理解,但是同樣的,內存溢出也是引發程序崩潰的罪魁禍首之一。由于 GC 一直在發展,所有一般情況下,除非應用程序占用的內存增長速度非???#xff0c;造成垃圾回收已經跟不上內存消耗的速度,否則不太容易出現 ooM 的情況。大多數情況下,GC 會進行各種年齡段的垃圾回收,實在不行了就放大招,來一次獨占式的 Full GC 操作,這時候會回收大量的內存,供應用程序繼續使用。javadoc 中對 OutOfMemoryError 的解釋是,沒有空閑內存,并且垃圾收集器也無法提供更多內存。
首先說沒有空閑內存的情況:說明 Java 虛擬機的堆內存不夠。
Java 虛擬機的堆內存設置不夠。比如:可能存在內存泄漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的數據量,但是沒有顯式指定 JVM 堆大小或者指定數值偏小。我們可以通過參數-Xms 、-Xmx來調整。
代碼中創建了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)對于老版本的 Oracle JDK,因為永久代的大小是有限的,并且 JVM 對永久代垃圾回收(如,常量池回收、卸載不再需要的類型)非常不積極,所以當我們不斷添加新類型的時候,永久代出現 OutOfMemoryError 也非常多見,尤其是在運行時存在大量動態類型生成的場合;類似 intern 字符串緩存占用太多空間,也會導致 OOM 問題。對應的異常信息,會標記出來和永久代相關:“java.lang.OutOfMemoryError: PermGen space"。隨著元數據區的引入,方法區內存已經不再那么窘迫,所以相應的 ooM 有所改觀,出現 OOM,異常信息則變成了:“java.lang.OutofMemoryError:Metaspace"。直接內存不足,也會導致 OOM。
這里面隱含著一層意思是,在拋出 OutOfMemoryError 之前,通常垃圾收集器會被觸發,盡其所能去清理出空間。
- 例如:在引用機制分析中,涉及到 JVM 會去嘗試回收軟引用指向的對象等。
- 在java.nio.BIts.reserveMemory()方法中,我們能清楚的看到,System.gc()會被調用,以清理空間。
當然,也不是在任何情況下垃圾收集器都會被觸發的
- 比如,我們去分配一個超大對象,類似一個超大數組超過堆的最大值,JVM 可以判斷出垃圾收集并不能解決這個問題,所以直接拋出 OutOfMemoryError。
5.2 內存泄漏(Memory Leak)
也稱作“存儲滲漏”。嚴格來說,只有對象不會再被程序用到了,但是 GC 又不能回收他們的情況,才叫內存泄漏。但實際情況很多時候一些不太好的實踐(或疏忽)會導致對象的生命周期變得很長甚至導致 00M,也可以叫做寬泛意義上的“內存泄漏”。盡管內存泄漏并不會立刻引起程序崩潰,但是一旦發生內存泄漏,程序中的可用內存就會被逐步蠶食,直至耗盡所有內存,最終出現 OutOfMemory 異常,導致程序崩潰。注意,這里的存儲空間并不是指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決于磁盤交換區設定的大小。
- 單例模式
- 單例的生命周期和應用程序是一樣長的,所以單例程序中,如果持有對外部對象的引用的話,那么這個外部對象是不能被回收的,則會導致內存泄漏的產生。
- 一些提供 close 的資源未關閉導致內存泄漏
- 數據庫連接(dataSourse.getConnection() ),網絡連接(socket)和 io 連接必須手動 close,否則是不能被回收的。
5.3 Stop The World
Stop-the-World,簡稱 STW,指的是 GC 事件發生過程中,會產生應用程序的停頓。停頓產生時整個應用程序線程都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為 STW。
可達性分析算法中枚舉根節點(GC Roots)會導致所有 Java 執行線程停頓。
- 分析工作必須在一個能確保一致性的快照中進行
- 一致性指整個分析期間整個執行系統看起來像被凍結在某個時間點上
- 如果出現分析過程中對象引用關系還在不斷變化,則分析結果的準確性無法保證
被STW中斷的應用程序線程會在完成GC之后恢復,頻繁中斷會讓用戶感覺像是網速不快造成電影卡帶一樣,所以我們需要減少 STW 的發生。STW事件和采用哪款 GC 無關,所有的 GC 都有這個事件。哪怕是G1也不能完全避免 Stop-the-World 情況發生,只能說垃圾回收器越來越優秀,回收效率越來越高,盡可能地縮短了暫停時間。STW 是 JVM 在后臺自動發起和自動完成的。在用戶不可見的情況下,把用戶正常的工作線程全部停掉。開發中不要用 System.gc() 會導致 Stop-the-World 的發生。
六、四大引用
在 JDK1.2 版之后,Java 對引用的概念進行了擴充,將引用分為:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)這 4 種引用強度依次逐漸減弱。除強引用外,其他 3 種引用均可以在 java.lang.ref 包中找到它們的身影。如下圖,顯示了這3種引用類型對應的類,開發人員可以在應用程序中直接使用它們。
Reference 子類中只有終結器引用是包內可見的,其他 3 種引用類型均為 public,可以在應用程序中直接使用。
- 強引用(StrongReference):最傳統的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj = new Object()”這種引用關系。無論任何情況下,只要強引用關系還存在,垃圾收集器就永遠不會回收掉被引用的對象。
- 軟引用(SoftReference):在系統將要發生內存溢出之前,將會把這些對象列入回收范圍之中進行第二次回收。如果這次回收后還沒有足夠的內存,才會拋出內存流出異常。
- 弱引用(WeakReference):被弱引用關聯的對象只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論內存空間是否足夠,都會回收掉被弱引用關聯的對象。
- 虛引用(PhantomReference):一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
6.1?強引用(GC不回收)
在Java 程序中,最常見的引用類型是強引用(普通系統 99%以上都是強引用),也就是我們最常見的普通對象引用,也是默認的引用類型。
當在Java 語言中使用 new 操作符創建一個新的對象,并將其賦值給一個變量的時候,這個變量就成為指向該對象的一個強引用。強引用的對象是可觸及的,垃圾收集器就永遠不會回收掉被引用的對象。
對于一個普通的對象,如果沒有其他的引用關系,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為 null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。相對的,軟引用、弱引用和虛引用的對象是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都是可以被回收的。所以,強引用是造成 Java 內存泄漏的主要原因之一。
強引用例子:StringBuffer str = new StringBuffer("hello mogublog");局部變量 str 指向 StringBuffer 實例所在堆空間,通過 str 可以操作該實例,那么 str 就是 StringBuffer 實例的強引用此時,如果再運行一個賦值語句: StringBuffer str1 = str;對應的內存結構
?本例中的兩個引用,都是強引用,強引用具備以下特點:
- 強引用可以直接訪問目標對象。
- 強引用所指向的對象在任何時候都不會被系統回收,虛擬機寧愿拋出 OOM 異常,也不會回收強引用所指向對象。
- 強引用可能導致內存泄漏。
6.2?軟引用(內存不足即回收)
軟引用是用來描述一些還有用,但非必需的對象。只被軟引用關聯著的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
軟引用通常用來實現內存敏感的緩存。比如:高速緩存就有用到軟引用。如果還有空閑內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。GC在某個時刻決定回收軟可達的對象的時候,會清理軟引用,并可選地把引用存放到一個引用隊列(Reference Queue)。類似弱引用,只不過 Java 虛擬機會盡量讓軟引用的存活時間長一些,迫不得已才清理。
在 JDK1.2 版之后提供了 java.lang.ref.SoftReference 類來實現軟引用Object obj = new Object(); // 聲明強引用SoftReference<Object> sf = new SoftReference<>(obj);obj = null; //銷毀強引用6.3?弱引用(發現即回收)
弱引用也是用來描述那些非必需對象,只被弱引用關聯的對象只能生存到下一次垃圾收集發生為止。在系統 GC 時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用關聯的對象。但是,由于垃圾回收器的線程通常優先級很低,因此,并不一定能很快地發現持有弱引用的對象。在這種情況下,弱引用對象可以存在較長的時間。
弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用隊列,當弱引用對象被回收時,就會加入指定的引用隊列,通過這個隊列可以跟蹤對象的回收情況。軟引用、弱引用都非常適合來保存那些可有可無的緩存數據。如果這么做,當系統內存不足時,這些緩存數據會被回收,不會導致內存溢出。而當內存資源充足時,這些緩存數據又可以存在相當長的時間,從而起到加速系統的作用。
在 JDK1.2 版之后提供了 WeakReference 類來實現弱引用Object obj = new Object(); // 聲明強引用WeakReference<Object> sf = new WeakReference<>(obj);obj = null; //銷毀強引用弱引用對象與軟引用對象的最大不同就在于,當 GC 在進行回收時,需要通過算法檢查是否回收軟引用對象,而對于弱引用對象,GC 總是進行回收。弱引用對象更容易、更快被 GC 回收。
你開發中使用過 WeakHashMap 嗎?WeakHashMap 用來存儲圖片信息,可以在內存不足的時候,及時回收,避免了OOM。
6.4?虛引用(對象回收跟蹤)
也稱為“幽靈引用”或者“幻影引用”,是所有引用類型中最弱的一個。一個對象是否有虛引用的存在,完全不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。它不能單獨使用,也無法通過虛引用來獲取被引用的對象。當試圖通過虛引用的 get()方法取得對象時,總是 null.
為一個對象設置虛引用關聯的唯一目的在于跟蹤垃圾回收過程。比如:能在這個對象被收集器回收時收到一個系統通知。虛引用必須和引用隊列一起使用。虛引用在創建時必須提供一個引用隊列作為參數。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象后,將這個虛引用加入引用隊列,以通知應用程序對象的回收情況。由于虛引用可以跟蹤對象的回收時間,可以將一些資源釋放操作放置在虛引用中執行和記錄。
在 JDK1.2 版之后提供了 PhantomReference 類來實現虛引用。Object obj = new Object(); // 聲明強引用ReferenceQueue phantomQueue = new ReferenceQueue();PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);obj = null;6.5?終結器引用
它用于實現對象的 finalize() 方法,也可以稱為終結器引用。無需手動編碼,其內部配合引用隊列使用。在 GC 時,終結器引用入隊。由 Finalizer 線程通過終結器引用找到被引用對象調用它的 finalize()方法,第二次 GC 時才回收被引用的對象。
博文參考
《JVM虛擬機原理》
總結
以上是生活随笔為你收集整理的JVM——GC算法原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MATLAB使用GPU加速计算
- 下一篇: 关于我于Security Assista