JVM內存模型&&垃圾回收

概述:
虛擬機自動內存管理機制下,不再需要像C/C++程序開發程序員這樣爲內一個new 操作去寫對應的delete/free操作,不容易出現內存泄漏和內存溢出問題。正是因爲Java程序員把內存控制權利交給Java虛擬機,一旦出現內存泄漏和溢出方面的問題,如果不瞭解虛擬機是怎樣使用內存的,那麼排查錯誤將會是一個非常艱鉅的任務
運行時數據區域:
Java虛擬機在執行Java程序的過程中會把它管理的內存劃分成若干個不同的數據區域。
在這裏插入圖片描述

1. 程序計數器
程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完。
另外,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

2. Java虛擬機棧
與程序計數器一樣,Java虛擬機棧也是線程私有的,它的生命週期和線程相同,描述的是Java方法執行的內存模型。

Java內存可以粗糙的區分爲堆內存(Heap)和棧內存(Stack),其中棧就是現在說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。

局部變量表主要存放了編譯器可知的各種數據類型、對象引用。

3. 本地方法棧
和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧爲虛擬機執行Java方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。

4. 堆

Java虛擬機所管理的內存中最大的一塊,Java堆是所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這裏分配內存。Java堆是垃圾收集器管理的主要區域,因此也被稱作GC堆(Garbage Collected Heap).從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集算法,所以Java堆還可以細分爲:新生代和老年代:在細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。

5. 方法區

方法區與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯後的代碼等數據。

HotSpot虛擬機中方法區也常被稱爲 “永久代”,本質上兩者並不等價。僅僅是因爲HotSpot虛擬機設計團隊用永久代來實現方法區而已,這樣HotSpot虛擬機的垃圾收集器就可以像管理Java堆一樣管理這部分內存了。但是這並不是一個好主意,因爲這樣更容易遇到內存溢出問題。
相對而言,垃圾收集行爲在這個區域是比較出現的,但並非數據進入方法區後就“永久存在”了。

6. 運行時常量池

運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用於存放編譯期生成的各種字面量和符號引用)

7. 直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致OutOfMemoryError異常出現。

JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基於通道(Channel) 與緩存區(Buffer) 的I/O方式,它可以直接使用Native函數庫直接分配堆外內存,然後通過一個存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆之間來回複製數據。

本機直接內存的分配不會收到Java堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

Java堆中對象分配

1. 對象的創建

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後便可確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。分配方式有 “指針碰撞” 和 “空閒列表” 兩種,選擇那種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性。

接下來,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的GC分代年齡等信息。這些信息存放在對象頭中,根據虛擬機當前運行狀態的不同,如是否啓用偏向鎖等,對象頭會與不同的設置方式。
new指令執行完後,再按照程序員的意願執行init方法後一個真正可用的對象才誕生。

2. 對象的內存佈局

在Hotspot虛擬機中,對象在內存中的佈局可以分爲3快區域:對象頭、實例數據和對齊填充。

Hotspot虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的自身運行時數據(哈希嗎、GC分代年齡、鎖狀態標誌等等),另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。

實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內容。

對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位作用。 因爲Hotspot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

3. 對象的訪問定位

建立對象就是爲了使用對象,我們的Java程序通過棧上的reference數據來操作堆上的具體對象。對象的訪問方式有虛擬機實現而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:

1.如果使用句柄的話,那麼Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;

在這裏插入圖片描述

2.如果使用直接指針訪問,那麼Java堆對像的佈局中就必須考慮如何防止訪問類型數據的相關信息,reference中存儲的直接就是對象的地址。

在這裏插入圖片描述

這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是reference中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而reference本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷

垃圾回收 : 哪些垃圾需要回收,什麼時候回收,如何回收

哪些需要回收

堆中幾乎放着所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)

1.引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任何時候計數器爲0的對象就是不可能再被使用的。

這個方法實現簡單,效率高,但是目前主流的虛擬機中並沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。

2.可達性分析算法

這個算法的基本思想就是通過一系列的稱爲 “GC Roots” 的對象作爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的話,則證明此對象是不可用的。

在這裏插入圖片描述
3.生存還是死亡

即使在可達性分析法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize方法。當對象沒有覆蓋finalize方法,或finalize方法已經被虛擬機調用過時,虛擬機將這兩種情況視爲沒有必要執行。被判定爲需要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。

4.回收方法區

方法區(或Hotspot虛擬中的永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是 “無用的類” :

1.該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。

2.加載該類的ClassLoader已經被回收。

3.該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

垃圾收集算法

1 標記-清除算法
算法分爲“標記”和“清除”階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。它是最基礎的收集算法,會帶來兩個明顯的問題;1:效率問題和2:空間問題(標記清除後會產生大量不連續的碎片)

在這裏插入圖片描述

2 複製算法

爲了解決效率問題,“複製”收集算法出現了。它可以將內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象複製到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。

在這裏插入圖片描述

3 標記-整理算法

根據老年代的特點特出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一段移動,然後直接清理掉端邊界以外的內存。

在這裏插入圖片描述

4分代收集算法

當前虛擬機的垃圾手機都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不同將內存分爲幾塊。一般將java堆分爲新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。

比如在新生代中,每次收集都會有大量對象死去,所以可以選擇複製算法,只需要付出少量對象的複製成本就可以完成每次垃圾收集。而老年代的對象存活機率是比較高的所以我們可以選擇“標記-清理”或“標記-整理”算法進行垃圾收集。

HotSpot爲什麼要分爲新生代和老年代?

根據上面的對分代收集算法的介紹回答。

垃圾收集器

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它而非常符合在注重用戶體驗的應用上使用。

從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程分爲四個步驟:

初始標記: 暫停所有的其他線程,並記錄下直接與root相連的對象,速度很快 ;

併發標記: 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。

重新標記: 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短

併發清除: 開啓用戶線程,同時GC線程開始對爲標記的區域做清掃。

在這裏插入圖片描述

從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:併發收集、低停頓。但是它有下面三個明顯的缺點:

1.對CPU資源敏感;

2.無法處理浮動垃圾;

3.它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生

G1收集器

上一代的垃圾收集器(串行serial, 並行parallel, 以及CMS)都把堆內存劃分爲固定大小的三個部分: 年輕代(young generation), 年老代(old generation), 以及持久代(permanent generation).

在這裏插入圖片描述

G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特徵.

被視爲JDK1.7中HotSpot虛擬機的一個重要進化特徵。它具備一下特點:

並行與併發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行。

分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。

空間整合:與CMS的“標記–清理”算法不同,G1從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“複製”算法實現的。

可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內。

G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內可以儘可能高的收集效率(把內存化整爲零)。

G1收集器的運作大致分爲以下幾個步驟:

初始標記

併發標記

最終標記

篩選回收

面幾個步驟的運作過程和CMS有很多相似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS的值,讓下一個階段用戶程序併發運行時,能在正確可用的Region中創建新對象,這一階段需要停頓線程,但是耗時很短,併發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段時耗時較長,但可與用戶程序併發執行。而最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remenbered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set中,這一階段需要停頓線程,但是可並行執行。最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。

在這裏插入圖片描述

內存分配與回收策略

對象優先在Eden區分配

大多數情況下,對象在新生代中Eden區分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC.

Minor Gc和Full GC 有什麼不同呢?

新生代GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。

老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC的慢10倍以上。

大對象直接進入老年代

大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。

長期存活的對象將進入老年代

既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別那些對象應放在新生代,那些對象應放在老年代中。爲了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。

動態對象年齡判定

爲了更好的適應不同程序的內存情況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果Survivor 空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需達到要求的年齡。

JVM調優總結 很全面 值得一看
https://blog.csdn.net/luxianping/article/details/52135149

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章