《深入理解Java虛擬機》學習筆記(第三章 垃圾收集器與內存分配策略)
第三章 垃圾收集器與內存分配策略
要解決的問題
- 哪些內存需要回收?
- 什麽時候回收?
- 如何回收?
概述
當需要排查各種內存溢出、內存泄漏問題時,當垃圾收集成為系統達到更高並發量的瓶頸時,
需要對內存動態分配和內存回收技術進行必要的監控和調節。程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;
棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作,每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知,
因此這幾個區域的內存分配和回收都具備確定性,因為方法結束或者線程結束時,內存自然就跟著回收。垃圾收集器主要關註的是Java堆和方法區,因為一個接口中的多個實現類需要的內存可能不一樣,
一個方法中的多個分支需要的內存也可能不一樣,只有在程序處於運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的。
對象已死嗎?
- 引用計數算法
- 給對象中添加一個引用計數器,每當有一個地方引用他時,計數器值就+1;
當引用失效時,計數器值就-1;任何時刻計數器為0的對象就是不可能再被使用的。 - 優點:實現簡單,判定效率高。
- 缺點:很難解決對象之間相互循環引用的問題。
- 可達性算法分析
通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,
當一個對象到GC Roots沒有任何引用鏈相連時(GC Roots到這個對象不可達),則證明此對象是不可用的。- Java中,可作為GC Roots的對象包括:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(即Native方法)引用的對象
- Java中,可作為GC Roots的對象包括:
再談引用
* 從上到下強度依次減弱 - 強引用:類似Object obj = new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。 - 軟引用:描述一些還有用但並非必需的對象。對於軟引用關聯著的對象,在系統將要發生內存溢出異常之前,會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常 - 弱引用:被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。 - 虛引用: 幽靈引用/幻影引用。一個對象是否有虛引用不會對其生存時間構成影響。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
finalize()方法
- 如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,會被第一次標記並進行一次篩選,篩選的條件是次對象是否有必要執行finalize()方法。
當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。
- 如果這個對象被判定為有必要執行finalize()方法,那麽這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它,這裏的“執行”指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。
- finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己,只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫基本上就真的被回收了。
- 任何一個對象的finalize()方法都只會被系統自動調用一次。
- 它的運行代價高昂,不確定性個大,無法保證各個對象的調用順序,不建議使用。
ps:java finalize方法總結、GC執行finalize的過程
### 回收方法區
- 在堆中,尤其是新生代中,常規應用進行一次垃圾收集一般可以回收70%-95%的空間,而永久代的垃圾收集效率遠低於此。
- 永久代的垃圾收集主要回收:廢棄常量和無用的類。
* 廢棄常量:沒有任何對象,沒有任何地方引用的常量池中的常量。
* 無用的類:同時滿足以下三個條件:
該類的所有實例都已經被回收,即Java堆中不存在該類的任何實例;
加載該類的ClassLoader已經被回收;
該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
垃圾收集算法
### 標記-清除算法(Mark-Sweep)
首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。
- 缺點:效率問題;空間問題:標記清除後會產生大量不連續的內存碎片。
### 復制算法
將可用內存劃分為大小相等的兩塊,每次只使用其中的一塊。當這塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後把已使用的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。但內存變為原來一般,代價太大。
- 現在的商業虛擬機都采用這種收集算法來回收新生代。Eden+Survivor+Sirvivor,8:1:1。
### 標記-整理算法
老年代,標記過程同標記清除算法,但後續讓所有存活的對象都想一端移動,然後清理掉端邊界以外的內存。
### 分代收集算法
根據對象存活周期的不同將內存劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,采用復制算法。老年代中對象存活率高,沒有額外空間進行分配擔保,則“標記清理”或者“標記整理”。
HotSpot算法實現
枚舉根節點
可作為GC Roots的節點主要在全局性的引用與執行上下文中。
可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行——這裏“一致性”的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點,不可以出現分析過程中對象引用關系還在不斷變化的情況。這點是導致GC進行時必須停頓所有Java執行線程(Stop The World)的一個重要原因。
虛擬機通過一組稱為OopMap的數據結構來直接得知哪些地方存在著對象引用。在類加載完成的時候,HotSpot就把對象內什麽偏移量上是什麽類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。(OopMap是一個附加信息,告訴你棧上哪個位置本來是什麽東西)。
安全點
在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots枚舉。
HotSpot在特定的位置即安全點記錄OopMap,即程序執行時只有在打到安全點才能暫停。安全點的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以致於過分增大運行時的負荷。
安全點的選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準選定——因為每條指令執行的時間都非常短暫,程序不太可能因為指令流長度太長而過長時間運行,“長時間執行”最明顯的特征就是指令序列復用:例如方法調用、循環跳轉、異常跳轉等。所以具有這些功能的指令才會產生安全點。
如何在GC發生時讓所有線程都“跑”到最近的安全點上再停頓下來?
搶先式中斷:GC發生時,首先所有線程中斷,恢復中斷的地方不在安全的線程,讓其“跑”到安全點。幾乎不使用。
主動式中斷:當GC需要中斷線程時,設置一個標誌,各個線程執行時主動輪詢這個標誌,發現中斷標誌為真時就自己中斷 掛起,輪詢標誌的地方和安全點是重合的。
安全區域
在一段代碼片段之中,引用關系不會發生變化。在這個區域中的任意地方開始GC都是安全的。可以看成是擴展的安全點。
在線程執行到安全區域的中的代碼時,首先標識自己已經進入了安全區域,當在這段時間JVM要發起GC時,就不用管標識自己為安全區域狀態的線程了。在線程要離開安全區域時,要檢查系統是否已經完成了GC過程,完成則繼續執行,否則必須等待直到收到可以安全離開安全區域的信號為止。
Java系列:JVM中的OopMap(zz)
來自 <https://www.cnblogs.com/strinkbug/p/6376525.html?utm_source=itdadao&utm_medium=referral>
JVM源碼分析之安全點safepoint
來自 <https://blog.csdn.net/fishmai/article/details/71056991>
垃圾收集器
Serial收集器:最基本,最悠久,單線程收集器。虛擬機運行在Client模式下的默認新生代收集器。
ParNew收集器:Serial收集器的多線程版本。許多運行在Server模式下的虛擬機中首選的新生代收集器。
Parallel Scavenge收集器:新生代,復制算法,吞吐量優先。
吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
CMS等收集器的關註點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而此收集器的目標則是達到一個可控制的吞吐量。
GC停頓時間的縮短是以犧牲吞吐量和新生代空間換來的。
UseAdaptiveSizePolicy:GC自適應調節策略開關。虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。
Serial Old收集器:Serial收集器的老年代版本
Parallel Old收集器:Parallel Scavenge收集器的老年代版本。在註重吞吐量以及CPU資源敏感的場合,可以考慮這兩者的組合。
CMS收集器:以獲取最短回收停頓時間為目標。
運行過程:
初始標記:STW,標記GC Roots能直接關聯到的對象。。
並發標記:GC Roots Tracing。
重新標記:STW,修正並發標記期間因用戶程序繼續運行而標記產生變動的那一部分對象的標記記錄。
並發清除
缺點:
對CPU資源敏感。占用了一部分線程而導致應用程序變慢,總吞吐量降低。
無法處理浮動垃圾。
空間碎片的產生。
G1收集器:整個Java堆劃分為多個大小相等的獨立區域。G1跟蹤各個Region裏面的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
用Remembered Set來避免全堆掃描。G1中的每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的對象是否處於不同的Region之中,如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。
運行過程:
初始標記:
並發標記:
最終標記:
篩選回收:
內存分配與回收策略
對象優先在Eden分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
新生代GC(Minor GC)
老年代GC(Major GC/Full GC)
大對象直接進入老年代。
長期存活的對象將進入老年代。
如果對象在Eden出生並經過第一次Minor GC後仍然存活並且能被Survivor容納的話,將被移動到Survivor空間中,對象年齡設為1。對象在Survivor區中每熬過一次Minor GC,年齡+1,增加到一定程度就可以晉升到老年代中。
動態對象年齡判定。
如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
空間分配擔保。
JDK6 Update24之後規定只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。若擔保失敗,發起Full GC。
【Java虛擬機學習筆記】《深入理解Java虛擬機》之第三章 - 垃圾收集器與內存分配策略
來自 https://blog.csdn.net/SnailMann/article/details/80912737
《深入理解Java虛擬機》學習筆記(第三章 垃圾收集器與內存分配策略)