三天吃透Java虛擬機器器面試八股文

2023-02-26 18:00:38

本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~

Github地址:https://github.com/Tyson0314/Java-learning


講一下JVM記憶體結構?

JVM記憶體結構分為5大區域,程式計數器虛擬機器器棧本地方法棧方法區

程式計數器

執行緒私有的,作為當前執行緒的行號指示器,用於記錄當前虛擬機器器正在執行的執行緒指令地址。程式計數器主要有兩個作用:

  1. 當前執行緒所執行的位元組碼的行號指示器,通過它實現程式碼的流程控制,如:順序執行、選擇、迴圈、例外處理。
  2. 在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,當執行緒被切換回來的時候能夠知道它上次執行的位置。

程式計數器是唯一一個不會出現 OutOfMemoryError 的記憶體區域,它的生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡。

虛擬機器器棧

Java 虛擬機器器棧是由一個個棧幀組成,而每個棧幀中都擁有:區域性變數表運算元棧動態連結方法出口資訊。每一次函數呼叫都會有一個對應的棧幀被壓入虛擬機器器棧,每一個函數呼叫結束後,都會有一個棧幀被彈出。

區域性變數表是用於存放方法引數和方法內的區域性變數。

每個棧幀都包含一個指向執行時常數池中該棧所屬方法的符號參照,在方法呼叫過程中,會進行動態連結,將這個符號參照轉化為直接參照。

  • 部分符號參照在類載入階段的時候就轉化為直接參照,這種轉化就是靜態連結
  • 部分符號參照在執行期間轉化為直接參照,這種轉化就是動態連結

Java 虛擬機器器棧也是執行緒私有的,每個執行緒都有各自的 Java 虛擬機器器棧,而且隨著執行緒的建立而建立,隨著執行緒的死亡而死亡。Java 虛擬機器器棧會出現兩種錯誤:StackOverFlowErrorOutOfMemoryError

可以通過-Xss引數來指定每個執行緒的虛擬機器器棧記憶體大小:

java -Xss2M

本地方法棧

虛擬機器器棧為虛擬機器器執行 Java 方法服務,而本地方法棧則為虛擬機器器使用到的 Native 方法服務。Native 方法一般是用其它語言(C、C++等)編寫的。

本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的區域性變數表、運算元棧、動態連結、出口資訊。

堆用於存放物件範例,是垃圾收集器管理的主要區域,因此也被稱作GC堆。堆可以細分為:新生代(Eden空間、From SurvivorTo Survivor空間)和老年代。

通過 -Xms設定程式啟動時佔用記憶體大小,通過-Xmx設定程式執行期間最大可佔用的記憶體大小。如果程式執行需要佔用更多的記憶體,超出了這個設定值,就會丟擲OutOfMemory異常。

java -Xms1M -Xmx2M

1.方法區

方法區與 Java 堆一樣,是各個執行緒共用的記憶體區域,它用於儲存已被虛擬機器器載入的類資訊、常數、靜態變數、即時編譯器編譯後的程式碼等資料。

對方法區進行垃圾回收的主要目標是對常數池的回收和對類的解除安裝

2.永久代

方法區是 JVM 的規範,而永久代PermGen是方法區的一種實現方式,並且只有 HotSpot 有永久代。對於其他型別的虛擬機器器,如JRockit沒有永久代。由於方法區主要儲存類的相關資訊,所以對於動態生成類的場景比較容易出現永久代的記憶體溢位。

3.元空間

JDK 1.8 的時候,HotSpot的永久代被徹底移除了,使用元空間替代。元空間的本質和永久代類似,都是對JVM規範中方法區的實現。兩者最大的區別在於:元空間並不在虛擬機器器中,而是使用直接記憶體。

為什麼要將永久代替換為元空間呢?

永久代記憶體受限於 JVM 可用記憶體,而元空間使用的是直接記憶體,受本機可用記憶體的限制,雖然元空間仍舊可能溢位,但是相比永久代記憶體溢位的概率更小。

執行時常數池

執行時常數池是方法區的一部分,在類載入之後,會將編譯器生成的各種字面量和符號引號放到執行時常數池。在執行期間動態生成的常數,如 String 類的 intern()方法,也會被放入執行時常數池。

直接記憶體

直接記憶體並不是虛擬機器器執行時資料區的一部分,也不是虛擬機器器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現。

NIO的Buffer提供了DirectBuffer,可以直接存取系統實體記憶體,避免堆內記憶體到堆外記憶體的資料拷貝操作,提高效率。DirectBuffer直接分配在實體記憶體中,並不佔用堆空間,其可申請的最大記憶體受作業系統限制,不受最大堆記憶體的限制。

直接記憶體的讀寫操作比堆記憶體快,可以提升程式I/O操作的效能。通常在I/O通訊過程中,會存在堆內記憶體到堆外記憶體的資料拷貝操作,對於需要頻繁進行記憶體間資料拷貝且生命週期較短的暫存資料,都建議儲存到直接記憶體。

Java物件的定位方式

Java 程式通過棧上的 reference 資料來操作堆上的具體物件。物件的存取方式由虛擬機器器實現而定,目前主流的存取方式有使用控制程式碼和直接指標兩種:

  • 如果使用控制程式碼的話,那麼 Java 堆中將會劃分出一塊記憶體來作為控制程式碼池,reference 中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件範例資料與型別資料各自的具體地址資訊。使用控制程式碼來存取的最大好處是 reference 中儲存的是穩定的控制程式碼地址,在物件被移動時只會改變控制程式碼中的範例資料指標,而 reference 本身不需要修改。
  • 直接指標。reference 中儲存的直接就是物件的地址。物件包含到物件型別資料的指標,通過這個指標可以存取物件型別資料。使用直接指標存取方式最大的好處就是存取物件速度快,它節省了一次指標定位的時間開銷,虛擬機器器hotspot主要是使用直接指標來存取物件。

本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~

Github地址:https://github.com/Tyson0314/Java-learning

說一下堆疊的區別?

  1. 堆的實體地址分配是不連續的,效能較慢;棧的實體地址分配是連續的,效能相對較快。

  2. 堆存放的是物件的範例和陣列;棧存放的是區域性變數,運算元棧,返回結果等。

  3. 堆是執行緒共用的;棧是執行緒私有的。

什麼情況下會發生棧溢位?

  • 當執行緒請求的棧深度超過了虛擬機器器允許的最大深度時,會丟擲StackOverFlowError異常。這種情況通常是因為方法遞迴沒終止條件。
  • 新建執行緒的時候沒有足夠的記憶體去建立對應的虛擬機器器棧,虛擬機器器會丟擲OutOfMemoryError異常。比如執行緒啟動過多就會出現這種情況。

類檔案結構

Class 檔案結構如下:

ClassFile {
    u4             magic; //類檔案的標誌
    u2             minor_version;//小版本號
    u2             major_version;//大版本號
    u2             constant_pool_count;//常數池的數量
    cp_info        constant_pool[constant_pool_count-1];//常數池
    u2             access_flags;//類的存取標記
    u2             this_class;//當前類的索引
    u2             super_class;//父類別
    u2             interfaces_count;//介面
    u2             interfaces[interfaces_count];//一個類可以實現多個介面
    u2             fields_count;//欄位屬性
    field_info     fields[fields_count];//一個類會可以有個欄位
    u2             methods_count;//方法數量
    method_info    methods[methods_count];//一個類可以有個多個方法
    u2             attributes_count;//此類的屬性表中的屬性數
    attribute_info attributes[attributes_count];//屬性表集合
}

主要引數如下:

魔數class檔案標誌。

檔案版本:高版本的 Java 虛擬機器器可以執行低版本編譯器生成的類檔案,但是低版本的 Java 虛擬機器器不能執行高版本編譯器生成的類檔案。

常數池:存放字面量和符號參照。字面量類似於 Java 的常數,如字串,宣告為final的常數值等。符號參照包含三類:類和介面的全限定名,方法的名稱和描述符,欄位的名稱和描述符。

存取標誌:識別類或者介面的存取資訊,比如這個Class是類還是介面,是否為 public 或者 abstract 型別等等。

當前類的索引:類索參照於確定這個類的全限定名。

什麼是類載入?類載入的過程?

類的載入指的是將類的class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個此類的物件,通過這個物件可以存取到方法區對應的類資訊。

載入

  1. 通過類的全限定名獲取定義此類的二進位制位元組流
  2. 將位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構
  3. 在記憶體中生成一個代表該類的Class物件,作為方法區類資訊的存取入口

驗證

確保Class檔案的位元組流中包含的資訊符合虛擬機器器規範,保證在執行後不會危害虛擬機器器自身的安全。主要包括四種驗證:檔案格式驗證,後設資料驗證,位元組碼驗證,符號參照驗證

準備

為類變數分配記憶體並設定類變數初始值的階段。

解析

虛擬機器器將常數池內的符號參照替換為直接參照的過程。符號參照用於描述目標,直接參照直接指向目標的地址。

初始化

開始執行類中定義的Java程式碼,初始化階段是呼叫類構造器的過程。

什麼是雙親委派模型?

一個類載入器收到一個類的載入請求時,它首先不會自己嘗試去載入它,而是把這個請求委派給父類別載入器去完成,這樣層層委派,因此所有的載入請求最終都會傳送到頂層的啟動類載入器中,只有當父類別載入器反饋自己無法完成這個載入請求時,子載入器才會嘗試自己去載入。

雙親委派模型的具體實現程式碼在 java.lang.ClassLoader中,此類的 loadClass() 方法執行過程如下:先檢查類是否已經載入過,如果沒有則讓父類別載入器去載入。當父類別載入器載入失敗時丟擲 ClassNotFoundException,此時嘗試自己去載入。原始碼如下:

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

為什麼需要雙親委派模型?

雙親委派模型的好處:可以防止記憶體中出現多份同樣的位元組碼。如果沒有雙親委派模型而是由各個類載入器自行載入的話,如果使用者編寫了一個java.lang.Object的同名類並放在ClassPath中,多個類載入器都去載入這個類到記憶體中,系統中將會出現多個不同的Object類,那麼類之間的比較結果及類的唯一性將無法保證。

什麼是類載入器,類載入器有哪些?

  • 實現通過類的全限定名獲取該類的二進位制位元組流的程式碼塊叫做類載入器。

    主要有一下四種類載入器:

    • 啟動類載入器:用來載入 Java 核心類庫,無法被 Java 程式直接參照。
    • 擴充套件類載入器:它用來載入 Java 的擴充套件庫。Java 虛擬機器器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。
    • 系統類載入器:它根據應用的類路徑來載入 Java 類。可通過ClassLoader.getSystemClassLoader()獲取它。
    • 自定義類載入器:通過繼承java.lang.ClassLoader類的方式實現。

類的範例化順序?

  1. 父類別中的static程式碼塊,當前類的static程式碼塊
  2. 父類別的普通程式碼塊
  3. 父類別的建構函式
  4. 當前類普通程式碼塊
  5. 當前類別建構函式

如何判斷一個物件是否存活?

對堆垃圾回收前的第一步就是要判斷那些物件已經死亡(即不再被任何途徑參照的物件)。判斷物件是否存活有兩種方法:參照計數法和可達性分析。

參照計數法

給物件中新增一個參照計數器,每當有一個地方參照它,計數器就加 1;當參照失效,計數器就減 1;任何時候計數器為 0 的物件就是不可能再被使用的。

這種方法很難解決物件之間相互迴圈參照的問題。比如下面的程式碼,obj1obj2 互相參照,這種情況下,參照計數器的值都是1,不會被垃圾回收。

public class ReferenceCount {
    Object instance = null;
	public static void main(String[] args) {
		ReferenceCount obj1 = new ReferenceCount();
		ReferenceCount obj2 = new ReferenceCount();
		obj1.instance = obj2;
		obj2.instance = obj1;
		obj1 = null;
		obj2 = null;
	}
}

可達性分析

通過GC Root物件為起點,從這些節點向下搜尋,搜尋所走過的路徑叫參照鏈,當一個物件到GC Root沒有任何的參照鏈相連時,說明這個物件是不可用的。

可作為GC Roots的物件有哪些?

  1. 虛擬機器器棧中參照的物件
  2. 本地方法棧中Native方法參照的物件
  3. 方法區中類靜態屬性參照的物件
  4. 方法區中常數參照的物件

什麼情況下類會被解除安裝?

需要同時滿足以下 3 個條件類才可能會被解除安裝 :

  • 該類所有的範例都已經被回收。
  • 載入該類的類載入器已經被回收。
  • 該類對應的 java.lang.Class 物件沒有在任何地方被參照,無法在任何地方通過反射存取該類的方法。

虛擬機器器可以對滿足上述 3 個條件的類進行回收,但不一定會進行回收。

強參照、軟參照、弱參照、虛參照是什麼,有什麼區別?

強參照:在程式中普遍存在的參照賦值,類似Object obj = new Object()這種參照關係。只要強參照關係還存在,垃圾收集器就永遠不會回收掉被參照的物件。

軟參照:如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。

//軟參照
SoftReference<String> softRef = new SoftReference<String>(str);

弱參照:在進行垃圾回收時,不管當前記憶體空間足夠與否,都會回收只具有弱參照的物件。

//弱參照
WeakReference<String> weakRef = new WeakReference<String>(str);

虛參照:虛參照並不會決定物件的生命週期。如果一個物件僅持有虛參照,那麼它就和沒有任何參照一樣,在任何時候都可能被垃圾回收。虛參照主要是為了能在物件被收集器回收時收到一個系統通知

GC是什麼?為什麼要GC?

GC(Garbage Collection),垃圾回收,是Java與C++的主要區別之一。作為Java開發者,一般不需要專門編寫記憶體回收和垃圾清理程式碼。這是因為在Java虛擬機器器中,存在自動記憶體管理和垃圾清理機制。對JVM中的記憶體進行標記,並確定哪些記憶體需要回收,根據一定的回收策略,自動的回收記憶體,保證JVM中的記憶體空間,防止出現記憶體洩露和溢位問題。

Minor GC 和 Full GC的區別?

  • Minor GC:回收新生代,因為新生代物件存活時間很短,因此 Minor GC會頻繁執行,執行的速度一般也會比較快。

  • Full GC:回收老年代和新生代,老年代的物件存活時間長,因此 Full GC 很少執行,執行速度會比 Minor GC 慢很多。

記憶體的分配策略?

物件優先在 Eden 分配

大多數情況下,物件在新生代 Eden 上分配,當 Eden 空間不夠時,觸發 Minor GC

大物件直接進入老年代

大物件是指需要連續記憶體空間的物件,最典型的大物件有長字串和大陣列。可以設定JVM引數 -XX:PretenureSizeThreshold,大於此值的物件直接在老年代分配。

長期存活的物件進入老年代

通過引數 -XX:MaxTenuringThreshold 可以設定物件進入老年代的年齡閾值。物件在Survivor區每經過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定程度,就會被晉升到老年代中。

動態物件年齡判定

並非物件的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有物件大小的總和大於 Survivor 空間的一半,則年齡大於或等於該年齡的物件可以直接進入老年代,無需達到 MaxTenuringThreshold 年齡閾值。

空間分配擔保

在發生 Minor GC 之前,虛擬機器器先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果條件成立的話,那麼 Minor GC 是安全的。如果不成立的話虛擬機器器會檢視 HandlePromotionFailure 的值是否允許擔保失敗。如果允許,那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次 Minor GC;如果小於,或者 HandlePromotionFailure 的值為不允許擔保失敗,那麼就要進行一次 Full GC

Full GC 的觸發條件?

對於 Minor GC,其觸發條件比較簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。而 Full GC 觸發條件相對複雜,有以下情況會發生 full GC:

呼叫 System.gc()

只是建議虛擬機器器執行 Full GC,但是虛擬機器器不一定真正去執行。不建議使用這種方式,而是讓虛擬機器器管理記憶體。

老年代空間不足

老年代空間不足的常見場景為前文所講的大物件直接進入老年代、長期存活的物件進入老年代等。為了避免以上原因引起的 Full GC,應當儘量不要建立過大的物件以及陣列、注意編碼規範避免記憶體洩露。除此之外,可以通過 -Xmn 引數調大新生代的大小,讓物件儘量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大物件進入老年代的年齡,讓物件在新生代多存活一段時間。

空間分配擔保失敗

使用複製演演算法的 Minor GC 需要老年代的記憶體空間作擔保,如果擔保失敗會執行一次 Full GC。

JDK 1.7 及以前的永久代空間不足

在 JDK 1.7 及以前,HotSpot 虛擬機器器中的方法區是用永久代實現的,永久代中存放的為一些 Class 的資訊、常數、靜態變數等資料。當系統中要載入的類、反射的類和呼叫的方法較多時,永久代可能會被佔滿,在未設定為採用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼虛擬機器器會丟擲 java.lang.OutOfMemoryError

垃圾回收演演算法有哪些?

垃圾回收演演算法有四種,分別是標記清除法、標記整理法、複製演演算法、分代收集演演算法

標記清除演演算法

首先利用可達性去遍歷記憶體,把存活物件和垃圾物件進行標記。標記結束後統一將所有標記的物件回收掉。這種垃圾回收演演算法效率較低,並且會產生大量不連續的空間碎片

複製清除演演算法

半區複製,用於新生代垃圾回收。將記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的記憶體使用完後,就將還存活的物件複製到另一塊去,然後再把使用的空間一次清理掉。

特點:實現簡單,執行高效,但可用記憶體縮小為了原來的一半,浪費空間。

標記整理演演算法

根據老年代的特點提出的一種標記演演算法,標記過程仍然與標記-清除演演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉邊界以外的記憶體。

分類收集演演算法

根據各個年代的特點採用最適當的收集演演算法。

一般將堆分為新生代和老年代。

  • 新生代使用複製演演算法
  • 老年代使用標記清除演演算法或者標記整理演演算法

在新生代中,每次垃圾收集時都有大批物件死去,只有少量存活,使用複製演演算法比較合適,只需要付出少量存活物件的複製成本就可以完成收集。老年代物件存活率高,適合使用標記-清理或者標記-整理演演算法進行垃圾回收。

有哪些垃圾回收器?

垃圾回收器主要分為以下幾種:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1

這7種垃圾收集器的特點:

收集器 序列、並行or並行 新生代/老年代 演演算法 目標 適用場景
Serial 序列 新生代 複製演演算法 響應速度優先 單CPU環境下的Client模式
ParNew 並行 新生代 複製演演算法 響應速度優先 多CPU環境時在Server模式下與CMS配合
Parallel Scavenge 並行 新生代 複製演演算法 吞吐量優先 在後臺運算而不需要太多互動的任務
Serial Old 序列 老年代 標記-整理 響應速度優先 單CPU環境下的Client模式、CMS的後備預案
Parallel Old 並行 老年代 標記-整理 吞吐量優先 在後臺運算而不需要太多互動的任務
CMS 並行 老年代 標記-清除 響應速度優先 集中在網際網路站或B/S系統伺服器端上的Java應用
G1 並行 both 標記-整理+複製演演算法 響應速度優先 面向伺服器端應用,將來替換CMS

Serial 收集器

單執行緒收集器,使用一個垃圾收集執行緒去進行垃圾回收,在進行垃圾回收的時候必須暫停其他所有的工作執行緒( Stop The World ),直到它收集結束。

特點:簡單高效;記憶體消耗小;沒有執行緒互動的開銷,單執行緒收集效率高;需暫停所有的工作執行緒,使用者體驗不好。

ParNew 收集器

Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾收集外,其他行為、引數與 Serial 收集器基本一致。

Parallel Scavenge 收集器

新生代收集器,基於複製清除演演算法實現的收集器。特點是吞吐量優先,能夠並行收集的多執行緒收集器,允許多個垃圾回收執行緒同時執行,降低垃圾收集時間,提高吞吐量。所謂吞吐量就是 CPU 中用於執行使用者程式碼的時間與 CPU 總消耗時間的比值(吞吐量 = 執行使用者程式碼時間 /(執行使用者程式碼時間 + 垃圾收集時間))。Parallel Scavenge 收集器關注點是吞吐量,高效率的利用 CPU 資源CMS 垃圾收集器關注點更多的是使用者執行緒的停頓時間

Parallel Scavenge收集器提供了兩個引數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis引數以及直接設定吞吐量大小的-XX:GCTimeRatio引數。

  • -XX:MaxGCPauseMillis引數的值是一個大於0的毫秒數,收集器將盡量保證記憶體回收花費的時間不超過使用者設定值。

  • -XX:GCTimeRatio引數的值大於0小於100,即垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。

Serial Old 收集器

Serial 收集器的老年代版本,單執行緒收集器,使用標記整理演演算法

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。多執行緒垃圾收集,使用標記整理演演算法

CMS 收集器

Concurrent Mark Sweep ,並行標記清除,追求獲取最短停頓時間,實現了讓垃圾收集執行緒與使用者執行緒基本上同時工作

CMS 垃圾回收基於標記清除演演算法實現,整個過程分為四個步驟:

  • 初始標記: 暫停所有使用者執行緒(Stop The World),記錄直接與 GC Roots 直接相連的物件 。
  • 並行標記:從GC Roots開始對堆中物件進行可達性分析,找出存活物件,耗時較長,但是不需要停頓使用者執行緒。
  • 重新標記: 在並行標記期間物件的參照關係可能會變化,需要重新進行標記。此階段也會暫停所有使用者執行緒。
  • 並行清除:清除標記物件,這個階段也是可以與使用者執行緒同時並行的。

在整個過程中,耗時最長的是並行標記和並行清除階段,這兩個階段垃圾收集執行緒都可以與使用者執行緒一起工作,所以從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起並行執行的。

優點:並行收集,停頓時間短。

缺點

  • 標記清除演演算法導致收集結束有大量空間碎片
  • 產生浮動垃圾,在並行清理階段使用者執行緒還在執行,會不斷有新的垃圾產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中回收它們,只好等到下一次垃圾回收再處理;

G1收集器

G1垃圾收集器的目標是在不同應用場景中追求高吞吐量和低停頓之間的最佳平衡

G1將整個堆分成相同大小的分割區(Region),有四種不同型別的分割區:Eden、Survivor、Old和Humongous。分割區的大小取值範圍為 1M 到 32M,都是2的冪次方。分割區大小可以通過-XX:G1HeapRegionSize引數指定。Humongous區域用於儲存大物件。G1規定只要大小超過了一個分割區容量一半的物件就認為是大物件。

G1 收集器對各個分割區回收所獲得的空間大小和回收所需時間的經驗值進行排序,得到一個優先順序列表,每次根據使用者設定的最大回收停頓時間,優先回收價值最大的分割區。

特點:可以由使用者指定期望的垃圾收集停頓時間。

G1 收集器的回收過程分為以下幾個步驟:

  • 初始標記。暫停所有其他執行緒,記錄直接與 GC Roots 直接相連的物件,耗時較短 。
  • 並行標記。從GC Roots開始對堆中物件進行可達性分析,找出要回收的物件,耗時較長,不過可以和使用者程式並行執行。
  • 最終標記。需對其他執行緒做短暫的暫停,用於處理並行標記階段物件參照出現變動的區域。
  • 篩選回收。對各個分割區的回收價值和成本進行排序,根據使用者所期望的停頓時間來制定回收計劃,然後把決定回收的分割區的存活物件複製到空的分割區中,再清理掉整個舊的分割區的全部空間。這裡的操作涉及存活物件的移動,會暫停使用者執行緒,由多條收集器執行緒並行完成。

常用的 JVM 調優的命令都有哪些?

jps:列出本機所有 Java 程序的程序號

常用引數如下:

  • -m 輸出main方法的引數
  • -l 輸出完全的包名和應用主類名
  • -v 輸出JVM引數
jps -lvm
//output
//4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8

jstack:檢視某個 Java 程序內的執行緒堆疊資訊。使用引數-l可以列印額外的鎖資訊,發生死鎖時可以使用jstack -l pid觀察鎖持有情況。

jstack -l 4124 | more

輸出結果如下:

"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

WAITING (parking)指執行緒處於掛起中,在等待某個條件發生,來把自己喚醒。

jstat:用於檢視虛擬機器器各種執行狀態資訊(類裝載、記憶體、垃圾收集等執行資料)。使用引數-gcuitl可以檢視垃圾回收的統計資訊。

jstat -gcutil 4124
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  67.21  19.20  96.36  94.96     10    0.084     3    0.191    0.275

引數說明:

  • S0Survivor0區當前使用比例
  • S1Survivor1區當前使用比例
  • EEden區使用比例
  • O:老年代使用比例
  • M:後設資料區使用比例
  • CCS:壓縮使用比例
  • YGC:年輕代垃圾回收次數
  • FGC:老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間

jmap:檢視堆記憶體快照。通過jmap命令可以獲得執行中的堆記憶體的快照,從而可以對堆記憶體進行離線分析。

查詢程序4124的堆記憶體快照,輸出結果如下:

>jmap -heap 4124
Attaching to process ID 4124, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4238344192 (4042.0MB)
   NewSize                  = 88604672 (84.5MB)
   MaxNewSize               = 1412431872 (1347.0MB)
   OldSize                  = 177733632 (169.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 327155712 (312.0MB)
   used     = 223702392 (213.33922576904297MB)
   free     = 103453320 (98.66077423095703MB)
   68.37795697725736% used
From Space:
   capacity = 21495808 (20.5MB)
   used     = 0 (0.0MB)
   free     = 21495808 (20.5MB)
   0.0% used
To Space:
   capacity = 23068672 (22.0MB)
   used     = 0 (0.0MB)
   free     = 23068672 (22.0MB)
   0.0% used
PS Old Generation
   capacity = 217579520 (207.5MB)
   used     = 41781472 (39.845916748046875MB)
   free     = 175798048 (167.65408325195312MB)
   19.20285144484187% used

27776 interned Strings occupying 3262336 bytes.

jinfojinfo -flags 1。檢視當前的應用JVM引數設定。

Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.111-b14
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=480247808 -XX:MaxNewSize=160038912 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldSize=20971520 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
Command line:

檢視所有引數java -XX:+PrintFlagsFinal -version。用於檢視最終值,初始值可能被修改掉(檢視初始值可以使用java -XX:+PrintFlagsInitial)。

[Global flags]
    uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
    uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {product}
    uintx AdaptiveSizePausePolicy                   = 0                                   {product}
    uintx AdaptiveSizePolicyCollectionCostMargin    = 50                                  {product}
    uintx AdaptiveSizePolicyInitializingSteps       = 20                                  {product}
    uintx AdaptiveSizePolicyOutputInterval          = 0                                   {product}
    uintx AdaptiveSizePolicyWeight                  = 10                                  {product}
    uintx AdaptiveSizeThroughPutPolicy              = 0                                   {product}
    uintx AdaptiveTimeWeight                        = 25                                  {product}
     bool AdjustConcurrency                         = false                               {product}
     bool AggressiveOpts                            = false                               {product}
     ....

本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~

Github地址:https://github.com/Tyson0314/Java-learning

物件頭瞭解嗎?

Java 記憶體中的物件由以下三部分組成:物件頭範例資料對齊填充位元組

而物件頭由以下三部分組成:mark word指向類資訊的指標陣列長度(陣列才有)。

mark word包含:物件的雜湊碼、分代年齡和鎖標誌位。

物件的範例資料就是 Java 物件的屬性和值。

對齊填充位元組:因為JVM要求物件佔的記憶體大小是 8bit 的倍數,因此後面有幾個位元組用於把物件的大小補齊至 8bit 的倍數。

記憶體對齊的主要作用是:

  1. 平臺原因:不是所有的硬體平臺都能存取任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。
  2. 效能原因:經過記憶體對齊後,CPU的記憶體存取速度大大提升。

main方法執行過程

以下是範例程式碼:

public class Application {
    public static void main(String[] args) {
        Person p = new Person("大彬");
        p.getName();
    }
}

class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

執行main方法的過程如下:

  1. 編譯Application.java後得到 Application.class 後,執行這個class檔案,系統會啟動一個 JVM 程序,從類路徑中找到一個名為 Application.class 的二進位制檔案,將 Application 類資訊載入到執行時資料區的方法區內,這個過程叫做類的載入。
  2. JVM 找到 Application 的主程式入口,執行main方法。
  3. main方法的第一條語句為 Person p = new Person("大彬") ,就是讓 JVM 建立一個Person物件,但是這個時候方法區中是沒有 Person 類的資訊的,所以 JVM 馬上載入 Person 類,把 Person 類的資訊放到方法區中。
  4. 載入完 Person 類後,JVM 在堆中分配記憶體給 Person 物件,然後呼叫建構函式初始化 Person 物件,這個 Person 物件持有指向方法區中的 Person 類的型別資訊的參照。
  5. 執行p.getName()時,JVM 根據 p 的參照找到 p 所指向的物件,然後根據此物件持有的參照定位到方法區中 Person 類的型別資訊的方法表,獲得 getName() 的位元組碼地址。
  6. 執行getName()方法。

物件建立過程

  1. 類載入檢查:當虛擬機器器遇到一條 new 指令時,首先檢查是否能在常數池中定位到這個類的符號參照,並且檢查這個符號參照代表的類是否已被載入過、解析和初始化過。如果沒有,那先執行類載入。
  2. 分配記憶體:在類載入檢查通過後,接下來虛擬機器器將為物件範例分配記憶體。
  3. 初始化。分配到的記憶體空間都初始化為零值,通過這個操作保證了物件的欄位可以不賦初始值就直接使用,程式能存取到這些欄位的資料型別所對應的零值。
  4. 設定物件頭Hotspot 虛擬機器器的物件頭包括:儲存物件自身的執行時資料(雜湊碼、分代年齡、鎖標誌等等)、型別指標和資料長度(陣列物件才有),型別指標就是物件指向它的類資訊的指標,虛擬機器器通過這個指標來確定這個物件是哪個類的範例。
  5. 按照Java程式碼進行初始化

如何排查 OOM 的問題?

線上JVM必須設定-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heapdump.hprof,當OOM發生時自動 dump 堆記憶體資訊到指定目錄

排查 OOM 的方法如下:

  • 檢視伺服器執行紀錄檔紀錄檔,捕捉到記憶體溢位異常
  • jstat 檢視監控JVM的記憶體和GC情況,評估問題大概出在什麼區域
  • 使用MAT工具載入dump檔案,分析大物件的佔用情況

什麼是記憶體溢位和記憶體洩露?

記憶體溢位指的是程式申請記憶體時,沒有足夠的記憶體供申請者使用,比如給了你一塊儲存int型別資料的儲存空間,但是你卻儲存long型別的資料,那麼結果就是記憶體不夠用,此時就會報錯OOM,即記憶體溢位。

記憶體洩露是指程式中間動態分配了記憶體,但在程式結束時沒有釋放這部分記憶體,從而造成那部分記憶體不可用的情況。這種情況重啟計算機可以解決,但也有可能再次發生記憶體洩露。記憶體洩露和硬體沒有關係,它是由軟體設計缺陷引起的。

像IO操作或者網路連線等,在使用完成之後沒有呼叫close()方法將其連線關閉,那麼它們佔用的記憶體是不會自動被GC回收的,此時就會產生記憶體洩露。

比如運算元據庫時,通過SessionFactory獲取一個session:

Session session=sessionFactory.openSession();

完成後我們必須呼叫session.close()方法關閉,否則就會產生記憶體洩露,因為sessionFactory這個長生命週期物件一直持有session這個短生命週期物件的參照。

那兩者有什麼不同呢?

記憶體洩露可以通過完善程式碼來避免,記憶體溢位可以通過調整設定來減少發生頻率,但無法徹底避免。

如何避免記憶體洩露和溢位呢?

  1. 儘早釋放無用物件的參照。比如使用臨時變數的時候,讓參照變數在退出活動域後自動設定為null,暗示垃圾收集器來收集該物件,防止發生記憶體洩露。
  2. 儘量少用靜態變數。因為靜態變數是全域性的,GC不會回收。
  3. 避免集中建立物件尤其是大物件,如果可以的話儘量使用流操作。
  4. 儘量運用池化技術(資料庫連線池等)以提高系統效能。
  5. 避免在迴圈中建立過多物件

參考資料

  • 周志明. 深入理解 Java 虛擬機器器 [M]. 機械工業出版社

最後給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~

Github地址https://github.com/Tyson0314/java-books