java垃圾回收策论_深入理解 Java 虚拟机【3】垃圾收集策略与算法
作者:楊立濱
鏈接:https://github.com/yanglbme/jvm
程序計數器、虛擬機棧、本地方法棧隨線程而生,也隨線程而滅;棧幀隨著方法的開始而入棧,隨著方法的結束而出棧。這幾個區域的內存分配和回收都具有確定性,在這幾個區域內不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨著回收了。
而對于 Java 堆和方法區,我們只有在程序運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的正是這部分內存。
判定對象是否存活
若一個對象不被任何對象或變量引用,那么它就是無效對象,需要被回收。
引用計數法
在對象頭維護著一個 counter 計數器,對象被引用一次則計數器 +1;若引用失效則計數器 -1。當計數器為 0 時,就認為該對象無效了。
引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法。但是主流的 Java 虛擬機里沒有選用引用計數算法來管理內存,主要是因為它很難解決對象之間循環引用的問題。
舉個栗子👉對象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它們互相引用著對方,導致它們的引用計數都不為 0,于是引用計數算法無法通知 GC 收集器回收它們。
可達性分析法
所有和 GC Roots 直接或間接關聯的對象都是有效對象,和 GC Roots 沒有關聯的對象就是無效對象。
GC Roots 是指:
Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
本地方法棧中引用的對象
方法區中常量引用的對象
方法區中類靜態屬性引用的對象
GC Roots 并不包括堆中對象所引用的對象,這樣就不會有循環引用的問題。
引用的種類
判定對象是否存活與“引用”有關。在 JDK 1.2 以前,Java 中的引用定義很傳統,一個對象只有被引用或者沒有被引用兩種狀態,我們希望能描述這一類對象:當內存空間還足夠時,則保留在內存中;如果內存空間在進行垃圾手收集后還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。
在 JDK 1.2 之后,Java 對引用的概念進行了擴充,將引用分為了以下四種:
強引用(Strong Reference)
類似 "Object obj = new Object()" 這類的引用,就是強引用,只要強引用存在,垃圾收集器永遠不會回收被引用的對象。
軟引用(Soft Reference)
軟引用是用來描述一些有用但并非必需的對象,j就是說,內存足夠時留著它們,內存即將發生溢出時把這些對象列入回收范圍進行回收。若回收過后還沒有足夠的內存,才拋出內存溢出異常。
弱引用(Weak Reference)
弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些。當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被軟引用關聯的對象。
虛引用(Phantom Reference)
虛引用也稱幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
回收堆中無效對象
對于可達性分析中不可達的對象,也并不是沒有存活的可能。
判定 finalize() 是否有必要執行
JVM 會判斷此對象是否有必要執行 finalize() 方法,如果對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過,那么視為“沒有必要執行”。那么對象基本上就真的被回收了。
如果對象被判定為有必要執行 finalize() 方法,那么對象會被放入一個 F-Queue 隊列中,虛擬機會以較低的優先級執行這些 finalize()方法,但不會確保所有的 finalize() 方法都會執行結束。如果 finalize() 方法出現耗時操作,虛擬機就直接停止指向該方法,將對象清除。
對象重生或死亡
如果在執行 finalize() 方法時,將 this 賦給了某一個引用,那么該對象就重生了。如果沒有,那么就會被垃圾收集器清除。
任何一個對象的 finalize() 方法只會被系統自動調用一次,如果對象面臨下一次回收,它的 finalize() 方法不會被再次執行,想繼續在 finalize() 中自救就失效了。
回收方法區內存
方法區中存放生命周期較長的類信息、常量、靜態變量,每次垃圾收集只有少量的垃圾被清除。方法區中主要清除兩種垃圾:
廢棄常量
無用的類
判定廢棄常量
只要常量池中的常量不被任何變量或對象引用,那么這些常量就會被清除掉。比如,一個字符串 "bingo" 進入了常量池,但是當前系統沒有任何一個 String 對象引用常量池中的 "bingo" 常量,也沒有其它地方引用這個字面量,必要的話,"bingo"常量會被清理出常量池。
判定無用的類
判定一個類是否是“無用的類”,條件較為苛刻:
該類的所有對象都已經被清除
加載該類的 ClassLoader 已經被回收
該類的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
一個類被虛擬機加載進方法區,那么在堆中就會有一個代表該類的對象:java.lang.Class。這個對象在類被加載進方法區時創建,在方法區該類被刪除時清除。
垃圾收集算法
學會了如何判定無效對象、無用類、廢棄常量之后,剩余工作就是回收這些垃圾。常見的垃圾收集算法有以下幾個:
標記-清除算法
判斷哪些數據需要清除,并對它們進行標記,然后清除被標記的數據。
這種方法有兩個不足:
效率問題:標記和清除兩個過程的效率都不高。
空間問題:標記清除之后會產生大量不連續的內存碎片,碎片太多可能導致以后需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
復制算法(新生代)
為了解決效率問題,“復制”收集算法出現了。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,需要進行垃圾收集時,就將存活者的對象復制到另一塊上面,然后將第一塊內存全部清除。這種算法有優有劣:
優點:不會有內存碎片的問題。
缺點:內存縮小為原來的一半,浪費空間。
為了解決空間利用率問題,可以將內存分為三塊: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一塊 Survivor。回收時,將 Eden 和 Survivor 中還存活的對象一次性復制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才使用的 Survivor 空間。這樣只有 10% 的內存被浪費。
但是我們無法保證每次回收都只有不多于 10% 的對象存活,當 Survivor 空間不夠,需要依賴其他內存(指老年代)進行分配擔保。
分配擔保
為對象分配內存空間時,如果 Eden+Survivor 中空閑區域無法裝下該對象,會觸發 MinorGC 進行垃圾收集。但如果 Minor GC 過后依然有超過 10% 的對象存活,這樣存活的對象直接通過分配擔保機制進入老年代,然后再將新對象存入 Eden 區。
標記-整理算法(老年代)
在回收垃圾前,首先將廢棄對象做上標記,然后將未標記的對象移到一邊,最后清空另一邊區域即可。
這是一種老年代的垃圾收集算法。老年代的對象一般壽命比較長,因此每次垃圾回收會有大量對象存活,如果采用復制算法,每次需要復制大量存活的對象,效率很低。
分代收集算法
根據對象存活周期的不同,將內存劃分為幾塊。一般是把 Java 堆分為新生代和老年代,針對各個年代的特點采用最適當的收集算法。
新生代:復制算法
老年代:標記-清除算法、標記-整理算法
(完)
推薦大而全的【后端技術精選】
始發于微信公眾號: Java知音
總結
以上是生活随笔為你收集整理的java垃圾回收策论_深入理解 Java 虚拟机【3】垃圾收集策略与算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: openfire java集群_优化op
- 下一篇: java命令主动清空jvm_JVM史上最