?JVM是Java高級部分,深入理解程序的運行及原理,面試中也問的比較多。
JVM是Java程序運行的虛擬機環境,實現了“一次編寫,到處運行”。它負責將字節碼解釋或編譯為機器碼,管理內存和資源,并提供運行時環境,使其不會被不同的底層操作系統和環境影響。
JVM是支持Java成為跨平臺語言的基礎,Java代碼在很多底層,針對不同操作系統做了處理,屏蔽了不同操作系統和處理器CPU的差異,才使得其能夠在多個平臺運行。
先把大部分知識點理解記熟了,再研究更深的。其中,內存結構和垃圾回收很重要,必問。
一:核心結構
首先,JVM與JRE、JDK的關系,也會經常被問到。使用Java時,要先安裝JDK,所以可以記住JDK是包裝的最完整的,然后再往前面推。
操作系統 => JVM(虛擬機) => JRE(JVM+基礎類庫) => JDK(JRE+編譯工具)
JVM主要由以下四大模塊構成:類加載器、運行時數據區、執行引擎、本地方法接口。其中,每個模塊又會有自己的處理和知識點。

下面就按照順序描述各個模塊的知識點和相關問題。
二:類加載器
類加載器(ClassLoader)是JVM的核心組件之一,負責在運行時動態加載類。
我們編寫的Java文件,會存儲為二進制字節碼,等待被使用。之后通過類加載器將二進制字節碼編譯為機器碼,生成完整的類并提供給運行時數據區。
1:類加載機制
一個類完整的生命周期,會經過五個階段,加載、鏈接、初始化、使用和卸載(被垃圾回收)。構成了類對象從無到有,以及最后結束的一個過程。
加載、連接、初始化稱為類加載機制,鏈接又分為驗證、準備、解析三個階段。

1:加載
簡單來說就是將類的二進制字節流(.class文件)轉換為方法區的數據結構,并生成 Class 對象。
方法區就是用來存儲類的數據、方法、結構等,加載就是將類數據放入方法區。方法區只是Java的一個概念,在不同廠商,不同版本有相應的實現(HotSpot Java 8使用元空間實現)。具體在內存結構中詳解。
注意:即時編譯的熱點代碼不在這個階段進入方法區。
類加載底層涉及到兩個概念:instanceKlass、_java_mirror。兩者均在加載階段完成,且是后續階段(鏈接、初始化)的基礎。
所以兩者的創建都屬于類加載機制。但兩者的角色和存儲位置不同,分別對應 JVM 內部元數據和 Java 層對象。
常見問題:Class對象在初始化階段生成?
Class?對象在加載階段就已生成(僅生成對象),但類的靜態變量賦值(初始化)在?初始化階段?執行,
重要:在加載階段,JVM會將Class文件加載到內存,并生成對應的Class對象。這時候就會創建運行時常量池,作為方法區的一部分。
運行時常量池此時包含Class文件常量池中的?原始數據?(字面量、符號引用等),但符號引用(如類、方法、字段的引用)?尚未解析為直接引用。
instanceKlass:
HotSpot JVM內部用C++結構 instanceKlass 描述類的?元數據?,屬于JVM內部的?元數據存儲?,用于?字節碼執行?。數據會放入方法區,所以instanceKlass也屬于?方法區的實現細節,
它的作用是存儲類的?完整結構信息?,例如:
_super:父類的instanceKlass指針;_methods:類的方法列表;_constants:運行時常量池(區別于Class文件中的常量池);_class_loader:加載該類的類加載器引用,以及其他。
特點:instanceKlass實例由JVM直接分配在?元空間(Metaspace)?中,屬于?非堆內存?。
_java_mirror:
_java_mirror是instanceKlass在Java堆中的?鏡像對象?,對應Java層的java.lang.Class實例。所以其就是我們平時的Class對象。
作用:為Java提供反射入口(Class.forName);存儲類的動態信息(如靜態變量);以及作為JVM內部元數據(instanceKlass)與Java應用層的橋梁,屬于應用層操作。
_java_mirror對象分配在?Java堆?中,由垃圾回收器管理。
關系:
二者有緊密聯系,當調用class.getName時,會先訪問Class實例,然后通過JNI調用到instanceKlass中的元數據,最后返回類名。
步驟 1?:JVM 解析 .class 文件,生成 instanceKlass(元空間)。
?步驟 2?:根據 instanceKlass 的信息,創建 _java_mirror(堆)。
?步驟 3?:instanceKlass 與 _java_mirror 建立雙向指針關聯。
instanceKlass內部有指針指向_java_mirror。
_java_mirror(即Class對象)通過Klass*指針關聯到instanceKlass(在JVM源碼中通過oopDesc結構實現)。
所以二者是有一個雙向綁定的關系,這是重點。
2:鏈接
總體來說就是確保字節碼符合JVM規范,以及進行默認值處理。其中又分為驗證、準備、解析三個階段。
驗證:
驗證文件格式是否編寫正確,數據是否正確,是否符合規范等。具體有文件格式驗證、元數據驗證、字節碼驗證以及符號引用驗證等。
說白了就是文件不能有問題,數據不能有問題,為了保證程序的安全性。
另外,驗證階段?不修改運行時常量池?,僅檢查其內容的合法性。
準備:
準備階段就是為靜態類變量分配內存并設置默認值。會根據是否final編譯期常量直接賦指定的值。
1:static
變量的賦值僅針對于 static 修飾的靜態基本數據類型,static修飾的變量稱為類變量;非static修飾的稱為實例變量,在此階段不處理,而是在對象實例化時分配內存及賦值。
2:final
如果是不加final的類變量,基本數據類型會在準備階段賦各自默認值,在初始化階段賦實際值;
如果是加了final的類變量,且值為編譯期常量(值在編譯器就確認了),則會在準備階段直接賦實際值。如果是運行期才能確定的值,則也同普通類變量,準備階段賦默認值。
static final int c = new Random().nextInt()。 //賦默認值0
?String字符串是特殊的引用類型,當String變量被 static final 修飾時,且賦值為字面量(編譯器常量),則也會直接賦值。其他情況下(非static,非final均為引用類型,默認為null)。具體原理可在方法區詳解,涉及常量池。
3:基本類型
準備階段僅針對于靜態八大基本類型賦值;如果是引用對象會默認為null(不涉及準備階段),統一在對象實例化時處理。
詳細理解準備階段賦值,有助于優化代碼,例如設置編譯器常量,減少初始化開銷。以及排查類加載問題等。
解析:
解析階段就是將常量池中的符號引用轉換為直接引用。符號引用就是一組符號來描述目標,直接引用就是直接指向目標的指針,可以用來定位到目標對象。
會將運行時常量池中的?符號引用?(如java/lang/Object)?解析為直接引用?(如內存地址或句柄),且緩存到運行時常量池中,避免重復解析。
這期間,運行時常量池會查詢字符串常量池是否存在字符串,如果有則直接引用,否則創建并引用。
使用 classLoader.loadClass() 獲取Class類對象時,不會觸發解析和初始化。
3:初始化
初始化是類加載的最后一個環節,但是注意,區別于之前的環節,初始化階段是嚴格按需觸發的。只有在首次主動使用時才會觸發,不會因類被加載而自動執行,即初始化是惰性的。
初始化賦值:
在Java中,對類變量(static修飾)進行初始化賦值有兩種方式:
- 聲明類變量時指定初始值
- 使用靜態代碼塊為類變量指定初始值
不加static的變量,稱為實例變量,只有在類實例化時才會分配空間并賦值。
注意:在使用靜態代碼塊方式時,必須把靜態變量定義在靜態代碼塊的前面,因為兩者是按照代碼順序執行的,順序不一致可能導致問題(空指針)。
如果靜態代碼塊中引用了其他類(例如通過靜態方法調用),被引用的類必須已經完成初始化,否則可能導致遞歸初始化問題。
實現原理:
編譯器會將所有?靜態變量的賦值操作?和?靜態代碼塊?,按代碼順序合并成一個名為<clinit>的類構造器方法。當觸發類初始化時,會執行類構造器<clinit>方法,為靜態變量賦實際值。原始構造的內容也會存在,但會在最后。
靜態代碼塊中如果拋出未捕獲的異常,會導致類初始化失敗,后續對該類的任何使用都會拋出ExceptionInInitializerError。
會引發初始化行為:
可將其歸納為主動使用類的場景,就會觸發類的初始化。
| 會初始化場景 | 場景描述 |
|---|---|
| new關鍵字 | 通過new創建對象時,類必須初始化。 |
| 訪問類的靜態成員 | 訪問類需計算的靜態變量(非編譯期常量),或訪問類的靜態方法,會導致初始化。 |
| 反射獲取類對象 | 調用 Class.forName() 默認會觸發實例化,可通過傳參設置為不初始化。 |
| 類繼承關系 | 子類初始化時,父類若沒有初始化過,會先初始化父類。 |
| 接口默認方法 | 如果接口的實現類初始化,且接口包含默認方法,則接口會初始化。 |
| 包含main方法的類 | 在程序啟動時,JVM會初始化包含main方法的類(主啟動類)。 |
| 動態語言支持(做了解) | 通過MethodHandle訪問靜態成員?:獲取類的靜態字段或方法句柄時觸發初始化。 MethodHandles.Lookup lookup = MethodHandles.lookup(); lookup.findStatic(MyClass.class, "staticMethod", MethodType.methodType(void.class)); |
靜態變量賦值包裝類型時,底層會自動裝箱,所以會導致初始化。
以上是會初始化的場景,問到了大致說幾個就可以了。另外還有不會觸發初始化的場景。
不會觸發初始化:
| 不會初始化場景 | 場景描述 |
|---|---|
| 訪問類的編譯時常量? | 訪問 static final 且值在編譯期確定(準備階段賦值)。 |
| ?數組類型聲明 | MyClass[] arr = new MyClass;(數組由JVM動態生成)。 |
| 反射獲取類對象 | 使用不會觸發初始化的反射獲取類對象,如.class,以及forName設置不初始化,classLoad等。 |
| ?父類已初始化 | 若父類已初始化,子類引用父類的靜態字段不會觸發子類初始化。 |
| 集合聲明類 | 有點牽強,做一個了解。類作為集合的泛型被聲明時,除非主動實例化類對象,否則不會初始化(泛型擦除)。 |
?注意,上述反射只是聲明時不會初始化,會在第一次使用該類class對象時觸發類的初始化。
初始化的唯一性:
JVM會通過鎖和狀態標記確保?類初始化僅執行一次?,無論觸發操作如何重復或并發。
1:同步鎖(<clinit>方法線程安全)
當多個線程同時嘗試初始化一個類時,JVM會通過隱式鎖確保只有一個線程執行<clinit>方法,其他線程阻塞等待(喚醒后根據狀態會跳過初始化)。
2:類標記狀態
JVM為每個類維護一個狀態(如uninitialized、initializing、initialized)。
一旦類完成初始化,后續操作直接跳過<clinit>方法。
具體使用時才初始化類,減輕了程序啟動時的開銷,避免了啟動程序時大批量初始化類的情況,提高了程序性能。
4:使用及卸載
使用:很好理解,類初始化之后,就可以進行使用了,可以對其屬性和方法進行操作。
卸載:JVM中的卸載指的是從JVM中移除Class對象、字節碼和靜態變量等,卸載并不常見。
因為通常只有 ClassLoader 被回收后,類才有可能被卸載。如果一個類是由系統類加載器加載的,那么它可能很難被卸載,因為系統類加載器通常不會回收(通常與JVM生命周期一致)。所以一般只有自定義類加載器,加載器實例不再被引用時,它加載的類才有可能被卸載。
類卸載條件:
- ?所有實例被回收?:該類(及其子類)的所有實例都已被垃圾回收。
- ?類加載器被回收?:加載該類的 ClassLoader 實例已被回收。
- 無活躍引用?:該類的 Class 對象(如 MyClass.class)沒有被任何地方強引用(例如反射、靜態變量等)。
注意:僅僅垃圾回收類的實例,?不意味著類本身被卸載,類卸載必須滿足上述條件。?
重新創建類:
類卸載后,可以再次重新創建類并使用。但這一行為已經不是簡單的初始化了,而是類從字節碼開始重新走一遍生命周期。
具體為嘗試使用對象時(調用靜態方法,實例化等),會由類加載器重新加載字節碼,可能需要新的類加載器,隨后重新觸發 clinit 方法,重新初始化類。
所以,重新加載是類卸載后的新生命周期開始。
public class Test {public static void main(String[] args) throws Exception {// 使用自定義類加載器加載類ClassLoader loader = new CustomClassLoader();Class<?> clazz = loader.loadClass("MyClass");// 觸發初始化clazz.getMethod("init").invoke(null);// 清除引用,觸發卸載clazz = null;loader = null;// 強制觸發GC(僅示例,實際生產環境慎用)System.gc();Thread.sleep(1000);// 再次加載(需要新的ClassLoader)loader = new CustomClassLoader();clazz = loader.loadClass("MyClass");// 會再次觸發初始化clazz.getMethod("init").invoke(null);}
}class CustomClassLoader extends ClassLoader {// 實現加載類的邏輯(例如從字節碼文件讀取)
}
5:枚舉
在Java中,枚舉的本質是一個?繼承自 java.lang.Enum 的類,其成員變量(枚舉實例)會被隱式聲明為 public static final,且由 ?JVM 保證全局唯一性?。
枚舉的加載遵循 Java 類加載機制,實例會在 cinit 方法中被靜態初始化。JVM會保證cinit的線程安全,無需額外同步機制。
枚舉的構造器是私有的(由 JVM 強制限制),通過反射調用 Constructor.newInstance() 時會拋出 IllegalArgumentException。
其內部實例也是單例的,跟隨初始化創建。
2:類加載器
JVM中有三類核心類加載器,形成?雙親委派模型?的層次結構。提供三個類加載器的原因是單一職責,分別負責不同的區域。按照由大到小的順序。
| 類加載器 | 描述 |
|---|---|
| ?Bootstrap ClassLoader(啟動類加載器) | 由C++實現,是JVM的一部分,無法直接訪問。 加載JAVA_HOME/lib目錄的核心類庫(如rt.jar)。 處于類加載器層次頂端,無父加載器。 |
| Extension ClassLoader(擴展類加載器) | 在Java中實現,Java類sun.misc.Launcher$ExtClassLoader。 加載JAVA_HOME/lib/ext目錄或java.ext.dirs系統變量指定的類庫。 父加載器?為 Bootstrap ClassLoader。 |
| ?Application ClassLoader(應用程序類加載器) | 在Java中實現,Java類sun.misc.Launcher$AppClassLoader。 加載用戶類路徑(ClassPath)下的類,主要加載我們寫的類。 父加載器?為 Extension ClassLoader。 |
| 自定義類加載器 | 自定義路徑,父加載器?為?Application ClassLoader。 |
其中,啟動類加載器為最頂級加載器,如果嘗試通過 getClassLoader() 獲取類加載器時,會打印null。
因為啟動類加載器由JVM內部的C++代碼實現,沒有對應的 ClassLoader 對象,因此返回null,并且這也是Java設計的一種規范,以此作為啟動類加載器標識。
其他的類加載器可以返回Java實例,但注意擴展類加載器的getParent()也會返回null,同上。
類的命名空間:由類加載器+包名+類名共同確定唯一類。
不同類加載器加載的同一個類,JVM視為不同類。
1:自定義類加載器
如果有特殊場景需求時,例如需要加載非classpath中的路徑文件、想通過接口來實現、同時加載相同的類時,則可以考慮創建并使用自定義類加載器。
步驟:繼承ClassLoad類;并重寫findClass()方法,讀取類字節碼;調用defineClass()生成Class對象(遵循雙親委派機制);在使用時通過該自定義類加載器 loadClass 方法獲取類對象。
public class CustomClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] bytes = loadClassFromDisk(name); // 自定義加載邏輯return defineClass(name, bytes, 0, bytes.length);}
}
自定義類加載器由JVM垃圾回收管理,并且其加載的類在一定條件下會被JVM卸載。
自定義類加載器需謹慎管理生命周期,避免內存泄漏(如Metaspace/PermGen溢出)。
2:雙親委派模型
首先術語,雙親是指的appClassLoader的上面兩個父類,所以這樣描述。
雙親委派模型是指:類加載請求會首先委派給父加載器,并且一層層往上,父加載器無法完成時,子加載器才嘗試加載。
優點:
唯一性:保證類的一致性,同一個類由同一個類加載加載。
安全性:避免重復加載,確保核心類安全(如String類),防止開發者對Java程序類進行篡改。

一般我們說的都是Java 8的雙親委派,其實在Java 9的雙親委派有一些變化,做了相應的優化。
JDK 9引入了模塊化概念,大體就是將不同的包指定為一個模塊,然后指定該模塊的類加載器。當需要加載類時,委派到平臺類加載器,會將其直接派給指定的類加載器處理。
會避免無用的委派,優化了類加載性能,提高程序啟動速度。
打破雙親委派模型:
有些場景可能需要由子加載器優先加載,不遵循雙親委派機制,該行為稱為打破雙親委派。
常見的有:
Tomcat:Web服務器,需要保證每個Web應用使用獨立的類加載器(加載各自獨立的類,即使同名),加載WEB-INF下的類。
SPI(Service Provider Interface)?:如JDBC驅動加載,使用線程上下文類加載器(Thread Context ClassLoader)加載廠商實現。
?OSGi模塊化?:每個Bundle有自己的類加載器,形成網狀依賴關系。
線程上下文類加載器:
也屬于打破雙親委派模型,其作用是解決父加載器需訪問子加載器資源的場景(如JDBC加載第三方驅動)。
在線程啟動時,會默認把應用程序類加載器放入線程加載器,使用getContentClassLoader可獲取當前線程類加載器。
可通過 Thread.currentThread().setContextClassLoader(),設置線程類加載器。
使用?ServiceLoader.load() 通過上下文類加載器加載服務實現。
3:類加載器源碼
在 loadClass的源碼中,通過方法 findLoaderClass 判斷是否加載過,如果有則從緩存中直接取,不會重新加載。
如果這個類沒被加載過,返回null。則標識需要加載類,此時會判斷一個parent對象,就是我們的類加載器。
如果parent對象不為空,則遞歸調用 loadClass 方法,遞歸時也會判斷各個類加載是否加載過。直到類加載器為頂級bootstrap時,parent才會為空,并進入方法嘗試獲取加載類(緩存)。
如果頂級加載器找不到類,會向下繼續找,如果所有類加載器都找不到類,會拋出找不到類異常。
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) {long t0 = System.nanoTime();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 (resolve) {resolveClass(c); //如果所有類加載器都無法加載類,會拋出找不到類異常}return c;}
}
三:運行時數據區
運行時數據區主要由五大部分組成:方法區、堆、虛擬機棧、本地方法棧、程序計數器,講解時也可以按照這個順序。

這五個部分:方法區、堆、棧會涉及內存溢出;堆是垃圾回收的主要區域,方法區的回收條件嚴格且效率低,虛擬機找的內存管理不依賴于垃圾回收,而是通過棧幀的彈出釋放內存。
棧內存的釋放是確定性的?(方法結束即釋放),而堆和方法區的回收是非確定性的(由 GC 策略決定)。
另外,方法區和堆是線程共享的,所以堆內存的共享變量要考慮線程安全問題。
1:方法區
方法區是一個規范,用于存儲?類信息、常量、靜態變量、即時編譯器(JIT)編譯后的代碼?等數據。它是所有線程共享的內存區域,生命周期與 JVM 一致。
注意方法區作為規范只定義了上述邏輯概念,沒有規定具體實現方法,不同的JVM可以用不同的數據結構實現(如Open J9),我們常說的就是 HotSpot,所以在描述方法區時,一定要指定JVM。
1:內存結構

方法區是一個規范,在Hotspot虛擬機中,Java 8對其方法區的實現做了調整和優化。
1.7及之前:
方法區的實現是永久代(PermGen),其位于堆內存中,大小固定,可以通過參數 -XX:PermSize 和 -XX:MaxPermSize 配置。
缺點:受固定內存限制,靈活性不高;易因類加載過多或常量池過大導致 PermGen OOM。
1.8及以后:
1.8修改為元空間(Metaspace)實現,元空間使用本地內存,默認不限制大小,但會受操作系統物理內存限制。
優勢為:降低 OOM 風險,內存分配更靈活;沒有永久代的內存焦慮問題,可設置元空間內存上限,避免耗盡系統內存;根據類加載的需求動態擴展,減少內存浪費。
2:核心作用
方法區的特點是線程安全的。當方法區無法滿足內存分配需求時,會拋出OutOfMemoryError內存溢出。Java 8之后,使用系統內存很難溢出,可以通過設置較小參數,來模擬類加載過多OOM的場景。
1:類元數據存儲
在上面解釋類加載過程時提過,Hotspot 方法區底層就是使用 instanceKlass 存儲類的元數據(類名、字段、方法、類加載器等)。
與堆中的 Class 對象相互關聯,并直接分配在本地內存中,而非堆內存。
2:靜態變量
類級別的變量(static 修飾),直接存儲在方法區中。注意如果是靜態引用對象,則方法區僅存儲引用地址,實際對象實例存在堆內存中。
3:JIT熱點代碼
JIT 編譯后的熱點代碼會存儲在方法區的“代碼緩存”(Code Cache)中?。這里簡單描述下,具體可以在執行引擎-JIT詳細描述(觸發條件)。
代碼緩存是 JVM 為存儲?即時編譯器(JIT)生成的本地機器碼?而預留的內存區域,會由JVM單獨分配,通常也會在本地內存。
4:垃圾回收
方法區也有垃圾回收機制,但是回收條件較為嚴格,需滿足類卸載條件(引出類加載機制)。主要用于回收廢棄常量和無用的類,不可過度依賴,容易發生內存泄漏和內存溢出問題。
這里可能會有問題:描述下方法區的?OutOfMemoryError原因和處理?
原因:
- 未合理配置元空間大小。
- 項目中可能存在大量動態生成的類(如 CGLib、反射、JSP)。
- 類加載器未卸載(如頻繁熱部署應用),或創建大量自定義類加載器。
處理:
- 調整 -XX:MaxMetaspaceSize,元空間內存大小。如果設置默認需考慮物理內存壓力。
- 減少動態類生成(如緩存代理對象),以及減少反射動態獲取,考慮緩存反射類等。
- 檢查代碼,去除或減少無用的類加載器,無用的類等,避免內存泄漏。
3:核心參數
描述針對于方法區的參數設置,以及元空間垃圾回收和擴容流程。
| 元空間參數 | 描述 |
|---|---|
| -XX:MetaspaceSize | 初始的高水位線,注意不是初始化這么大!初始分配的內存可能較小,然后根據需要動態調整。 當元空間使用量達到此值時,觸發Full GC嘗試回收無用元數據。若回收后仍不足,則擴容。 |
| -XX:MaxMetaspaceSize | 元空間的最大上限,默認無限制(受限于系統內存)。建議生產環境設置此值,防止內存耗盡。 如果達到了MaxMetaspaceSize的限制,就無法繼續擴容,導致OOM錯誤。 |
| -XX:MinMetaspaceFreeRatio | 觸發元空間擴容的最小空閑比例,默認 40%(注意設置值時無需百分號)。 |
| -XX:MaxMetaspaceFreeRatio | 觸發元空間縮容的最大空閑比例,默認 70%(內存退回給操作系統)。 |
通過參數可以控制元空間的垃圾回收頻率,以及擴容的觸發時機。
1:擴容觸發
擴容針對的是設置了初始初始高水位線的場景,不設置默認為系統最大內存,生產建議設置避免宕機。
擴容機制:元空間由多個內存塊組成,每個塊分配給特定的類加載器。當加載新類時,JVM從當前塊分配內存。當觸發擴容時,JVM會向操作系統申請新的內存塊,每次擴容的大小由JVM內部策略決定(通常逐步增加)。達到上限時,不會觸發擴容,而是觸發GC。
上述的兩個參數都可以用來控制擴容:初始高水位線、最小空閑比例。
- 最小空閑比例:當元空間的空閑比例低于這個閾值時,會觸發元空間擴容,不會直接觸發GC。只會在擴容失敗時(達到最大上限),才會觸發GC嘗試回收數據。
- 初始高水位線:當元空間使用量達到該值時,觸發Full GC嘗試回收元數據,若GC后內存還不足,則擴容。
- 當兩個參數同時設置了,且條件同時滿足時。JVM會優先處理【最小空閑比例】的擴容需求,擴容成功后直接分配,擴容失敗則GC。后續會檢查【初始高水位線】,在首次使用量達到時,如果上個參數已擴容,則此時不會觸發GC,而是將高水位線自動更新為擴容后的容量。如果上個參數未擴容,則按原邏輯觸發GC再嘗試擴容。
所以,兩個參數對于擴容和GC順序不同,不會產生沖突?,能夠協同確保內存分配的效率與穩定性,從性能方面看,肯定是先擴容好,因為GC成本太高。
優化初始值:避免初始值過小導致頻繁 GC。
優化最小空閑比例:如果元空間增長過快,降低該值以減少擴容頻率;如果內存充足,提高該值以保留更多空閑內存,減少GC風險。
對于縮容,元空間將內存釋放給操作系統的條件較嚴格,通常需滿足最大空閑比例,且依賴不同的JVM實現。
2:垃圾回收觸發
如果元空間的使用達到了設置的最大閾值,分配新內存失敗時,會觸發Full GC,回收方法區不再使用的元數據,清除滿足類卸載條件的數據,條件較為嚴格,無用類無法被回收即為內存泄漏。
Full GC:表示全局垃圾回收,暫停所有線程STW。這里的Full GC與堆內存的Full GC是同一個過程,只是觸發的條件不同。都是由垃圾回收器執行。
因此,元空間觸發的Full GC實際上會觸發整個堆的回收,而堆觸發的Full GC同樣可能影響元空間。
如果垃圾回收后內存還是不夠,則會拋出內存溢出,程序終止。
4:運行時常量池
運行時常量池其實也是方法區的核心部分,比較重要,這里單獨描述。在描述之前,需要先理解三個術語以及他們的關系。
1:常量池
常量池指的是Class文件結構里的一部分,里面存放了各種類編譯時期生成的各種字面量和符號引用,如類接口名、字段、方法等信息。
這部分信息是在編譯時生成的,存在于Class文件中。每個類都有自己的常量池。
2:運行時常量池
運行時常量池是?方法區的一部分?,每個類或接口在JVM中加載后,其Class文件中的常量池會被解析并加載到運行時常量池。其跟隨Hotspot方法區實現,1.8之前在永久代,1.8及之后在元空間。
運行時常量池創建發生在類的加載階段,符號引用替換發生在解析階段。
每個類或接口都有自己的運行時常量池(同Class常量池)。
在加載時,運行時常量池存的是符號引用,在解析階段,會替換為字符串常量池的引用。
兩者關系:Class文件中的常量池是運行時常量池的“靜態快照”,運行時常量池是其在JVM中的運行時形態。
3:字符串常量池
可稱為String Pool / String Table,字符串常量池是JVM中?全局共享的字符串緩存池?,用于存儲字符串對象的引用(避免重復創建相同字符串)。
在Java中,字符串是不可變的,所以JVM為了優化,會有一個全局的字符串常量池。當用雙引號直接創建字符串時,JVM會檢查字符串常量池中是否存在該字符串,如果有就直接返回引用,否則創建并放入池中。
在Java 7之前位于永久代(方法區),Java 7及之后被移到堆內存(Heap)。
字符串常量池是一個JVM內部實現的哈希表結構(不是集合框架中的),在Java7存放在永久代時,大小固定不可擴容,易導致內存問題。Java 7及之后移動到堆內存后,大小可通過 JVM 參數調整?(例如 -XX:StringTableSize=N,默認 60013)。
字符串常量池是惰性加載,按需加載即用到時才加載。實際字符串對象的創建和入池操作發生在?首次主動使用該字面量時?(如賦值、方法調用)。
如果字符串相加的值在編譯期能夠確認,則會進行編譯期優化,從串池中獲取。所以有些面試題會考察字符串相加判斷,根據其是否從串池獲取,以及存儲位置是否一樣判斷。
與運行時常量池關系:運行時常量池可能包含字符串常量池中的符號引用。
比如在類加載階段,Class創建并加載到運行時常量池后。
隨后會在解析符號階段,查詢字符串常量池,如果存在則引用指向已有的對象,否則創建新的字符串對象并放入字符串常量池。
所以運行時常量池中的字符串實際上是引用到字符串常量池中的對象。
5:String.intern()
String提供的一個方法,調用 intern() 時,JVM 會檢查字符串常量池(StringTable)中是否存在與當前字符串內容相同的對象,如果有則直接返回;如果不存在,會將當前字符串對象的引用添加到字符串常量池?,并返回該引用。
intern() 直接操作的是 ?字符串常量池(StringTable)?,而非運行時常量池。運行時常量池中的符號引用在類加載時已被解析為字符串常量池中的對象引用,不會修改其內容。
伴隨著Java版本對方法區的調整,intern方法對串池的處理也不同(妥協),所以經常會有字符串比對的相關問題。
Java 6:
如果一個堆內存字符串對象,調用intern方法,會判斷字符串常量池是否存在。
如果不存在,則會將堆中的字符串內容拷貝到永久代?,生成一個新的字符串對象,加入池中,并返回永久代的引用。如果存在,則直接返回永久代的引用。
結果:堆中的對象和池中的對象是?兩個獨立的對象?,地址不同。會導致==判斷失敗。
String s1 = new String("abc"); // 堆中對象
String s2 = s1.intern(); // 永久代對象(拷貝生成)
System.out.println(s1 == s2); // false(地址不同)
Java 7+:
從 Java 7 開始,字符串常量池被移至堆內存,與普通對象共存。
所以對象調用intern方法時,如果不存在,則會將將堆中該字符串對象的引用直接加入池中(無需拷貝),如果存在,則直接返回池中的引用。
結果:池中存儲的是?堆中對象的引用?,地址相同。
String s1 = new String("abc"); // 堆中對象
String s2 = s1.intern(); // 池中引用指向堆中的 s1 對象
System.out.println(s1 == s2); // true(地址相同)
但是不能確保一定返回堆中的引用,具體要看 intern 執行的時機。
如果字符串常量池中已有相同內容的字符串(例如通過字面量 "abc" 提前加載),則 intern() 直接返回池中引用,與調用時機無關。此時再和堆內存對象判斷地址,會比較不成功。
String s0 = "abc"; // 池中已加載 "abc"
String s1 = new String("abc");
String s2 = s1.intern();
System.out.println(s2 == s0); // true(s2 指向池中已有的 "abc")
System.out.println(s1 == s2); // false(s1 是堆中的新對象)
equals() 與 == 的區別?:
這里就更加清晰兩者的區別了,很經典的面試題,以及建議字符串用equlse判斷值的原因。
無論 Java 6 還是 Java 7+,只要字符串內容相同,equals() 始終會返回 true(判斷內容相等)。
但 == 的結果取決于對象地址:
Java 6 中,intern() 后的對象地址與堆對象地址不同(拷貝到永久代),會比較失敗。
Java 7+ 中,intern() 后的對象地址可能與堆對象地址相同(引用復用),會比對成功,但是需要注意 intern 方法的執行時機(串池中沒有提前字面量加載)。
2:堆內存
堆內存是JVM管理的內存區域中最重要的一部分,用于存儲對象實例和數組。它是所有線程共享的內存空間,也是垃圾回收的主要區域。
堆內存存儲的數據:
- new 創建的對象和數組。
- 存儲類級別的靜態實例對象,存儲實例變量的值(基本類型)或堆對象引用。
- Java 7及之后存儲字符串常量池,作為全局字符串緩沖池。
- 存儲線程私有的分配緩沖區,JVM 為每個線程在堆內存中分配一小塊私有區域(TLAB),用于快速分配對象,避免多線程競爭。
- 存儲逃逸分析優化失敗的對象,JVM 的?逃逸分析?會嘗試將未逃逸的對象分配在棧上(棧上分配),但若分析失敗或未啟用優化,對象仍分配在堆中(棧章節中詳細描述)。
- 垃圾回收相關的元數據,如標記信息,分代年齡等數據。
靜態變量不在堆中的原因:
靜態變量屬于類,由方法區管理,與堆內存隔離。這種設計避免了靜態變量被垃圾回收(除非類卸載),同時減少堆內存壓力。
1:內存結構
為了優化垃圾回收效率,Java將堆內存劃分為不同區域,稱之為不同的代,且隨著JDK版本做了相關優化。?

上圖是Java 8堆內存詳細的劃分,在之前還會存在一個永久代內存區域。所以堆內存最大的改動就是在Java 8對方法區的實現。
新生代:
新生代用于存放新創建的對象,其中?又分為了 Eden 區?和??Survivor 區?,存儲不同時期對象。新生代垃圾回收稱為 Minor GC,理解為代價小一點的GC。
Eden區:對象首次分配的區域。在Minor GC時會被清空,用于分配新對象。
Survivor區?:存儲并處理垃圾回收時的對象,其中又分為 Survivor From 和 Survivor To 區。進入From區和To區的對象,會在兩者之間移動,不會回到Eden區。直到年齡足夠晉升到老年代,或被回收。
這里涉及到垃圾回收算法的標記整理,用來清除無用對象,保留存活對象(具體可以在GC篇描述)。
老年代:
用來存放長期存活的對象,如直接晉升老年代或經過多次 Minor GC 后依然存活的晉升對象。老年代的垃圾回收稱為 Migor GC,Full GC表示全局垃圾回收,一般會認為是老年代垃圾回收導致的全局回收,會造成STD,速度較慢,代價比較大(具體可以在GC篇詳細描述)。
老年代就是一塊完整的內存區域,沒有更具體的區域劃分(Java 8)。
永久代:
這里就能理解為什么Java 8之前的方法區叫永久代,因為1.8版本前方法區實現在堆中,所以也遵循了堆中的分代規則,起了永久代的名字,并劃分一塊內存區域,內存固定容易造成OOM。
1.8及之后版本,將永久代從堆內存移除,改為元空間使用本地內存,避免了永久代導致的內存溢出問題。
Java 9:
回答問題時可以提一下,在Java 9中,G1默認稱為垃圾回收器,G1 將堆劃分為多個大小相等的 ?Region?(區域),不再嚴格物理分代。
邏輯上我們還會稱為分代,但物理上是動態 Region 分配。其優勢是提供更可控的停頓時間,適合大內存應用。
2:核心參數
通過 JVM 參數可以配置堆內存的大小和分代比例,從而進行我們常說的JVM調優和GC調優,以及內存溢出等相關問題的故障排查。
堆內存的相關參數大致分為四個部分:堆內存基礎參數、分代內存參數、高級調優參數。
使用的話就是加在程序的啟動參數中,中途加入不會生效,程序需重啟。
堆內存基礎設置:
-Xms:初始堆大小,建議與?-Xmx?相同,避免堆動態擴容導致的性能波動。
-Xmx:最大堆大小,堆內存上限,超過會觸發 OutOfMemoryError。
-XX:+UseCompressedOops:啟用壓縮指針,默認為開啟,可優化對象頭大小,節省內存。替換為減號即為關閉。
使用以上參數可簡單設置堆內存大小,結合本地內存容量,減少內存不足的問題,提高系統穩定性。
-Xmx 至少為系統可用內存的 1/4,但不超過 80%,避免系統崩潰。
分代內存參數(年輕代/老年代):
| 分代參數 | 描述 |
|---|---|
| -XX:NewSize | 年輕代初始大小,設置年輕代的初始值。 |
| -XX:MaxNewSize | 年輕代最大大小,結合堆初始/最大內存使用。 |
| -XX:OldSize | 老年代初始大小,結合堆初始/最大內存使用。 |
| -XX:NewRatio | 老年代與年輕代的比例,默認值 JDK8 為 2。 使用:-XX:NewRatio=3,表示老年代:年輕代=3:1。 |
| -XX:SurvivorRatio | Eden 區與 Survivor 區的比例,默認為8。 使用:-XX:SurvivorRatio=8,表示Eden區和survivor區(From和To區)比例8:1:1。 |
| -XX:MaxTenuringThreshold | 對象晉升老年代的年齡閾值,默認15,設置0表示直接進入年代。 使用:-XX:MaxTenuringThreshold=15。 |
一般情況下,直接增大堆內存可以簡單快速的解決內存問題,但如果有內存緊缺的場景,例如項目體量過大、機器本地內存限制等,則只能最大程度的分配堆內存中的分代內存。
新生代內存不足,頻繁觸發垃圾回收時,可考慮提高新生代大小,或減小晉升閾值,讓對象進入老年代。
老年代內存不足,可考慮提高老年代比例。或提高晉升閾值,使對象保留在新生代,但要注意可能需要提高Survivor區的比例,因為進入Survivor區的對象不會回到Eden區。
高級調優參數:
| 高級參數 | 描述 |
|---|---|
| -XX:+UseTLAB | 啟用線程本地分配緩沖(默認開啟),可加速對象分配,減少競爭。 |
| -XX:TLABSize | 設置 TLAB 大小(每個線程在堆內存分配的一小塊區域),使用時根據線程數調整優化。 |
| -XX:+AlwaysPreTouch | 啟動時預分配物理內存,避免運行時內存分配延遲,但會延長啟動時間。 |
| -XX:+EliminateAllocations | 啟用逃逸分析優化,自動將未逃逸對象分配在棧上(默認開啟)。 |
生產環境中,一般是多個參數結合使用,根據場景,選擇構建高吞吐量、低延遲、大內存的的程序,提高系統性能,減少內存問題。
3:內存溢出
當發生內存泄漏或堆內存不足時(設置較小或對象較多),會觸發垃圾回收機制,當回收后內存還不夠時,就會拋出內存溢出OutOfMemoryError(OOM),程序終止。
堆內存時垃圾回收的主要區域,一般也稱Full GC為堆內存不足,雖然方法區也可引發Full GC,但和堆相比概率較低。
常見的堆內存溢出原因有:內存泄漏、堆內存設置較小。
優化策略是:排查代碼有沒有內存泄漏問題;設置較大堆內存;優化對象生命周期,調整分代內存結構;使用工具分析堆內存使用情況(VisualVM / JConsole/圖形化查看堆內存使用)。
內存泄漏和內存溢出區別:
總結:內存泄漏是程序中的錯誤,導致對象無法被回收,可用內存逐漸減少;而內存溢出是程序需要的內存超過了可用的內存,可能由泄漏引起,也可能是其他原因。
兩者的關系是內存泄漏可能導致內存溢出,但溢出不一定由泄漏引起,也可能是正常的內存不足。
內存泄漏:
對象已經不再被程序使用,但由于錯誤被意外保留在內存中,導致垃圾回收器(GC)無法回收它們。例如:靜態集合長期持有對象引用、ThreadLocal的引用、資源泄漏(連接池或IO未關閉)、類無法卸載、Hash值改變等。
處理:使用工具(如MAT、VisualVM)分析堆轉儲,找到未被回收的對象引用鏈,刪除或優化代碼。注意內存泄漏不是單純加內存能解決的,問題的源頭還是代碼問題。
內存溢出:
申請內存時,JVM的堆或方法區沒有足夠空間分配對象,就會拋出OOM,程序終止。可能的原因為:內存泄漏長期累計、JVM方法區或堆內存設置較小、程序體積或數據量過大等。
處理:檢查是否存在泄漏,設置JVM參數調整,檢查程序邏輯是否處理了大數據場景,修改或優化代碼。
兩者在生產環境都是比較嚴重的問題:內存泄漏是溫水煮青蛙,加內存也只是緩解一時,需要解決代碼的源頭問題,不好排查;內存溢出會使程序終止,可能造成業務操作中斷、用戶數據丟失等,問題很嚴重。
3:虛擬機棧
虛擬機棧是Java虛擬機內存模型運行時的重要部分,用于支持方法調用和執行過程中的數據管理,棧內存是線程私有的,不涉及垃圾回收的區域。
線程私有:每個線程在創建時都會分配一個獨立的虛擬機棧,生命周期與線程相同。
每個棧只能有一個活動棧幀,對應著當前正在執行的那個方法。
1:內存結構

棧幀:每個方法的執行對應一個棧幀的入棧(方法開始)和出棧(方法結束),棧幀中存儲方法的局部變量、操作數棧、動態鏈接、返回地址等信息。
后進先出(LIFO)?:方法調用鏈的棧幀按調用順序壓入和彈出。
棧幀內包含幾個重要部分,主要用來存儲局部變量,以及對數據進行操作。
全局變量需要考慮線程安全。看一個棧幀內的變量是否線程安全,可以看其是否被其他方法引用,或當作返回值被引用(逃離方法)。
局部變量表:存儲方法參數和局部變量,包括基本數據類型、對象引用和返回地址。以?變量槽(Slot)?為最小單位,32位類型(如int)占1個槽,64位類型(如long、double)占2個槽,對象通常占用一個槽,具體由JVM實現。局部變量表大小在編譯期確定,不會在運行時改變。
操作數棧:執行字節碼指令時的工作區,用于存放計算中間結果和調用參數。提供給編譯器通過指令獲取計算。操作數棧大小也是編譯時確定,寫入方法的Code屬性中。
動態鏈接:將符號引用轉換為直接引用(方法在內存中的實際地址)。每個棧幀持有指向運行時常量池的引用,支持動態綁定。
方法返回地址:記錄方法正常退出或異常中斷后應返回的位置。
其他還有例如多線程中,存儲對象頭進行鎖獲取等。
在方法中發生未捕獲的異常時,棧幀會被彈出,直到找到合適的異常處理器。可能涉及多個棧幀的銷毀,同時異常信息會被傳遞到調用者棧幀中。
2:棧內存溢出
當線程請求的棧深度超過虛擬機允許的最大值時,會拋出StackOverflowError,例如無限遞歸,或設置了較小的棧內存。
當線程擴展棧時無法申請足夠內存時,可能會拋出OutOfMemoryError,不過通常棧溢出更常見于StackOverflowError。
可以通過-Xss設置棧大小(如-Xss1M),默認值依賴JVM實現和操作系統。
異常拋出時,JVM通過棧幀中的信息生成堆棧軌跡,可使用JConsole、VisualVM或jstack命令查看線程棧信息。
4:程序計數器
程序計數器是用來保存當前線程?正在執行的字節碼指令地址,是線程私有的,且唯一不會內存溢出的區。
?線程私有?:每個線程獨立擁有一個程序計數器,互不影響。
部分JVM實現可能將程序計數器映射到CPU的寄存器,以提升執行效率。
簡單來說就是記住下一條JVM指令的執行地址,提供并指導執行引擎完成計算。是虛擬機棧和執行引擎之間的橋梁。
5:本地方法棧
Java虛擬機用于管理本地方法的調用,而本地方法棧就用來管理本地方法的調用,即代碼中使用native修飾的方法,底層可能是C或C++編寫的本地接口。
需注意不是所有的JVM都支持本地方法,本地方法棧也是線程私有的。
常見的有例如Object類的clone方法,就是調用本地方法。
對一個運行中的Java程序而言,當其某個線程調用本地方法時,它就進入了一個全新的不受虛擬機管理的模塊,本地方法可以通過本地方法接口來訪問虛擬機中的運行時數據區,以及其他數據。
四:執行引擎
執行引擎時負責將JVM中字節碼,轉換為底層操作系統能夠執行的機器指令,并實際運行程序。
執行引擎主要有三個功能點:解釋器、即時編譯器、垃圾回收器。
1:解釋器
逐條讀取并解釋字節碼指令,將字節碼解釋為機器碼,即使下次讀取是相同的,仍會重復解釋。
優點是啟動速度快,內存占用較低;缺點是執行效率較低,每次運行會重復解釋。
2:即時編譯器
一般稱為JIT,其功能是將熱點代碼編譯為本地機器碼,后續執行直接運行機器碼。
優點是顯著提升長期運行程序的性能(如服務器應用)。
1:編譯器實現
- ?C1編譯器(Client Compiler)?:輕量級編譯器,優化啟動速度,適用于客戶端程序。
- ?C2編譯器(Server Compiler)?:深度優化編譯器,犧牲編譯時間換取更高的執行效率。
- ?分層編譯(Tiered Compilation)?:JDK 7+默認啟用,結合C1和C2的優勢,先由C1編譯,再對熱點代碼由C2深度優化。
熱點代碼探測:
可通過-XX:CompileThreshold(默認 10000),當方法調用次數超過閾值時會觸發編譯。
此時JIT 編譯器(如 C1/C2)將字節碼編譯為機器碼,存入代碼緩存。
編譯優化策略:
方法內聯:將小方法調用替換為方法體代碼,減少調用開銷,例如A(1*1),并可結合常量折疊進一步優化。
逃逸分析:判斷對象是否逃逸出方法或線程,決定是否進行棧上分配或鎖消除。
2:代碼緩存
代碼緩存是 JVM 為存儲?即時編譯器(JIT)生成的本地機器碼?而預留的內存區域。
在 HotSpot 中,代碼緩存是方法區的一部分,由JVM單獨分配,通常也在本地內存。
可通過 -XX:ReservedCodeCacheSize 和 -XX:InitialCodeCacheSize 參數配置大小。
當代碼緩存占滿時,JIT 編譯會停止,導致程序退化為解釋執行(性能下降)。可通過添加參數?-XX:+UseCodeCacheFlushing?允許在緩存不足時回收無用代碼(如已卸載類的方法代碼)。
后續調用可直接執行代碼緩存中的機器碼,大幅提升性能。
3:其他描述
JVM基于棧?:字節碼指令通過操作數棧進行運算,指令更緊湊,跨平臺性好,但效率略低。
物理CPU基于寄存器?:直接操作寄存器,效率更高,但依賴硬件架構,不符合Java跨平臺運行。
垃圾回收器屬于內存管理模塊,但執行引擎需要與GC協作,在安全點(Safepoint)暫停線程以執行垃圾回收。
Graal編譯器?:JDK 10+引入的可插拔編譯器,支持更多激進優化,未來可能替代C2。
?AOT編譯:JDK 9+通過jaotc工具將字節碼預先編譯為機器碼,減少啟動時間。
五:垃圾回收
JVM的垃圾回收是自動管理內存的核心機制,負責回收不再使用的對象以釋放內存,避免內存泄漏。吸取了C++的經驗,改為自動管理,避免手動管理垃圾回收導致的一些問題。
主要從幾個方面描述:回收條件、回收算法、垃圾回收器、回收操作。
1:回收條件
回收算法的目的是判斷對象是否可進行回收,并自動進行垃圾回收,釋放資源,節省內存空間。
1:引用計數法
引用計數法并未在Java中使用,而是作為一個概念理解,在其他語言中有使用到。
原理:每個對象維護一個計數器,記錄被引用的次數。當引用計數歸零時,對象被回收。
缺點:兩個對象互相引用,引用計數無法歸零,導致內存泄漏;另外需要頻繁更新計數器,并且在多線程環境下還需要考慮線程安全問題,造成較大性能開銷。
2:可達性分析算法
Java中使用的是根搜索算法,也稱為可達性分析算法。
原理:從GC Roots出發,遍歷對象引用鏈。若對象不可達(例如將屬性或對象賦值為null),則標記為可回收。
GC Root主要有四種對象:線程棧幀中的局部變量或參數、引用類型靜態變量、字符串常量池引用、同步鎖持有的對象。
在可達和不可達的狀態中間,還有一個可復活狀態:對象被標記為可回收,但在finalize()方法中重新與引用鏈關聯。
3:引用類型
JVM提供了四種引用類型,用來在發生垃圾回收時,判斷特定的對象是否可被回收釋放內存。
| ?強引用 | 平時用到的所有引用都屬于強引用,例如new對象,對象間賦值等。 對象只要被一個強引用關聯,GC 永遠不會回收。強引用鏈斷裂時可被回收。 |
| ?軟引用(Soft) | 適用于內存敏感型緩存,通過 SoftReference 類包裝使用。 當沒有強引用直接引用時,且即使發生垃圾回收內存也不夠時,回收軟引用對象。 |
| 弱引用(Weak) | 適用于臨時緩存、監聽器注冊,通過WeakReference 類包裝使用。 當沒有強引用直接引用時,無論內存是否充足,發生GC就會被回收。 |
| ?虛引用(Phantom) | 適用于精準控制資源釋放,必須與 ReferenceQueue 結合使用,當對象被回收時,虛引用會被加入隊列。 虛引用對象無法通過 get() 獲取實際對象,僅用于跟蹤對象被回收的時機。 |
軟弱引用還可以搭配隊列使用,因為其本身也是對象,會占用空間。使用隊列后,當軟弱引用的對象被回收后,會進入引用隊列進一步處理。
// 創建軟引用對象
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
// 嘗試獲取對象
byte[] data = softRef.get(); // 若內存充足,返回字節數組;若內存不足,返回 null// 創建弱引用對象
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 手動觸發 GC(僅示例,實際開發中避免調用 System.gc())
System.gc();
// 檢查對象是否存活
if (weakRef.get() == null) {System.out.println("對象已被回收");
}// 創建引用隊列
ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 創建虛引用對象
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// 檢查對象是否被回收
Reference<?> ref = queue.poll(); // 若對象被回收,返回虛引用對象
if (ref != null) {System.out.println("對象已被回收,可執行清理操作");
}
2:回收算法
堆內存是垃圾回收的主要區域,JVM提供了不同的算法用于堆內存的垃圾回收。
為什么標記存活對象,而不是標記失效對象?
- 效率問題?:垃圾回收的目的是回收內存,而 JVM 中大部分對象是“朝生夕死”的。直接標記存活對象可以避免遍歷大量無效對象,提高效率。
- 安全性問題?:如果誤刪存活對象會導致程序崩潰,因此必須明確標記存活對象后再清理未標記的部分。
注意:對于標記對象,網上有的說是標記失效對象,這個一定要明確是標記存活對象。在《深入理解Java虛擬機》的書中,確實描述標記需要回收的對象,但實際實現是標記存活對象反向推導的。
1:標記清除
分為兩個步驟,核心思想是通過“標記存活對象”和“清除未標記對象”兩個階段來回收內存。
1:沿GC Root引用鏈,標記所有存活對象。若需并發標記(如CMS收集器),需通過“三色標記”等機制處理應用線程與GC線程的并發修改。
2:線性遍歷整個堆內存,識別未標記的對象。將未標記對象占用的內存標記為空閑,供后續分配使用。
優點是速度快,只需做一個標記;且實現邏輯簡單,無需移動對象,適合處理存活率高的老年代對象。
缺點是清除后會產生大量不連續的內存碎片,可能導致大對象分配失敗;會造成兩次停頓,降低響應時間;清除階段需遍歷整個堆,時間復雜度為O(n)。
2:標記整理
是對于標記清除算法,產生內存碎片的優化方案。
其步驟為標記存活對象后,將其移動到內存一端,清除邊界外的空間。解決了內存碎片問題。
缺點是效率低、速度慢,整理需要移動對象造成開銷,且對象中的局部/引用變量也需要改變引用地址。
同標記清除,適用于老年代,對象存活率高的場景。
3:復制算法
將內存分為兩塊區域,稱為From區和To區,To區域始終保持空區域狀態。核心思想是通過空間劃分?和對象復制?來高效管理內存,避免內存碎片化問題。
步驟:從GC Root根對象觸發,標記所有可達對象。將From區的可達對象按順序復制到To區,隨后直接清空From區的所有對象。最后From區和To區互換角色,為下一次回收做準備。
優點:由于保持順序分配,所以沒有內存碎片問題。
缺點:傳統復制算法需要預留 50% 的閑置內存(From 和 To 區各占一半),利用率較低。
4:分代回收機制
上面都是JVM提供的三種回收算法,但實際上虛擬機不會單純使用某種具體的算法,在Hotspot虛擬機中,是采用三種算法結合的方式,稱為分代機制。
具體可以參考堆內存篇結構描述。新生代老年代采用不同算法,新生代使用復制算法并優化了傳統復制算法的雙倍內存問題。老年代采用標記清除或標記整理算法。
具體流程為:
創建對象時,優先分配在 ?Eden 區?;當觸發 Minor GC 時,將 ?Eden + From Survivor 中的存活對象? 標記并復制到 ?To Survivor?(年齡+1),清空無用對象。當存活對象年齡達到閾值(默認 15)后晉升到老年代。
3:垃圾收集器
JVM提供了多種垃圾收集器,適用于不同場景,可以從兩個方面對其進行分類。
1:從設計目標區分
| 單線程型 | 串行執行,如 Serial,僅用于特定場景(小內存或客戶端應用)。 |
| 并行-吞吐量優先型 | 如 Parallel Scavenge,目標是最大化應用運行時間占比(GC時間占比最小化)。 可理解為處理效率高。適用于多線程、注重整體吞吐量的場景。 |
| 并發-低延遲型 | 如 CMS、G1、ZGC,目標是減少單次GC停頓時間(STW時間短)。 最小化單次停頓時間,適用于大堆內存和對延遲敏感的場景(如Web服務)。 |
簡單來說,類似微服務的CAP理論,是性能和響應速度的選擇。
2:根據作用區域和算法,可分為兩種收集器:分代收集器和全堆收集器。
垃圾回收器并行:并行指的是多個垃圾回收線程一起執行,不能有用戶線程。
垃圾回收器并發:用戶線程和垃圾回收線程一起執行,能提高響應速度。
1:Serial
Serial是串行的垃圾回收器,屬于分代收集器類型。單線程串行工作,通過 -XX:+UseSerialGC 參數顯式使用。
針對新生代和老年代,新生代采用復制算法,老年代采用標記-整理算法。
GC時觸發STW,暫停所有用戶線程,執行垃圾回收線程。
優點為內存占用低,無多線程開銷。但其只適用于客戶端應用或小內存應用,不適合生產高并發場景。
2:Parallel Scavenge
JDK8默認收集器!
Parallel Scavenge是吞吐量優先的垃圾回收器,屬于分代收集器類型,多線程并行工作,可通過兩個參數開啟。
-XX:+UseParallelGC,表示新生代的垃圾回收器,算法是復制(多線程并行)。
-XX:+UseParallelOldGC,表示老年代的垃圾回收器,算法是標記-整理。
這兩個開關在1.8中是默認開啟的,且只要有一個開啟,另一個也會開啟。
?核心特點是多線程并行回收?,注重最大化吞吐量(單位時間處理請求數)。垃圾回收線程數取決于CPU個數,會執行多個垃圾回收線程,盡快處理。適用于后臺計算密集型任務。
提供了核心參數用于動態調整:
-XX:ParallelGCThreads=4 # 并行GC線程數(默認與CPU核數相同)
-XX:GCTimeRatio=99 # GC時間與總時間占比(1/(1+99)=1%)
-XX:MaxGCPauseMillis=200 # 目標最大GC停頓時間(毫秒)
-XX:GCTimeRatio=99,注意垃圾回收時間不能超過工作總時間的百分之一,否則考慮加大堆內存。并且時間占比參數和最大停頓時間互相沖突,需根據場景具體選擇。
-XX:GCTimeRatio=99,表示吞吐量,堆越大,則吞吐越大(清理的資源多),所需時間會增加。
-XX:MaxGCPauseMillis=ms,表示時間,設置時間越少,則表示堆越小,吞吐量越小,時間縮短。
3:ParNew
parNew是Serial收集器的多線程版本,屬于分代收集器類型,多線程并行工作,通過參數?-XX:+UseParNewGC 啟用。
需注意只針對新生代,使用復制算法,多線程并行處理。
需要和CMS搭配使用,在JDK9后逐漸被G1替代。
4:CMS
CMS(Concurrent Mark-Sweep)是并發低延遲的垃圾回收器,屬于分代收集器,多線程并發執行,通過參數?-XX:+UseConcMarkSweepGC 啟用。
注意:JDK9后標記為廢棄(Deprecated),JDK14中移除。
需注意只針對老年代,使用標記-清除算法。CMS只能與ParNew或Serial配合。
特點是采用?并發標記和清除?,減少STW時間。適用于對延遲敏感的服務(如Web應用)。
缺點是受標記清除算法影響,存在內存碎片問題,可能會觸發Full GC進行壓縮。由于內存是不連續的,所以對象在分配內存時會采用空閑列表方式。
工作流程:
- ?初始標記?(STW):標記GC Roots直接關聯的對象。
- ?并發標記?:遍歷對象圖,無停頓,用戶和垃圾回收線程同時運行。
- ?重新標記?(STW):修正并發標記期間的變動,最后標記,未避免期間再變動,會STW。
- ?并發清除?:回收垃圾對象,無停頓,和用戶線程同時運行。
并發標記階段使用三色標記,通過寫屏障處理并發修改。用戶線程只會在打初始標記時短暫阻塞。
優化參數:
-XX:CMSInitiatingOccupancyFraction=75 # 老年代使用率觸發CMS的閾值(%)
-XX:+UseCMSCompactAtFullCollection # Full GC時壓縮內存
-XX:CMSFullGCsBeforeCompaction=4 # 每4次Full GC后壓縮一次
5:G1
G1(Garbage-First)是同時注重吞吐量、低延遲、超大堆內存的垃圾回收器,屬于全堆收集器(混合收集),多線程并發執行,通過參數?-XX:+UseG1GC 啟用。
G1是在Java 7引入的,Java 8需手動開啟,在JDK9+作為默認收集器。
結合了?標記-整理算法?和?分區回收策略?,旨在平衡?吞吐量?和?停頓時間?,尤其適合現代多核處理器和大內存應用場景。
每個Region的內存較少,且可以單獨回收,所以它可以采用標記整理方式,避免內存碎片。并且內存規整后,對象在 Region 分配內存時,會使用指針碰撞的方式,最大限度使用空間。
核心思想是將堆劃分為多個等大的Region(默認2048個),每個region都可以是以下類型之一,在GC過程中動態變化,無需固定分區比例。
?Eden Region?:存放新對象。
?Survivor Region?:存放存活對象(Minor GC后晉升)。
?Old Region?:存放長期存活對象。
?Humongous Region?:存儲超大對象(大小 ≥ Region的50%)。
可通過 -XX:MaxGCPauseMillis 設定目標最大停頓時間(默認200ms),G1會結合歷史GC數據動態調整策略,動態選擇回收價值最高的Region以達成停頓目標。
1:分階段回收
- Young GC?:回收Eden和Survivor區(類似傳統新生代GC)。
- Mixed GC?:同時回收Young和部分Old Region(減少老年代碎片)。
- ?Full GC?(后備方案):當并發回收速度跟不上對象分配速度時觸發(需避免)。
工作流程:
| 初始標記 | 通常由新生代觸發,在STW階段,標記GC Roots直接關聯的對象。 耗時極短,僅標記根對象。 |
| 并發標記 | 通常發生在老年代,當老年代占用堆空間達到閾值時,進行并發標記。 也是標記存活對象,不會造成STW,不會暫停用戶線程。通過SATB算法處理漏標記(標記期間對象變化)。 |
| 最終標記 | 處理剩余的SATB記錄,修正標記結果(相比CMS處理的更少)。 會STW,耗時較短,取決于并發標記期間的對象變動量。 |
| 篩選回收 | 最終標記完成后,根據停頓目標,選擇回收價值高的Region,將其存活對象復制到空閑Region(復制算法)。 也稱為混合收集,會造成STW,耗時主要取決于存活對象數量和region選擇策略。 |
并發標記階段采用三色標記,結合SATB(Snapshot-At-The-Beginning)算法,通過寫屏障記錄引用變化。?
注意并不是操作所有老年代數據復制,而是為了達成停頓目標,動態選擇效率最高的老年代對象處理。如果對象不多,又可以滿足最大停頓時間,那么它會把所有老年代復制。
SATB:
目的是解決并發標記期間的對象漏標問題。
在初始化時對整個堆建立快照;在并發標記期間,對發生修改的對象記錄到SATB隊列;最終標記階段處理SATB隊列,確保標記完整性。
?Card Table(卡表):
一種?底層數據結構?,用于?粗略跟蹤跨代引用?。早期的跨代引用優化處理。
將堆內存劃分為固定大小的?卡頁?,每個卡頁對應卡表中的一個?條目。當老年代對象引用新生代對象時,標記對應卡頁為?臟頁?(Dirty Card),表示需掃描該區域。
在垃圾回收時,避免全堆掃描,僅掃描臟頁以找到跨代引用。
維護成本較低(僅標記臟頁),但掃描臟頁時需遍歷卡頁內所有對象。
Remembered Set(記憶集):
目的是解決跨Region引用問題(避免全堆掃描)。
一般稱為RSet,是一種?高級抽象結構?,用于?精確記錄跨Region引用?。基于卡表實現,但功能更精確。避免跨Region引用掃描,直接讀取RSet中的引用,掃描效率更高。
所以Rset和卡表是協作關系,Rset在其基礎上做了升級優化。
實現:在G1中,每個Region維護一個RSet,記錄其他Region中指向本Region的?指針位置?。
1:寫屏障。當對象A(Region X)引用對象B(Region Y)時,觸發寫屏障,標記對象A所在卡頁為臟頁。
2:并發優化線程。定期處理臟頁,解析其中的跨Region引用,并將?具體引用地址?記錄到目標Region的RSet中。
3:GC階段使用Rset。回收Region時,直接查詢其RSet,僅掃描相關卡頁中的引用,避免遍歷全堆。
優點:寫屏障的輕量化(僅標記卡表)保證應用線程性能;Refinement線程的并發處理避免GC停頓過長。
場景:
適用于堆內存較大(>=4G,建議6G+),要求低延遲業務場景(頁面,實時系統),以及對象生命周期分布不均勻場景。
不適用于堆內存較小(<2G),堆內存小使用Serial或Parallel更高效;不適用超大堆內存(≥16GB)。
6:ZGC
ZGC(Z Garbage Collector)是并發低延遲的垃圾回收器,屬于全堆收集器,多線程并發執行,通過參數?-XX:+UseZGC 啟用。
在JDK 11引入初始版本,JDK11至JDK14使用需解鎖實驗室選項開啟,JDK15+正式啟用。
ZGC的設計目標是低延遲、高吞吐量、停頓時間與堆大小無關、支持動態調整堆大小。其專為超大堆內存設計,核心理念是全程并發。
算法是染色指針 + 讀屏障。并發階段采用三色標記結合染色指針(Colored Pointers),通過讀屏障處理并發標記。
染色指針:
核心特性之一,在指針中嵌入元數據(?元空間與對象地址分離?)。
通過指針標記對象狀態(存活、可回收等),?無需對象頭?。可快速實現并發標記與轉移,無需STW暫停。
讀屏障:
當應用線程從堆中讀取對象引用時觸發。
檢查指針的染色位,若對象正在被轉移,觸發?自愈?機制,自動更新引用到新地址。以保證應用線程始終訪問有效對象,?無需暫停?。
內存多重映射:
原理是將同一物理內存映射到多個虛擬地址空間(如Marked0和Marked1視圖)。
目的是支持染色指針快速切換標記狀態,無需復制內存。
適用于堆內存 ≥ 8GB(最佳實踐為16GB+)、要求?極致低延遲?(金融、大規模微服務)、堆內存動態變化頻繁等場景。
優點:ZGC通過?染色指針?和?全程并發?設計,在超大堆場景下實現了?亞毫秒級停頓?,是Java應對現代高并發、低延遲需求的終極方案。
缺點:
?內存開銷?:染色指針和元數據占用約3%~5%額外內存。
?CPU開銷?:讀屏障和并發處理需更多CPU資源(建議多核環境)。
7:Shenandoah
Shenandoah(讀音深圳do啊)是并發低延遲垃圾回收器,屬于全堆收集器,多線程并發執行,通過參數?-XX:+UseShenandoahGC 啟用。
算法是并發復制 + 讀屏障?。在JDK12中正式引入,JDK15優化性能。
與ZGC類似,但實現方式不同(無染色指針,依賴Brooks指針)。并發標記與整理均依賴三色標記,使用讀/寫屏障實現高并發。
同樣適合大堆內存、低延遲需求。有兩個關鍵技術。
并發壓縮:在對象存活時移動并更新引用。
Brooks指針?:每個對象頭存儲轉發指針,指向復制后的新地址。
在堆內存 ≥16GB時,可選擇ZGC或Shenandoah(追求亞毫秒級停頓)。如果時 TB 級別的堆內存,優先使用JDK 15版本的ZGC,生產已驗證。
4:垃圾回收
垃圾回收具體處理,及GC調優。
1:Minor GC
新生代(Eden區)空間不足時觸發。
當new一個新對象,默認會放到 Eden 區,Eden區內存不足時,觸發垃圾回收。
不同的垃圾回收器有不同處理,以Java 8的Par為例,第一次GC會標記Eden存活對象,復制到To區,且對象壽命加1。第二次GC會標記Eden區和From區存活對象,復制到To區。對象壽命達到閾值后,晉升至老年代。
頻繁發生Minor GC時,可能是新生代內存設置較小,或有無用對象無法回收。
2:Mijor GC
老年代內存不足時處理,對整個堆進行回收,通常伴隨長時間STW,通常也稱其為Full GC。
以Java 8的Par處理器為例,會使用標記-整理算法清除無用對象,GC后內存還不足時,拋出內存溢出,程序終止。
頻繁Mijor GC時,可能是老年代分配過小,或整個堆內存較小。
問題排查:創建了很多大對象,全部晉升到了老年代;發生了內存泄漏問題;代碼bug等(手動gc)。
3:Full GC
一般情況下指的就是Mijor GC老年代垃圾回收,但不全面,因為方法區也會導致Full GC。
方法區動態加在類過多或內存泄漏,觸發的Full GC和堆內存觸發的是同一個,都是由垃圾回收器執行全面的垃圾回收,包含方法區和堆內存。
通常會伴隨長時間STW,是比較嚴重的問題。不同的垃圾回收器會有不同的優化方案。
4:GC調優
GC調優時,首先檢查導致GC的問題,以及想要實現的目標(高吞吐量或低延遲等)。
調優方法大致有垃圾回收器選型、內存、鎖競爭、CPU占用、響應時間等。
使用 jstat、jmap、jconsole、VisualVM 等工具分析內存和 GC 日志。
1:新生代調優
新生代的對象特點是內存分配廉價,對象操作頻繁,大部分對象是用完就消除。
Minor GC時間短,所以一般調優會從新生代開始。
新生代內存不能太大或太小,取一個中間的合理值。
太小會導致創建對象內存不夠用(頻繁minor gc);太大會導致內存空閑,老年代內存緊張,從而導致Full GC更嚴重。
建議設置 25 < 新生代 < 50 的堆內存總量,足夠對象的日常創建銷毀。
并發場景下,考慮最大并發量時,占用的內存是否會超過新生代內存,如果不超過,則可能不會或較少的觸發垃圾回收。
新生代中的幸存區需要能夠保留當前活躍對象及需要晉升的對象。如果幸存區內存過小,JVM會動態調整晉升閾值,可能會導致部分不活躍對象提前晉升到老年代。
對象提前晉升到老年代后,只有等到Full GC的時候才能被回收,等于是延長了存活短對象的生存周期,從而占用內存空間。
另一方面,有時候可能又需要將一些對象提前晉升老年代,如果有大量存活對象未被晉升,那么會留在幸存區中不停的復制移動,浪費系統性能。
所以,實際中還是根據業務場景,項目體量具體考慮優化方案。
2:老年代調優
一般不涉及老年代調優,理論上是越大越好,取決于系統內存。
如果頻繁Full GC的話,考慮增加老年代內存,或減少對象晉升(搭配新生代),以及排查內存泄漏問題。或考慮使用并發吞吐量或低延遲垃圾回收器。
六:面試問題
結合上述所有JVM結構等知識,進一步擴展的JVM面試相關問題,大致回答清晰即可。
1:對象創建過程
通過 new 創建一個對象,JVM都做了什么?
1:首先會進入類的加載過程,通過類加載器及雙親委派模型,嘗試從緩存中(常量池中)獲取已加載過的類,如果找不到,則重新加載并實例化類,在加載階段分配堆內存空間并生成初始Class對象。
2:獲取到已經加載過的類之后,會在堆內存分配空間。根據垃圾回收機制不同,有兩種內存分配方式:指針碰撞(serial、ParNew、G1)和空閑列表(CMS)。
3:分配內存后,會將對象存入新生代的Eden區,這個過程如果是多線程并發情況下,可能會發生JVM內存的搶占(另一個面試題,可引出)。
4:對象存入Eden區之后,會進入類加載的準備階段,為類對象設置默認值(可引出final修飾)。
5:對象頭設置,對象頭包括GC的分代年齡、鎖升級標識、HashCode。
6:最后觸發類的初始化,執行 cinit 方法,隨后執行 init 方法。init也是自動生成的一個方法,用來對非靜態變量賦值,在cinit之后執行。
2:對象內存分配方式
注意不要講成對象的內存布局,盡量精準回答。
類加載階段,獲取到緩存中的類之后,會在堆中分配內存。根據內存是否規整區分為兩種分配方式。
堆內存是否規整是由垃圾收集器是否帶有壓縮整理功能決定的。
1:內存規整
堆內存規整的情況下,已使用的內存在一邊,未使用內存在一邊。對應標記-整理算法。
該場景下,使用指針碰撞方式分配內存。
已使用和未使用的內存中間存放一個分界指示器,分配內存時,指針會向未使用內存移動待分配對象大小的位數,用來存放對象。
這種方式內存使用充分,但是開銷較大,在垃圾回收后,需要整理壓縮內存便于后續使用。?

2:內存不規整
內存不規整時,JVM內部維護了一個記錄可用內存塊的列表。在分配對象時,找一塊足夠容納待分配對象的空間,劃分給對象實例。
該方式稱為空閑列表方式,對應標記清除-方式。
該方式性能較好,垃圾回收后無需整理。但可能導致內存浪費,使用不充分(大內存存小對象)。
3:內存搶占問題
多線程并發環境下,無論通過哪個內存分配方式,多個對象可能會指向同一塊內存地址,即發生內存搶占問題。
JVM針對內存線程安全問題提供了兩種解決方式:CAS 和 TLAB,這兩種方式可以共同使用,協作處理。
TLAB:
線程本地分配緩沖區,JDK 8默認開啟。默認占Eden區的1%,可通過參數調整。
每個線程在堆內存中預先分配一小塊內存,當線程要分配內存時,優先在本地緩沖區分配。本地緩沖區用完之后,會重新申請新的緩沖區。
CAS:
當本地緩沖區分配失敗,或對象大小超過設置的緩沖區大小時,會通過CAS分配內存。性能相比TLAB較差。
4:對象的內存布局
一個對象在JVM中是如何存儲的,或者如何計算對象大小。
在Hotspot虛擬機中,對象在Java內存中的存儲布局可分為三塊,三塊數據相加就是對象的大小。
1:對象頭區域
Mark Word:鎖升級的狀態、對象的HashCode、GC的分代年齡、是否偏向鎖和鎖標志位。
類型指針:存在方法區的 KlassInstance 中,通過類型指針可以獲取到類的信息并實例化。
數組長度:只有數組對象才會有。
2:實例數據區域
實例數據是指我們代碼中定義的字段內容,例如屬性、字段等。
3:對齊填充區域
添加占位符,起占位作用,保證對象的大小必須是8字節的整數倍,因為Hotspot要求對象的起始地址必須是8字節的整數倍,且對象頭部分正好是8字節的倍數。
如果不對齊填充,會導致對象頭中有空位,從而導致其他對象可能存進來,數據錯亂。并且8的倍數也會提高運行速度。
5:三色標記算法
在傳統的GC過程中,通過可達性分析算法標記存活對象,再執行垃圾回收,此時程序會STW,暫停用戶線程,只執行垃圾回收線程,效率低且用戶體驗不好。
三色標記算法是在原有的垃圾回收器上升級,將STW變為并發標記,減少停頓時間。程序一邊運行,一邊標記垃圾。并且做了優化,避免重復掃描對象,提升標記階段的效率。
在傳統標記中,對象引用不會發生改變,不會有問題;但是在并發標記時,對象間的引用可能發生改變,可能會出現錯標和漏標的情況。
用到三色標記的垃圾回收器:CMS、G1、ZGC、Shenandoah。
1:三色
三色標記算法也是根據可達性分析,從GC Roots開始進行遍歷訪問,只是在遍歷對象過程中做了優化,按照【是否檢查過】這個條件將對象標記成三種顏色。
- 白色:該對象沒有被標記,表示垃圾對象。
- 灰色:該對象已經被標記過了,但該對象下的屬性(如A引用B)沒有全被標記完,GC會繼續向下尋找垃圾。
- 黑色:該對象已被標記過,且該對象下所有屬性也都被標記過了。
可以理解為三種顏色是三個集合,分別將對象放入不同集合。初始都是白色,發生垃圾回收且找完之后,就會清空白色。灰色可以理解為中轉站,最后結束時灰色一定是空的。

2:浮動垃圾
在一個GC的并發標記過程中,一個對象已經被標記為黑色或灰色,但并發修改導致變成了垃圾(白色)。但此時不會對標記過的對象重新掃描,所以不會發現。
那么此時這個對象即不會被清除,也不會被重新找到,就稱為浮動垃圾。
浮動垃圾對系統的影響不大,在下一次GC時會被同樣處理。
3:對象漏標問題
簡答來說就是需要用的對象被回收。也是由于并發導致,可使用寫屏障技術記錄引用變化解決。
在一個GC的并發標記的過程中,一個業務線程將一個白色對象引用斷開置為垃圾,同時有一個黑色對象引用這個對象(新增引用),這兩個順序可以互換,也可以先引用再斷開。
此時GC Roots不會再去黑色節點下面找,但又因為其是白色標記。所以會導致出現:既被需要使用,又會被垃圾回收。導致系統出現問題。
CMS和G1垃圾回收器,都針對該問題做了應對,CMS增加引用環節確認標記,G1增加SATB算法確認并發期間修改的標記。
6:垃圾回收器選擇
問到項目線上使用什么垃圾回收器,可以先大概描述下常見的回收器,然后說有缺陷。
新生代收集器:Serial、ParNew、Parallel Scavenge。
老年代收集器:Serial Old、CMS、Parallel Old。
全堆收集器:G1、ZGC、Shenandoah。
在實際使用時,會根據項目的JDK版本,以及業務的類型綜合考慮垃圾回收器。
JDK8常用的有兩種組合,ParNew + CMS,Parallel Scavenge + Parallel Old。這兩種組合的新生代處理基本一樣,但是對于老年代,CMS組合使用標記清除方式,性能及響應速度會更好一點。
所以,如果我的項目是ToB的,或者堆內存是4G以下的,可以選擇JDK8默認的Parallel組合。
如果項目是ToC的(用戶),或者堆內存是4-8G更大一點,可以選擇CMS方式。
然后G1是JDK 9的默認回收器,Java 8需要顯式開啟使用。G1回收器對于內存大且需要更快的響應速度時,是很好的選擇。
最后對于ZGC、Shenandoah這兩個垃圾回收器,要確認自己的項目體積內存已達到十幾G或T級別,并且追求毫秒級的停頓時,可以考慮。一般用到這兩個回收器的都是中大型項目了,如果體量下,用了可能反而不如其他回收器。
7:逃逸分析
還有另一種提問方式,對象一定分配在堆中嗎?這個一定要回答不一定,然后描述逃逸分析。
逃逸分析:
編譯期間,JIT做的優化功能,用于減少堆內存分配壓力。
簡單來說就是在方法中創建的對象,其指針有可能被返回或者被全局引用,然后被其他方法或線程引用,這種現象就稱為引用的逃逸。
JVM 的?逃逸分析?會嘗試將未逃逸的對象分配在棧上,即方法中創建的對象,沒有傳遞出去。
但是如果創建的對象過大,使得無法分配在棧上時(棧內存不夠),會正常存儲到堆中。
好處:
棧上分配。未逃逸的對象會隨著棧幀出棧而銷毀,減輕垃圾回收的壓力。
同步消除。如果確定一個變量不會方法逃逸,那么它在多線程環境下是安全的,不需要考慮變量的線程安全問題,避免同步開銷。
標量替換。未方法逃逸的變量,棧可以使用內存碎片進行存儲,將其變量恢復為原始類型訪問,提升速度和性能。