JVM系列(四)
前言
前面介紹了Java內存運行時區域,其中程序計數器、虛擬機棧、本地方法棧三個區域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的,因此這幾個區域的內存分配和回收都具備確定性。在這幾個區域內不需要過多考慮回收的問題,因為方法結束或線程結束時,內存自然就跟隨著回收了。
Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣。我們只有在程序處于運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。
正文
(一). 對象生死判定
如何判斷Java中一個對象應該“存活”還是“死去”,這是 垃圾回收器要做的第一件事。
1. 引用計數算法
Java堆中每個具體對象(不是引用)都有一個引用計數器。當一個對象被創建并初始化賦值后,該變量計數設置為1。每當有一個地方引用它時,計數器值就加1。當引用失效時,即一個對象的某個引用超過了生命周期(出作用域后)或者被設置為一個新值時,計數器值就減1。任何引用計數為0的對象可以被當作垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數減1。
優點:
引用計數收集器執行簡單,判定效率高,交織在程序運行中。對程序不被長時間打斷的實時環境比較有利。
缺點:
難以檢測出對象之間的循環引用。同時,引用計數器增加了程序執行的開銷。所以Java語言并沒有選擇這種算法進行垃圾回收。
2. 可達性分析算法
可達性分析算法也叫根搜索算法,通過一系列的稱為GC Roots的對象作為起點,然后向下搜索。搜索所走過的路徑稱為引用鏈 (Reference Chain), 當一個對象到GC Roots沒有任何引用鏈相連時, 即該對象不可達,也就說明此對象是不可用的。
如下圖所示:Object5、Object6、Object7雖然互有關聯, 但它們到GC Roots是不可達的, 因此也會被判定為可回收的對象。
GC根對象
在Java中, 可作為GC Roots的對象包括以下四種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
本地方法棧中JNI(Native方法)引用的變量
方法區中類靜態屬性引用的變量
方法區中常量引用的變量
JVM中用到的所有現代GC算法在回收前都會先找出所有仍存活的對象。可達性分析算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖。下圖展示的JVM中的內存布局可以用來很好地闡釋這一概念:
(二). 對象引用分類
1. 強引用(Strong Reference)
在代碼中普遍存在的,類似Object obj = new Object()這類引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
2. 軟引用(Sofe Reference)
有用但并非必需的對象,可用SoftReference類來實現軟引用。在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
3. 弱引用(Weak Reference)
非必需的對象,但它的強度比軟引用更弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前,JDK提供了WeakReference類來實現弱引用。無論當前內存是否足夠,用軟引用相關聯的對象都會被回收掉。
4. 虛引用(Phantom Reference)
虛引用也稱為幽靈引用或幻影引用,是最弱的一種引用關系,JDK提供了PhantomReference類來實現虛引用。為一個對象設置虛引用的唯一目的是:能在這個對象在垃圾回收器回收時收到一個系統通知。
(三). finalize()二次標記
一個對象是否應該在垃圾回收器在GC時回收,至少要經歷兩次標記過程。
第一次標記過程
通過可達性分析算法分析對象是否與GC Roots可達。經過第一次標記,并且被篩選為不可達的對象會進行第二次標記。
第二次標記過程
判斷不可達對象是否有必要執行finalize方法。執行條件是當前對象的finalize方法被重寫,并且還未被系統調用過。如果允許執行那么這個對象將會被放到一個叫F-Query的隊列中,等待被執行。
注意:由于
finalize由一個優先級比較低的Finalizer線程運行,所以該對象的的finalize方法不一定被執行,即使被執行了,也不保證finalize方法一定會執行完。如果對象第二次小規模標記,即finalize方法中拯救自己,只需要重新和引用鏈上的任一對象建立關聯即可。
(四). 垃圾回收算法
本節具體介紹一下各種垃圾回收算法的思想:
1. 標記-清除算法
標記-清除算法對根集合進行掃描,對存活的對象進行標記。標記完成后,再對整個空間內未被標記的對象掃描,進行回收。
1.1. 標記階段
標記階段,通過根節點,標記所有從根節點開始的可達對象,未標記過的對象就是未被引用的垃圾對象。
1.2. 清除階段
清除階段,清除所有未被標記的對象。
優點:實現簡單,不需要進行對象進行移動。
缺點:標記、清除過程效率低,產生大量不連續的內存碎片,提高了垃圾回收的頻率。
2. 復制算法
這種收集算法解決了標記清除算法存在的效率問題。它將內存區域劃分成相同的兩個內存塊。每次僅使用一半的空間,JVM生成的新對象放在一半空間中。當一半空間用完時進行GC,把可到達對象復制到另一半空間,然后把使用過的內存空間一次清理掉。
優點:按順序分配內存即可,實現簡單、運行高效,不用考慮內存碎片。
缺點:可用的內存大小縮小為原來的一半,對象存活率高時會頻繁進行復制。
3. 標記-整理算法
標記-整理算法采用和標記-清除算法一樣的方式進行對象的標記,但后續不直接對可回收對象進行清理,而是將所有的存活對象往一端空閑空間移動,然后清理掉端邊界以外的內存空間。
優點:解決了標記-清理算法存在的內存碎片問題。
缺點:仍需要進行局部對象移動,一定程度上降低了效率。
4. 分代收集算法
當前商業虛擬機都采用分代收集的垃圾收集算法。分代收集算法,顧名思義是根據對象的存活周期將內存劃分為幾塊。一般包括年輕代、老年代和永久代,如圖所示:
新生代(Young generation)
絕大多數最新被創建的對象會被分配到這里,由于大部分對象在創建后會很快變得不可達,所以很多對象被創建在新生代,然后消失。對象從這個區域消失的過程我們稱之為minor GC。
新生代中存在一個Eden區和兩個Survivor區。新對象會首先分配在Eden中(如果新對象過大,會直接分配在老年代中)。在GC中,Eden中的對象會被移動到Survivor中,直至對象滿足一定的年紀(定義為熬過GC的次數),會被移動到老年代。
可以設置新生代和老年代的相對大小。這種方式的優點是新生代大小會隨著整個堆大小動態擴展。參數-XX:NewRatio設置老年代與新生代的比例。例如-XX:NewRatio=8指定老年代/新生代為8/1.老年代占堆大小的7/8,新生代占堆大小的1/8(默認即是1/8)。
例如:
1 |
-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8 |
老年代(Old generation)
對象沒有變得不可達,并且從新生代中存活下來,會被拷貝到這里。其所占用的空間要比新生代多。也正由于其相對較大的空間,發生在老年代上的GC要比新生代要少得多。對象從老年代中消失的過程,可以稱之為major GC(或者full GC)。
永久代(permanent generation)
像一些類的層級信息,方法數據和方法信息(如字節碼,棧和變量大小),運行時常量池(JDK7之后移出永久代),已確定的符號引用和虛方法表等等。它們幾乎都是靜態的并且很少被卸載和回收,在JDK8之前的HotSpot虛擬機中,類的這些永久的數據存放在一個叫做永久代的區域。
永久代是一段連續的內存空間,我們在JVM啟動之前可以通過設置-XX:MaxPermSize的值來控制永久代的大小。但是JDK8之后取消了永久代,這些元數據被移到了一個與堆不相連的稱為元空間(Metaspace) 的本地內存區域。
小結
JDK8堆內存一般是劃分為年輕代和老年代,不同年代根據自身特性采用不同的垃圾收集算法。
對于新生代,每次GC時都有大量的對象死亡,只有少量對象存活。考慮到復制成本低,適合采用復制算法。因此有了From Survivor和To Survivor區域。
對于老年代,因為對象存活率高,沒有額外的內存空間對它進行擔保。因而適合采用標記-清理算法和標記-整理算法進行回收。
參考
周志明,深入理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社
歡迎關注技術公眾號:零壹技術棧
零壹技術棧
本帳號將持續分享后端技術干貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分布式和微服務,架構學習和進階等學習資料和文章。
總結
- 上一篇: 使用postman删除Marketing
- 下一篇: 华为手机通讯录怎么导入苹果手机