歡迎大家關注我的微信公眾號【老周聊架構】,Java后端主流技術堆疊的原理、原始碼分析、架構以及各種互聯網高并發、高性能、高可用的解決方案,
如果你是 Java 中高、高級程式員,那我相信你一定被面試官問過 JVM,下次再被問到 JVM,你直接把老周的這篇文章丟給他吧!話不多說,讓我們直接進入主題吧,
1、Java 類加載程序
Java 類加載需要經歷以下 7 個程序:

1.1 加載
加載是類加載的第一個程序,在這個階段,將完成一下三件事情:
- 通過一個類的全限定名獲取該類的二進制流,
- 將該二進制流中的靜態存盤結構轉化為方法去運行時資料結構,
- 在記憶體中生成該類的 Class 物件,作為該類的資料訪問入口,
1.2 驗證
驗證的目的是為了確保 Class 檔案的位元組流中的資訊不回危害到虛擬機,
在該階段主要完成以下四鐘驗證:
- 檔案格式驗證:驗證位元組流是否符合 Class 檔案的規范,如主次版本號是否在當前虛擬機范圍內,常量池中的常量是否有不被支持的型別,
- 元資料驗證:對位元組碼描述的資訊進行語意分析,如這個類是否有父類,是否集成了不被繼承的類等,
- 位元組碼驗證:是整個驗證程序中最復雜的一個階段,通過驗證資料流和控制流的分析,確定程式語意是否正確,主要針對方法體的驗證,如:方法中的型別轉換是否正確,跳轉指令是否正確等,
- 符號參考驗證:這個動作在后面的決議程序中發生,主要是為了確保決議動作能正確執行,
1.3 準備
準備階段是為類的靜態變數分配記憶體并將其初始化為默認值,這些記憶體都將在方法區中進行分配,準備階段不分配類中的實體變數的記憶體,實體變數將會在物件實體化時隨著物件一起分配在 Java 堆中,
public static String value = "公眾號【老周聊架構】"; // 在準備階段 value 初始值為 null ,在初始化階段才會變為 "公眾號【老周聊架構】" ,
1.4 決議
該階段主要完成符號參考到直接參考的轉換動作,決議動作并不一定在初始化動作完成之前,也有可能在初始化之后,
1.5 初始化
初始化是類加載的最后一步,前面的類加載程序,除了在加載階段用戶應用程式可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制,到了初始化階段,才真正開始執行類中定義的 Java 程式代碼,
1.5.1 類構造器
初始化階段是執行類構造器<client>方法的程序,<client>方法是由編譯器自動收集類中的類變數的賦值操作和靜態陳述句塊中的陳述句合并而成的,虛擬機會保證子<client>方法執行之前,父類的<client>方法已經執行完畢,如果一個類中沒有對靜態變數賦值也沒有靜態陳述句塊,那么編譯器可以不為這個類生成<client>()方法,
注意以下幾種情況不會執行類初始化:
- 通過子類參考父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化,
- 定義物件陣列,不會觸發該類的初始化,
- 常量在編譯期間會存入呼叫類的常量池中,本質上并沒有直接參考定義常量的類,不會觸
發定義常量所在的類, - 通過類名獲取 Class 物件,不會觸發類的初始化,
- 通過 Class.forName 加載指定類時,如果指定引數 initialize 為 false 時,也不會觸發類初
始化,其實這個引數是告訴虛擬機,是否要對類進行初始化, - 通過 ClassLoader 默認的 loadClass 方法,也不會觸發初始化動作,
1.6 使用
1.7 卸載
2、描述一下 JVM 加載 Class 檔案的原理機制
Java 語言是一種具有動態性的解釋型語言,類(Class)只有被 載到 JVM 后才能運行,當運行指定程式時,JVM 會將編譯生成 的 .class 檔案按照需求和一定的規則加載到記憶體中,并組織成為一個完整的 Java 應用程式,這個加載程序是由類加載器完成,具體來說,就是由 ClassLoader 和它的子類來實作的,類加載器本身也是一個類,其實就是把類檔案從硬碟讀取到記憶體中,
類的加載方式分為隱式加載和顯示加載,隱式加載指的是程式在使 用 new 等方式創建物件時,會隱式地呼叫類的加載器把對應的類加載到 JVM 中,顯示加載指的是通過直接呼叫 class.forName() 方法來把所需的類加載到 JVM 中,
任何一個工程專案都是由許多類組成的,當程式啟動時,只把需要的類加載到 JVM 中,其他類只有被使用到的時候才會被加載,采用這種方法一方面可以加快加載速度,另一方面可以節約程式運行時對記憶體的開銷,此外,在 Java 語言中,每個類或介面都對應一個 .class 檔案,這些檔案可以被看成是一個個可以被動態加載的單元,因此當只有部分類被修改時,只需要重新編譯變化的類即可, 而不需要重新編譯所有檔案,因此加快了編譯速度,
在 Java 語言中,類的加載是動態的,它并不會一次性將所有類全部加載后再運行,而是保證程式運行的基礎類(例如基類)完全加載到 JVM 中,至于其他類,則在需要的時候才加載,
類加載的主要步驟:
-
裝載,根據查找路徑找到相應的 class 檔案,然后匯入,
-
鏈接,鏈接又可分為 3 個小步:
檢查,檢查待加載的 class 檔案的正確性,
準備,給類中的靜態變數分配存盤空間,
決議,將符號參考轉換為直接參考(這一步可選), -
初始化,對靜態變數和靜態代碼塊執行初始化作業,
3、什么是類加載器,類加載器有哪些?
實作通過類的全限定名獲取該類的二進制位元組流的代碼塊叫做類加載器,
主要有一下四種類加載器:

- 啟動類加載器(Bootstrap ClassLoader):用來加載 Java 核心類別庫,無法被 Java 程式直接參考,
- 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫,Java 虛擬機的實作會提供一個擴展庫目錄,該類加載器在此目錄里面查找并加載 Java 類,
- 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類,一般來說,Java 應用的類都是由它來完成加載的,可以通過 ClassLoader.getSystemClassLoader() 來獲取它,
- 用戶自定義類加載器,通過繼承 java.lang.ClassLoader 類的方式實作,
3.1 雙親委派機制
當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父
類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應該傳送到啟動類加載器中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的 Class),子類加載器才會嘗試自己去加載,
采用雙親委派的一個好處是比如加載位于 rt.jar 包中的類 java.lang.Object,不管是哪個加載
器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載,

4、談談你對JVM的理解
JVM 是可運行 Java 代碼的假想計算機 ,包括一套位元組碼指令集、一組暫存器、一個堆疊、 一個垃圾回收,堆和一個存盤方法域,JVM 是運行在作業系統之上的,它與硬體沒有直接的互動,

5、JVM 記憶體模型
JVM 記憶體區域主要分為:
- 執行緒共享區域:【方法區、JAVA 堆】、直接記憶體,
- 執行緒私有區域:【程式計數器、虛擬機堆疊、本地方法區】
執行緒共享區域隨虛擬機的啟動/關閉而創建/銷毀,
執行緒私有資料區域生命周期與執行緒相同,依賴用戶執行緒的啟動/結束而創建/銷毀(在 Hotspot VM 內,每個執行緒都與作業系統的本地執行緒直接映射,因此這部分記憶體區域的存活跟隨本地執行緒的生死對應),
直接記憶體并不是 JVM 運行時資料區的一部分,但也會被頻繁的使用,在 JDK 1.4 引入的 NIO 提供了基于 Channel 與 Buffer 的 IO 方式,它可以使用 Native 函式庫直接分配堆外記憶體,然后使用 DirectByteBuffer 物件作為這塊記憶體的參考進行操作,這樣就避免了在 Java 堆和 Native 堆中來回復制資料,因此在一些場景中可以顯著提高性能,

5.1 程式計數器(執行緒私有)
一塊較小的記憶體空間,是當前執行緒所執行的位元組碼的行號指示器,每條執行緒都要有一個獨立的程式計數器,這類記憶體也稱為“執行緒私有”的記憶體,
正在執行 java 方法的話,計數器記錄的是虛擬機位元組碼指令的地址(當前指令的地址),如果還是 Native 方法,則為空,
這個記憶體區域是唯一一個在虛擬機中沒有規定任何 OutOfMemoryError 情況的區域,
5.2 虛擬機堆疊(執行緒私有)
是描述 java 方法執行的記憶體模型,每個方法在執行的同時都會創建一個堆疊幀(Stack Frame)用于存盤區域變數表、運算元堆疊、動態鏈接、方法出口等資訊,每一個方法從呼叫直至執行完成的程序,就對應著一個堆疊幀在虛擬機堆疊中入堆疊到出堆疊的程序,
堆疊幀(Frame)是用來存盤資料和部分程序結果的資料結構,同時也被用來處理動態鏈接 (Dynamic Linking)、 方法回傳值和例外分派(Dispatch Exception),堆疊幀隨著方法呼叫而創建,隨著方法結束而銷毀——無論方法是正常完成還是例外完成(拋出了在方法內未被捕獲的例外)都算作方法結束,
5.3 本地方法區(執行緒私有)
本地方法區和 Java Stack 作用類似,區別是虛擬機堆疊為執行 Java 方法服務,而本地方法堆疊則為 Native 方法服務,如果一個 VM 實作使用 C-linkage 模型來支持 Native 呼叫,那么該堆疊將會是一個 C 堆疊,但 HotSpot VM 直接就把本地方法堆疊和虛擬機堆疊合二為一,
5.4 堆(Heap)-運行時資料區(執行緒共享)
是被執行緒共享的一塊記憶體區域,創建的物件和陣列都保存在 Java 堆記憶體中,也是垃圾收集器進行垃圾收集的最重要的記憶體區域,由于現代 VM 采用分代收集演算法,因此 Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代,
5.5 方法區/永久代(執行緒共享)
即我們常說的永久代(Permanent Generation),用于存盤被 JVM 加載的類資訊、常量、靜態變數、即時編譯器編譯后的代碼等資料,HotSpot VM 把 GC 分代收集擴展至方法區,即使用 Java 堆的永久代來實作方法區,這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分記憶體,而不必為方法區開發專門的記憶體管理器(永久代的記憶體回收的主要目標是針對常量池的回收和型別的卸載,因此收益一般很小),
運行時常量池(Runtime Constant Pool)是方法區的一部分,Class 檔案中除了有類的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號參考,這部分內容將在類加載后存放到方法區的運行時常量池中,Java 虛擬機對 Class 檔案的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個位元組用于存盤哪種資料都必須符合規范上的要求,這樣才會被虛擬機認可、裝載和執行,
這里提一下 JDK 8 永久代被元空間(Metaspace)替換,
6、JVM 運行時記憶體
Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、Survivor From 區和 Survivor To 區)和老年代,

6.1 新生代
是用來存放新生的物件,一般占據堆的 1/3 空間,由于頻繁創建物件,所以新生代會頻繁觸發 MinorGC 進行垃圾回收,新生代又分為 Eden 區、Survivor From、Survivor To 三個區,
6.1.1 Eden 區
Java 新物件的出生地(如果新創建的物件占用記憶體很大,則直接分配到老年代),當 Eden 區記憶體不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收,
6.1.2 Survivor From
上一次 GC 的幸存者,作為這一次 GC 的被掃描者,
6.1.3 Survivor To
保留了一次 MinorGC 程序中的幸存者,
6.1.4 MinorGC 的程序(復制->清空->互換)
- eden、Survivor From 復制到 Survivor To,年齡+1
首先,把 Eden 和 SurvivorFrom 區域中存活的物件復制到 Survivor To 區域(如果有物件的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些物件的年齡+1(如果 Survivor To 不夠位置了就放到老年區); - 清空 eden、Survivor From
然后,清空 Eden 和 Survivor From 中的物件; - Survivor To 和 Survivor From 互換
最后,Survivor To 和 Survivor From 互換,原 Survivor To 成為下一次 GC 時的 Survivor From
區,
6.2 老年代
主要存放應用程式中生命周期長的記憶體物件,
老年代的物件比較穩定,所以 MajorGC 不會頻繁執行,在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的物件晉身入老年代,導致空間不夠用時才觸發,當無法找到足夠大的連續空間分配給新創建的較大物件時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間,
MajorGC 采用標記清除演算法:首先掃描一次所有老年代,標記出存活的物件,然后回收沒有標記的物件,MajorGC 的耗時比較長,因為要掃描再回收,MajorGC 會產生記憶體碎片,為了減少記憶體損耗,我們一般需要進行合并或者標記出來方便下次直接分配,當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)例外,
6.3 永久代
指記憶體的永久保存區域,主要存放 Class 和 Meta(元資料)的資訊,Class 在被加載的時候被放入永久區域,它和存放實體的區域不同,GC 不會在主程式運行期對永久區域進行清理,所以這也導致了永久代的區域會隨著加載的 Class 的增多而脹滿,最終拋出 OOM 例外,
6.3.1 JDK 8 與元資料
在 JDK 8 中,永久代已經被移除,被一個稱為“元資料區”(元空間)的區域所取代,元空間的本質和永久代類似,元空間與永久代之間最大的區別在:元空間并不在虛擬機中,而是使用本地記憶體,因此,默認情況下,元空間的大小僅受本地記憶體限制,類的元資料放入 native memory,字串池和類的靜態變數放入 java 堆中,這樣可以加載多少類的元資料就不再由 MaxPermSize 控制,而由系統的實際可用空間來控制,
這里老周要提兩點注意的地方:
- 如果你們的應用是 JDK 8 以上的話,PermSize 以及 MaxPermSize 引數是不生效的,要改成 MetaspaceSize 以及 MaxMetaspaceSize,
- 應用是 JDK 8 以上的話,MetaspaceSize 以及 MaxMetaspaceSize 一定要設定,因為不設定的話,32 位的 JVM MetaspaceSize 以及 MaxMetaspaceSize 默認是 16M、64M,64 位的 JVM MetaspaceSize 以及 MaxMetaspaceSize 默認是 21M、82M,因為老周線上遇到過沒有設定而 JVM 采用的默認值,導致專案部署階段多次 FullGC 的問題,
7、垃圾回收與演算法
7.1 如何確定垃圾
7.1.1 參考計數法
在 Java 中,參考和物件是有關聯的,如果要操作物件則必須用參考進行,因此,很顯然一個簡單 的辦法是通過參考計數來判斷一個物件是否可以回收,簡單說,即一個物件如果沒有任何與之關聯的參考,即他們的參考計數都不為 0,則說明物件不太可能再被用到,那么這個物件就是可回收物件,
7.1.2 可達性分析
為了解決參考計數法的回圈參考問題,Java 使用了可達性分析的方法,通過一系列的“GC roots”物件作為起點搜索,如果在“GC roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的,要注意的是,不可達物件不等價于可回收物件,不可達物件變為可回收物件至少要經過兩次標記程序,兩次標記后仍然是可回收物件,則將面臨回收,
7.2 垃圾回收演算法
7.2.1 標記清除演算法(Mark-Sweep)
最基礎的垃圾回收演算法,分為兩個階段,標記和清除,標記階段標記出所有需要回收的物件,清除階段回收被標記的物件所占用的空間,

從圖中我們就可以發現,該演算法最大的問題是記憶體碎片化嚴重,后續可能發生大物件不能找到可利用空間的問題,
7.2.2 復制演算法(copying)
為了解決 Mark-Sweep 演算法記憶體碎片化的缺陷而被提出的演算法,按記憶體容量將記憶體劃分為等大小的兩塊,每次只使用其中一塊,當這一塊記憶體滿后將尚存活的物件復制到另一塊上去,把已使用的記憶體清掉,

這種演算法雖然實作簡單,記憶體效率高,不易產生碎片,但是最大的問題是可用記憶體被壓縮到了原本的一半,且存活物件增多的話,Copying 演算法的效率會大大降低,
7.2.3 標記整理演算法(Mark-Compact)
結合了以上兩個演算法,為了避免缺陷而提出,標記階段和 Mark-Sweep 演算法相同,標記后不是清理物件,而是將存活物件移向記憶體的一端,然后清除端邊界外的物件,

7.2.4 分代收集演算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根據物件存活的不同生命周期將記憶體劃分為不同的域,一般情況下將 GC 堆劃分為老年代(Tenured/Old Generation)和新生代(Young Generation),老年代的特點是每次垃圾回收時只有少量物件需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的演算法,
7.2.4.1 新生代與復制演算法
目前大部分 JVM 的 GC 對于新生代都采取 Copying 演算法,因為新生代中每次垃圾回收都要 回收大部分物件,即要復制的操作比較少,但通常并不是按照 1:1 來劃分新生代,一般將新生代劃分為一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用 Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的物件復制到另一塊 Survivor 空間中,

7.2.4.2 老年代與標記整理演算法
而老年代因為每次只回收少量物件,因而采用 Mark-Compact 演算法,
- JAVA 虛擬機提到過的處于方法區的永久代(Permanet Generation),它用來存盤 class 類,
常量,方法描述等,對永久代的回收主要包括廢棄常量和無用的類, - 物件的記憶體分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目
前存放物件的那一塊),少數情況會直接分配到老年代, - 當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 后,Eden Space 和 From Space 區的存活物件會被挪到 To Space,然后將 Eden Space 和 From Space 進行清理,
- 如果 To Space 無法足夠存盤某個物件,則將這個物件存盤到老生代,
- 在進行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反復回圈,
- 當物件在 Survivor 區躲過一次 GC 后,其年齡就會+1,默認情況下年齡到達 15 的物件會被移到老年代中,
8、JAVA 四中參考型別
8.1 強參考
在 Java 中最常見的就是強參考,把一個物件賦給一個參考變數,這個參考變數就是一個強參考,當一個物件被強參考變數參考時,它處于可達狀態,它是不可能被垃圾回識訓制回收的,即使該物件以后永遠都不會被用到 JVM 也不會回收,因此強參考是造成 Java 記憶體泄漏的主要原因之一,
8.2 軟參考
軟參考需要用 SoftReference 類來實作,對于只有軟參考的物件來說,當系統記憶體足夠時它
不會被回收,當系統記憶體空間不足時它會被回收,軟參考通常用在對記憶體敏感的程式中,
8.3 弱參考
弱參考需要用 WeakReference 類來實作,它比軟參考的生存期更短,對于只有弱參考的物件來說,只要垃圾回識訓制一運行,不管 JVM 的記憶體空間是否足夠,總會回收該物件占用的記憶體,
8.4 虛參考
虛參考需要 PhantomReference 類來實作,它不能單獨使用,必須和參考佇列聯合使用,虛 參考的主要作用是跟蹤物件被垃圾回收的狀態,
9、GC 垃圾收集器
Java 堆記憶體被劃分為新生代和老年代兩部分,新生代主要使用復制和標記-清除垃圾回收,
老年代主要使用標記-整理垃圾回收演算法,因此 Java 虛擬中針對新生代和年老代分別提供了多種不
同的垃圾收集器,Sun HotSpot 虛擬機的垃圾收集器如下:

9.1 Serial 垃圾收集器(單執行緒、復制演算法)
Serial(連續)是最基本垃圾收集器,使用復制演算法,曾經是 JDK1.3.1 之前新生代唯一的垃圾收集器,Serial 是一個單執行緒的收集器,它不但只會使用一個 CPU 或一條執行緒去完成垃圾收集作業,并且在進行垃圾收集的同時,必須暫停其他所有的作業執行緒,直到垃圾收集結束,
Serial 垃圾收集器雖然在收集垃圾程序中需要暫停所有其他的作業執行緒,但是它簡單高效,對于限定單個 CPU 環境來說,沒有執行緒互動的開銷,可以獲得最高的單執行緒垃圾收集效率,因此 Serial 垃圾收集器依然是 Java 虛擬機運行在 Client 模式下默認的新生代垃圾收集器,
9.2 ParNew 垃圾收集器(Serial+多執行緒)
ParNew(平行的) 垃圾收集器其實是 Serial 收集器的多執行緒版本,也使用復制演算法,除了使用多執行緒進行垃圾收集之外,其余的行為和 Serial 收集器完全一樣,ParNew 垃圾收集器在垃圾收集程序中同樣也要暫停所有其他的作業執行緒,
ParNew 收集器默認開啟和 CPU 數目相同的執行緒數,可以通過 -XX:ParallelGCThreads 引數來限制垃圾收集器的執行緒數,
ParNew 雖然是除了多執行緒外和 Serial 收集器幾乎完全一樣,但是 ParNew 垃圾收集器是很多 Java 虛擬機運行在 Server 模式下新生代的默認垃圾收集器,
9.3 Parallel Scavenge 收集器(多執行緒復制演算法、高效)
Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用復制演算法,也是一個多執行緒的垃圾收集器,它重點關注的是程式達到一個可控制的吞吐量(Thoughput,CPU 用于運行用戶代碼的時間/CPU 總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)), 高吞吐量可以最高效率地利用 CPU 時間,盡快地完成程式的運算任務,主要適用于在后臺運算而不需要太多互動的任務,自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個重要區別,
9.4 Serial Old收集器(單執行緒標記整理演算法)
Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單執行緒的收集器,使用標記-整理演算法, 這個收集器也主要是運行在 Client 默認的 Java 虛擬機默認的年老代垃圾收集器,
在 Server 模式下,主要有兩個用途:
- 在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用,
- 作為年老代中使用 CMS 收集器的后備垃圾收集方案,
9.5 Parallel Old收集器(多執行緒標記整理演算法)
Parallel Old 收集器是 Parallel Scavenge 的年老代版本,使用多執行緒的標記-整理演算法,在 JDK1.6 才開始提供,
在 JDK1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old 正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略,
9.6 CMS收集器(多執行緒標記清除演算法)
Concurrent Mark Sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間,和其他年老代使用標記-整理演算法不同,它使用多執行緒的標記-清除演算法,最短的垃圾收集停頓時間可以為互動比較高的程式提高用戶體驗,
CMS 作業機制相比其他的垃圾收集器來說更復雜,整個程序分為以下 4 個階段:
9.6.1 初始標記
只是標記一下 GC Roots 能直接關聯的物件,速度很快,仍然需要暫停所有的作業執行緒,
9.6.2 并發標記
進行 GC Roots 跟蹤的程序,和用戶執行緒一起作業,不需要暫停作業執行緒,
9.6.3 重新標記
為了修正在并發標記期間,因用戶程式繼續運行而導致標記產生變動的那一部分物件的標記 記錄,仍然需要暫停所有的作業執行緒,
9.6.4 并發清除
清除 GC Roots 不可達物件,和用戶執行緒一起作業,不需要暫停作業執行緒,由于耗時最長的并發標記和并發清除程序中,垃圾收集執行緒可以和用戶現在一起并發作業,所以總體上來看 CMS 收集器的記憶體回收和用戶執行緒是一起并發地執行,
9.7 G1收集器
Garbage First 垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與 CMS 收集器,G1 收集器兩個最突出的改進是:
- 基于標記-整理演算法,不產生記憶體碎片,
- 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實作低停頓垃圾回收,
G1 收集器避免全區域垃圾收集,它把堆記憶體劃分為大小固定的幾個獨立區域,并且跟蹤這些區域的垃圾收集進度,同時在后臺維護一個優先級串列,每次根據所允許的收集時間,優先回收垃圾最多的區域,區域劃分和優先級區域回識訓制,確保 G1 收集器可以在有限時間獲得最高的垃圾收集效率,
10、簡述 Java 垃圾回識訓制
在 Java 中,程式員是不需要顯式的去釋放一個物件的記憶體的,而是由虛擬機自行執行,在 JVM 中,有一個垃圾回收執行緒,它是低優先級的,在正常情況下是不會執行的,只有在虛擬機空閑或者當前堆記憶體不足時,才會觸發執行,掃面那些沒有被任何參考的物件,并將它們添加到要回收的集合中,進行回收,
11、如何判斷一個物件是否存活?(或者 GC 物件的判定方法)
其實第 7 點回答了哈,這里再詳細說一下,
判斷一個物件是否存活有兩種方法:
11.1 參考計數法
所謂參考計數法就是給每一個物件設定一個參考計數器,每當有一個地方參考這個物件時,就將計數器加一,參考失效時,計數器就減一,當一個物件的參考計數器為零時,說明此物件沒有被參考,也就是“死物件”,將會被垃圾回收,
參考計數法有一個缺陷就是無法解決回圈參考問題,也就是說當物件 A 參考對 象 B,物件 B 又參考者物件 A,那么此時 A、B 物件的參考計數器都不為零, 也就造成無法完成垃圾回收,所以主流的虛擬機都沒有采用這種演算法,
11.2 可達性演算法(參考鏈法)
該演算法的思想是:從一個被稱為 GC Roots 的物件開始向下搜索,如果一個物件到 GC Roots 沒有任何參考鏈相連時,則說明此物件不可用,
在 Java 中可以作為 GC Roots 的物件有以下幾種:
- 虛擬機堆疊中參考的物件
- 方法區類靜態屬性參考的物件
- 方法區常量池參考的物件
- 本地方法堆疊 JNI 參考的物件
雖然這些演算法可以判定一個物件是否能被回收,但是當滿足上述條件時,一個物件不一定會被回收,當一個物件不可達 GC Root 時,這個物件并不會立馬被回收,而是出于一個死緩的階段,若要被真正的回收需要經歷兩次標記,
如果物件在可達性分析中沒有與 GC Root 的參考鏈,那么此時就會被第一次標記并且進行一次篩選,篩選的條件是是否有必要執行 finalize() 方法,當物件沒有覆寫 finalize() 方法或者已被虛擬機呼叫過,那么就認為是沒必要的, 如果該物件有必要執行 finalize() 方法,那么這個物件將會放在一個稱為 F-Queue 的對佇列中,虛擬機會觸發一個 Finalize() 執行緒去執行,此執行緒是低優先級的, 并且虛擬機不會承諾一直等待它運行完,這是因為如果 finalize() 執行緩慢或者發生了死鎖,那么就會造成 F-Queue 佇列一直等待,造成了記憶體回收系統的崩潰,GC 對處于 F-Queue 中的物件進行第二次被標記,這時,該物件將被移除 ”即將回收” 集合,等待回收,
12、垃圾回收的優點和原理
Java 語言中一個顯著的特點就是引入了垃圾回識訓制,使 C++ 程式員最頭疼的記憶體管理的問題迎刃而解,它使得 Java 程式員在撰寫程式的時候不再需要考慮記憶體管理,由于有個垃圾回識訓制,Java 中的物件不再有“作用域”的概念,只有物件的參考才有"作用域",垃圾回收可以有效的防止記憶體泄露,有效的使用可以使用的記憶體,垃圾回收器通常是作為一個單獨的低級別的執行緒運行,不可預知的情況下對記憶體堆中已經死亡的或者長時間沒有使用的物件進行清除和回收,程式員不能實時的呼叫垃圾回收器對某個物件或所有物件進行垃圾回收,
13、垃圾回收器可以馬上回收記憶體嗎?有什么辦法主動通知虛擬機進行垃圾回收?
對于 GC 來說,當程式員創建物件時,GC 就開始監控這個物件的地址、大小以及使用情況,通常,GC 采用有向圖的方式記錄和管理堆(heap)中的所有物件,通過這種方式確定哪些物件是”可達的”,哪些物件是”不可達的”,當 GC 確定一些物件為“不可達”時,GC 就有責任回收這些記憶體空間,
可以,程式員可以手動執行 System.gc(),通知 GC 運行,但是 Java 語言規范并不保證 GC 一定會執行,
14、Java 中會存在記憶體泄漏嗎,請簡單描述,
所謂記憶體泄露就是指一個不再被程式使用的物件或變數一直被占據在記憶體中,Java 中有垃圾回識訓制,它可以保證一物件不再被參考的時候,即物件變成了孤兒的時候,物件將自動被垃圾回收器從記憶體中清除掉,由于 Java 使用有向圖的方式進行垃圾回收管理,可以消除參考回圈的問題,例如有兩個物件,相互參考,只要它們和根行程不可達的,那么 GC 也是可以回收它們的,例如下面的代碼可以看到這種情況的記憶體回收:
public class GarbageTest {
public static void main(String[] args) throws IOException {
try {
gcTest();
} catch (IOException e) {
}
System.out.println("has exited gcTest!");
System.in.read();
System.in.read();
System.out.println("out begin gc!");
for (int i = 0; i < 100; i++) {
System.gc();
System.in.read();
System.in.read();
}
}
private static void gcTest() throws IOException {
System.in.read();
System.in.read();
Person p1 = new Person();
System.in.read();
System.in.read();
Person p2 = new Person();
p1.setMate(p2);
p2.setMate(p1);
System.out.println("before exit gctest!");
System.in.read();
System.in.read();
System.gc();
System.out.println("exit gctest!");
}
private static class Person {
byte[] data = new byte[20000000];
Person mate = null;
public void setMate(Person other) {
mate = other;
}
}
}
Java 中的記憶體泄露的情況:長生命周期的物件持有短生命周期物件的參考就很可能發生記憶體泄露,盡管短生命周期物件已經不再需要,但是因為長生命周期物件持有它的參考而導致不能被回收,這就是 Java 中記憶體泄露的發生場景,通俗地說,就是程式員可能創建了一個物件,以后一直不再使用這個物件,這個物件卻一直被參考,即這個物件無用但是卻無法被垃圾回收器回收的,這就是 Java 中可能出現記憶體泄露的情況,例如,快取系統,我們加載了一個物件放在快取中 (例如放在一個全域 map 物件中),然后一直不再使用它,這個物件一直被快取參考,但卻不再被使用,
檢查 Java 中的記憶體泄露,一定要讓程式將各種分支情況都完整執行到程式結束,然后看某個物件是否被使用過,如果沒有,則才能判定這個物件屬于記憶體泄露,
如果一個外部類的實體物件的方法回傳了一個內部類的實體物件,這個內部類物件被長期參考了,即使那個外部類實體物件不再被使用,但由于內部類持久外部類的實體物件,這個外部類物件將不會被垃圾回收,這也會造成記憶體泄露,
我們來看個堆疊經典的例子,主要特點就是清空堆疊中的某個元素,并不是徹底把它從陣列中拿掉,而是把存盤的總數減少,
public class Stack {
private Object[] elements = new Object[10];
private int size = 0;
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size) {
Object[] oldElements = elements;
elements = new Object[(2 * elements.length) + 1];
System.arraycopy(oldElements, 0, elements, 0, size);
}
}
}
上面的原理應該很簡單,假如堆疊加了 10 個元素,然后全部彈出來,雖然堆疊是空的,沒有我們要的東西,但是這是個物件是無法回收的,這個才符合了記憶體泄露的兩個條件:無用,無法回收,但是就是存在這樣的東西也不一定會導致什么樣的后果,如果這個堆疊用的比較少,也就浪費了幾個 K 記憶體而已,反正我們的記憶體都上 G 了,哪里會有什么影響,再說這個東西很快就會被回收的,有什么關系,下面再看個例子,
public class Bad {
public static Stack s = Stack();
static {
s.push(new Object());
s.pop(); //這里有一個物件發生記憶體泄露
s.push(new Object()); //上面的物件可以被回收了,等于是自愈了
}
}
因為是 static,就一直存在到程式退出,但是我們也可以看到它有自愈功能,就是說如果你的 Stack 最多有 100 個物件,那么最多也就只有 100 個物件無法被回收,其實這個應該很容易理解,Stack 內部持有 100 個參考,最壞的情況就是他們都是無用的,因為我們一旦放新的進去,以前的參考自然消失!
記憶體泄露的另外一種情況:當一個物件被存盤進 HashSet 集合中以后,就不能修改這個物件中的那些參與計算哈希值的欄位了,否則,物件修改后的哈希值與最初存盤進 HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該物件的當前參考作為的引數去 HashSet 集合中檢索對 象,也將回傳找不到物件的結果,這也會導致無法從 HashSet 集合中單獨洗掉當前物件,造成記憶體泄露,
15、簡述 Java 記憶體分配與回收策略以及 Minor GC 和 Major GC,
- 物件優先在堆的 Eden 區分配
- 大物件直接進入老年代
- 長期存活的物件將直接進入老年代
當 Eden 區沒有足夠的空間進行分配時,虛擬機會執行一次 Minor GC,Minor GC 通常發生在新生代的 Eden 區,在這個區的物件生存期短,往往發生 GC 的 頻率較高,回收速度比較快;Full GC/Major GC 發生在老年代,一般情況下, 觸發老年代 GC 的時候不會觸發 Minor GC,但是通過配置,可以在 Full GC 之前進行一次 Minor GC 這樣可以加快老年代的回收速度,
16、JVM 記憶體為什么要分成新生代,老年代,持久代,新生代中為什么要分為 Eden 和 Survivor,
第一個問題我覺得是通過分新生代,老年代,持久代而更好的利用有限的記憶體空間,
第二個問題:
- 如果沒有 Survivor,Eden 區每進行一次 Minor GC,存活的物件就會被送到老年代,老年代很快被填滿,觸發 Major GC,老年代的記憶體空間遠大于新生代,進行一次 Full GC 消耗的時間比 Minor GC 長得多,所以需要分為 Eden 和 Survivor,
- Survivor 的存在意義,就是減少被送到老年代的物件,進而減少 Full GC 的發生,Survivor 的預篩選保證,只有經歷 16 次 Minor GC 還能在新生代中存活的物件,才會被送到老年代,
- 設定兩個 Survivor 區最大的好處就是解決了碎片化,剛剛新建的物件在 Eden 中,經歷一次 Minor GC,Eden 中的存活物件就會被移動到第一塊 survivor space S0,Eden 被清空;等 Eden 區再滿了,就再觸發一次 Minor GC,Eden 和 S0 中的存活物件又會被復制送入第二塊 survivor space S1(這個程序非常重要,因為這種復制演算法保證了 S1 中來自 S0 和 Eden 兩部分的存活物件占用連續的記憶體空間,避免了碎片化的發生),
17、 Minor GC ,Full GC 觸發條件
Minor GC 觸發條件:當 Eden 區滿時,觸發 Minor GC,
Full GC 觸發條件:
- 呼叫 System.gc 時,系統建議執行 Full GC,但是不必然執行
- 老年代空間不足
- 方法區空間不足
- 通過 Minor GC 后進入老年代的平均大小大于老年代的可用記憶體
- 由 Eden區、From Space 區向 To Space 區復制時,物件大小大于 To Space 可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小于該物件大小,
18、當出現了記憶體溢位,你怎么排錯?
- 首先控制臺查看錯誤日志
- 然后使用 jdk 自帶的 jvisualvm工具查看系統的堆疊日志
- 定位出記憶體溢位的空間:堆,堆疊還是永久代(jdk8 以后不會出現永久代的記憶體溢位),
- 如果是堆記憶體溢位,看是否創建了超大的物件
- 如果是堆疊記憶體溢位,看是否創建了超大的物件,或者產生了死回圈,
19、你們線上應用的 JVM 引數有哪些?
這里老周給我們服務的 JVM 引數給大家參考下哈,按照自己線上應用來答就好了,
- -server
- -Xms4096M
- Xmx4096M
- -Xmn1536M
- -XX:MetaspaceSize=256M
- -XX:MaxMetaspaceSize=256M
- -XX:+UseParNewGC
- -XX:+UseConcMarkSweepGC
- -XX:+CMSScavengeBeforeRemark
- -XX:CMSInitiatingOccupancyFraction=75
- -XX:CMSInitiatingOccupancyOnly
20、什么是記憶體泄漏,它與記憶體溢位的關系?
20.1 記憶體泄漏 memory leak
是指程式在申請記憶體后,無法釋放已申請的記憶體空間,一次記憶體泄漏似乎不會有大的影響,但記憶體泄漏堆積后的后果就是記憶體溢位,
20.2 記憶體溢位 out of memory
指程式申請記憶體時,沒有足夠的記憶體供申請者使用,或者說,給了你一塊存盤 int 型別資料的存盤空間,但是你卻存盤 long 型別的資料,那么結果就是記憶體不夠用,此時就會報錯 OOM,即所謂的記憶體溢位,
20.3 二者的關系
- 記憶體泄漏的堆積最侄訓導致記憶體溢位
- 記憶體溢位就是你要的記憶體空間超過了系統實際分配給你的空間,此時系統相當于沒法滿足你的需求,就會報記憶體溢位的錯誤,
- 記憶體泄漏是指你向系統申請分配記憶體進行使用(new),可是使用完了以后卻不歸還(delete),結果你申請到的那塊記憶體你自己也不能再訪問(也許你把它的地址給弄丟了),而系統也不能再次將它分配給需要的程式,就相當于你租了個帶鑰匙的柜子,你存完東西之后把柜子鎖上之后,把鑰匙丟了或者沒有將鑰匙還回去,那么結果就是這個柜子將無法供給任何人使用,也無法被垃圾回收器回收,因為找不到他的任何資訊,
- 記憶體溢位:一個盤子用盡各種方法只能裝4個果子,你裝了5個,結果掉倒地上不能吃了,這就是溢位,比方說堆疊,堆疊滿時再做進堆疊必定產生空間溢位,叫上溢,堆疊空時再做退堆疊也產生空間溢位,稱為下溢,就是分配的記憶體不足以放下資料項序列,稱為記憶體溢位,說白了就是我承受不了那么多,那我就報錯,
20.4 記憶體泄漏的分類(按發生方式來分類)
-
常發性記憶體泄漏,發生記憶體泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊記憶體泄漏,
-
偶發性記憶體泄漏,發生記憶體泄漏的代碼只有在某些特定環境或操作程序下才會發生,常發性和偶發性是相對的,對于特定的環境,偶發性的也許就變成了常發性的,所以測驗環境和測驗方法對檢測記憶體泄漏至關重要,
-
一次性記憶體泄漏,發生記憶體泄漏的代碼只會被執行一次,或者由于演算法上的缺陷,導致總會有一塊僅且一塊記憶體發生泄漏,比如,在類的建構式中分配記憶體,在解構式中卻沒有釋放該記憶體,所以記憶體泄漏只會發生一次,
-
隱式記憶體泄漏,程式在運行程序中不停的分配記憶體,但是直到結束的時候才釋放記憶體,嚴格的說這里并沒有發生記憶體泄漏,因為最終程式釋放了所有申請的記憶體,但是對于一個服務器程式,需要運行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體,所以,我們稱這類記憶體泄漏為隱式記憶體泄漏,
20.5 記憶體溢位的原因及解決方法
20.5.1 記憶體溢位原因
- 記憶體中加載的資料量過于龐大,如一次從資料庫取出過多資料;
- 集合類中有對物件的參考,使用完后未清空,使得 JVM 不能回收;
- 代碼中存在死回圈或回圈產生過多重復的物件物體;
- 使用的第三方軟體中的 BUG;
- 啟動引數記憶體值設定的過小,
20.5.2 記憶體溢位的解決方案
- 修改 JVM 啟動引數,直接增加記憶體,(-Xms,-Xmx引數一定不要忘記加,)
- 檢查錯誤日志,查看“OutOfMemory”錯誤前是否有其它例外或錯誤,
- 對代碼進行走查和分析,找出可能發生記憶體溢位的位置,
20.5.3 重點排查以下幾點
-
檢查對資料庫查詢中,是否有一次獲得全部資料的查詢,一般來說,如果一次取十萬條記錄到記憶體,就可能引起記憶體溢位,這個問題比較隱蔽,在上線前,資料庫中資料較少,不容易出問題,上線后,資料庫中資料多了,一次查詢就有可能引起記憶體溢位,因此對于資料庫查詢盡量采用分頁的方式查詢,
-
檢查代碼中是否有死回圈或遞回呼叫,
-
檢查是否有大回圈重復產生新物件物體,
-
檢查 List、Map 等集合物件是否有使用完后,未清除的問題,List、Map 等集合物件會始終存有對物件的參考,使得這些物件不能被 GC 回收,
21、Full GC 問題的排查和解決經歷說一下
我們可以從以下幾個方面來進行排查
21.1 碎片化
對于 CMS,由于老年代的碎片化問題,在 YGC 時可能碰到晉升失敗(promotion failures,即使老年代還有足夠多有效的空間,但是仍然可能導致分配失敗,因為沒有足夠連續的空間),從而觸發Concurrent Mode Failure,發生會完全 STW 的 Full GC,Full GC 相比 CMS 這種并發模式的 GC 需要更長的停頓時間才能完成垃圾回收作業,這絕對是 Java 應用最大的災難之一,
為什么 CMS 場景下會有碎片化問題?由于 CMS 在老年代回收時,采用的是標記清理(Mark-Sweep)演算法,它在垃圾回收時并不會壓縮堆,榷訓月累,導致老年代的碎片化問題會越來越嚴重,直到發生單執行緒的 Mark-Sweep-Compact GC,即FullGC,會完全 STW,如果堆比較大的話,STW 的時間可能需要好幾秒,甚至十多秒,幾十秒都有可能,
21.2 GC 時作業系統的活動
當發生 GC 時,一些作業系統的活動,比如 swap,可能導致 GC 停頓時間更長,這些停頓可能是幾秒,甚至幾十秒級別,
如果你的系統配置了允許使用 swap 空間,作業系統可能把 JVM 行程的非活動記憶體頁移到 swap 空間,從而釋放記憶體給當前活動行程(可能是作業系統上其他行程,取決于系統調度),Swapping 由于需要訪問磁盤,所以相比物理記憶體,它的速度慢的令人發指,所以,如果在 GC 的時候,系統正好需要執行 Swapping,那么 GC 停頓的時間一定會非常非常恐怖,
除了swapping 以外,我們也需要監控了解長 GC 暫停時的任何 IO 或者網路活動情況等, 可以通過 iostat 和 netstat 兩個工具來實作,我們還能通過 mpstat 查看 CPU 統計資訊,從而弄清楚在 GC 的時候是否有足夠的 CPU 資源,
21.3 堆空間不夠
如果應用程式需要的記憶體比我們執行的 Xmx 還要大,也會導致頻繁的垃圾回收,甚至 OOM,由于堆空間不足,物件分配失敗,JVM 就需要呼叫 GC 嘗試回收已經分配的空間,但是 GC 并不能釋放更多的空間,從而又回導致 GC,進入惡性回圈,
同樣的,如果在老年代的空間不夠的話,也會導致頻繁 Full GC,這類問題比較好辦,給足老年代和永久代,
21.4 JVM Bug
什么軟體都有 BUG,JVM 也不例外,有時候,GC 的長時間停頓就有可能是 BUG 引起的,例如,下面列舉的這些 JVM 的 BUG,就可能導致 Java 應用在 GC 時長時間停頓,
6459113: CMS+ParNew: wildly different ParNew pause times depending on heap shape caused by allocation spread
fixed in JDK 6u1 and 7
6572569: CMS: consistently skewed work distribution indicated in (long) re-mark pauses
fixed in JDK 6u4 and 7
6631166: CMS: better heuristics when combatting fragmentation
fixed in JDK 6u21 and 7
6999988: CMS: Increased fragmentation leading to promotion failure after CR#6631166 got implemented
fixed in JDK 6u25 and 7
6683623: G1: use logarithmic BOT code such as used by other collectors
fixed in JDK 6u14 and 7
6976350: G1: deal with fragmentation while copying objects during GC
fixed in JDK 8
如果你的 JDK 正好是上面這些版本,強烈建議升級到更新 BUG 已經修復的版本,
21.5 顯式 System.gc 呼叫
檢查是否有顯示的 System.gc 呼叫,應用中的一些類里,或者第三方模塊中呼叫 System.gc 呼叫從而觸發 STW 的 Full GC,也可能會引起非常長時間的停頓,如下 GC 日志所示,Full GC 后面的(System)表示它是由呼叫 System.GC 觸發的 FullGC,并且耗時 5.75 秒:
164638.058: [Full GC (System) [PSYoungGen: 22789K->0K(992448K)]
[PSOldGen: 1645508K->1666990K(2097152K)] 1668298K->1666990K(3089600K)
[PSPermGen: 164914K->164914K(166720K)], 5.7499132 secs] [Times: user=5.69, sys=0.06, real=5.75 secs]
如果你使用了 RMI,能觀察到固定時間間隔的 Full GC,也是由于 RMI 的實作呼叫了 System.gc,這個時間間隔可以通過系統屬性配置:
-Dsun.rmi.dgc.server.gcInterval=7200000
-Dsun.rmi.dgc.client.gcInterval=7200000
JDK 1.4.2和5.0的默認值是60000毫秒,即1分鐘;JDK6以及以后的版本,默認值是3600000毫秒,即1個小時,
如果你要關閉通過呼叫 System.gc() 觸發 Full GC,配置JVM引數 -XX:+DisableExplicitGC 即可,
21.6 那么如何定位并解決這類問題問題呢?
- 配置 JVM 引數:-XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps and -XX:+PrintGCApplicationStoppedTime. 如果是 CMS,還需要添加-XX:PrintFLSStatistics=2,然后收集 GC 日志,因為 GC 日志能告訴我們 GC 頻率,是否長時間停頓等重要資訊,
- 使用 vmstat, iostat, netstat 和 mpstat 等工具監控系統全方位健康狀況,
- 使用 GCHisto 工具可視化分析 GC 日志,弄明白消耗了很長時間的 GC,以及這些 GC 的出現是否有一定的規律,
- 嘗試從 GC 日志中能否找出一下 JVM 堆碎片化的表征,
- 監控指定應用的堆大小是否足夠,
- 檢查你運行的 JVM 版本,是否有與長時間停頓相關的 BUG,然后升級到修復問題的最新 JDK,
22、GC 中的三色標記你了解嗎?
Java 垃圾回收目前采用的演算法是可達性標記演算法,即基于 GC Roots 進行可達性分析,分析標記程序采用三色標記法,
三色標記按照垃圾回收器 ”是否訪問過“ 為條件將物件標為三種顏色:
- 白色:表示物件未被垃圾回收器訪問過;
- 灰色:表示物件本身被垃圾回收器訪問過,但這個物件上至少有一個參考未被訪問掃描過;
- 黑色:物件完全被掃描,并且其所有參考都已完成掃描,
其實灰色就是一個過渡狀態,在垃圾回收器標記完成結束后,物件只有白色或者黑色其中一種狀態,當為白色時,說明該物件在可達性分析后沒有參考,也就是之后被銷毀的物件,當為黑色時,說明當前物件為此次垃圾回收存活物件,
當垃圾回收開始時,GC Roots 物件是黑色物件,沿著他找到的物件 A 首先是灰色物件,當物件 A 所有參考都掃描后,物件 A 為黑色物件,以此類推繼續往下掃描,
這是垃圾回收標記基本操作,
但目前的垃圾回收是并發操作的,就是在你進行標記的時候,程式執行緒也是繼續運行的,那原有的物件參考就有可能發生變化,
比如已經標記為黑色(存活物件)物件,程式運行將其所有參考取消,那么這個物件應該是白色的(垃圾物件),這種情況相對好一些,在下一次垃圾回收時候,我們還是可以把他回收,只是讓他多活了一會兒,系統也不會出現什么問題,可以不解決,
當已經標記為白色物件(垃圾物件)時,此時程式運行又讓他和其他黑色(存活)物件產生參考,那么該物件最終也應該是黑色(存活)物件,如果此時垃圾回收器標記完回收后,會出現物件丟失,這樣就引起程式問題,
出現物件丟失的必要條件是(在垃圾回收器標記進行時出現的改變):
- 重新建立了一潭訓多條黑色物件到白色物件的新參考
- 洗掉了灰色物件到白色物件的直接或間接參考
因為已經標記黑色的物件說明此輪垃圾回收中垃圾回收器對其的掃描已經完成,不會再掃描,如果他又參考了一個白色物件,而且這個白色物件在垃圾掃描完后還是白色,那么這個白色物件最侄訓被誤回收,
為了防止這種情況的出現,上邊說的必要條件中的一個處理掉即可避免物件誤洗掉;
當黑色物件直接參考了一個白色物件后,我們就將這個黑色物件記錄下來,在掃描完成后,重新對這個黑色物件掃描,這個就是增量更新(Incremental Update),
當洗掉了灰色物件到白色物件的直接或間接參考后,就將這個灰色物件記錄下來,再以此灰色物件為根,重新掃描一次,這個就是原始快照(Snapshot At TheBeginning,SATB),
自此,物件可達標記完成,
歡迎大家關注我的公眾號【老周聊架構】,Java后端主流技術堆疊的原理、原始碼分析、架構以及各種互聯網高并發、高性能、高可用的解決方案,

喜歡的話,一鍵三連走一波,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/275776.html
標籤:java
