垃圾收集器與記憶體分配策略(一)
參考:《深入理解Java虛擬機器》-jvm高階特性與最佳實現(周志明著)
前言
1、Java記憶體執行時區域的各個部分,其中程式計數器,虛擬機器棧,本地方法棧3個區域隨著執行緒而生,隨著執行緒而滅;在方法結束後,佔用的記憶體跟著就回收了,不需要過多考慮垃圾回收問題;
2、但是Java堆和方法區則不一樣,一個方法中多個分支需要的記憶體也不一樣,我們只有在程式執行期間才知道會建立哪些物件?這部分記憶體的分配都是動態的,垃圾收集器所關注的也是這部分記憶體。
一、判斷物件是否活著的依據
在Java堆中存放著Java世界裡幾乎所有的物件例項,垃圾收集器在對堆進行垃圾回收前,首先就要判斷哪些物件活著?哪些物件依據死去?
1、引用計數演算法:給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器的值就減1;任何時刻計數器的值為0的物件就是不可能在被使用的。
但是,主流的Java虛擬機器裡面都沒有選用引用計數演算法來管理記憶體,其最主要的原因就是因為它很難解決物件之間迴圈引用的問題
public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 這個成員變數唯一的意義就是佔用記憶體,以便在GC日誌中看清楚是否被回收過 * * VM 引數:-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:D:\app\file\java-log\gc-log.log * 輸出GC 詳細日誌 */ private byte[] bigSize = new byte[2*_1MB]; public static void testGC(){ ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; // 這時候,objA,objB的引用計數都為2 // 引用失效,計數器減1 objA = null; objB = null; // 假設這裡發生GC System.gc(); } public static void main(String[] args) { testGC(); } }
執行如上的程式碼main方法,檢視GC日誌
Java HotSpot(TM) 64-Bit Server VM (25.131-b11) for windows-amd64 JRE (1.8.0_131-b11), built on Mar 15 2017 01:23:53 by "java_re" with MS VC++ 10.0 (VS2010) Memory: 4k page, physical 8305264k(2089760k free), swap 10402416k(1249036k free) CommandLine flags: -XX:InitialHeapSize=132884224 -XX:MaxHeapSize=2126147584 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 2018-10-03T16:14:27.209+0800: 0.272: [GC (System.gc()) [PSYoungGen: 9459K->1330K(38400K)] 9459K->1338K(125952K), 0.0016711 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2018-10-03T16:14:27.211+0800: 0.273: [Full GC (System.gc()) [PSYoungGen: 1330K->0K(38400K)] [ParOldGen: 8K->1086K(87552K)] 1338K->1086K(125952K), [Metaspace: 3336K->3336K(1056768K)], 0.0094396 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Heap PSYoungGen total 38400K, used 333K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000) eden space 33280K, 1% used [0x00000000d5c00000,0x00000000d5c534a8,0x00000000d7c80000) from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000) to space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000) ParOldGen total 87552K, used 1086K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000) object space 87552K, 1% used [0x0000000081400000,0x000000008150f8a0,0x0000000086980000) Metaspace used 3344K, capacity 4568K, committed 4864K, reserved 1056768K class space used 360K, capacity 392K, committed 512K, reserved 1048576K
我們發現即使上述程式碼中的兩個物件相互引用,也發生了垃圾回收,這也從側面說明虛擬機器並不是通過引用計數演算法來管理記憶體的。
二、可達性分析演算法
在主流的商用語言的主流實現中,都是通過可達性分析來判斷物件是否存活的。這個演算法的基本思路就是通過一些列的稱為GC Roots 的物件作為起始節點,從這些物件向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots 沒有任何引用鏈的時候,則證明該物件是不可用的,它們將會被判定為可回收的物件
如上圖中的物件object5,object6,object7 雖然相互有關聯,但是他們到GC Roots不是可達的,所以他們會被判定為可以回收的物件
在Java語言中,可以作為GC Roots的物件的包括如下幾種
1、虛擬機器棧(棧中的本地變量表)中引用的物件
2、方法區中靜態類屬性引用的物件。
3、方法區中常量引用的物件。
4、本地方法棧中JNI(也就是本地方法)引用的物件
三、再談引用
判定一個物件是否存活都與引用有關。jdk1.2之後,Java對引用的概念進行了擴充,將引用分為強引用,軟引用,弱引用,虛引用4種,這四種引用強度以此減弱。
四、物件-生存還是死亡
1、要宣告一個物件死亡,至少要經歷兩次標記的過程:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記,並進行以此篩選。篩選的條件是此物件是否有必要執行finalize()方法,當物件沒有覆蓋finalize方法或者虛擬機器已經呼叫過該方法,那麼虛擬機器將這兩種情況都視為沒有必要執行。
如果這個物件唄判定為有必要執行finalize方法,那麼這個物件將被放進一個叫做F-Queue 的佇列中,並在稍後由一個虛擬機器自動建立的,低優先順序的finalizer執行緒去執行它。這裡的執行指的是虛擬機器會觸發這個方法,但是並不承諾會等待他娙結算書