Java虛擬機和垃圾回收詳解

JVM組成

JVM底層由三個系統構成分別是:==類加載子系統、運行時數據區、執行引擎==。

image.png

類加載機制

類是在運行期間第一次使用時動態加載的,而不是一次性加載所有類。因爲如果一次性加載,那麼會佔用很多的內存。

類的生命週期

image.png

類的生命週期包含以下7個階段:加載、驗證、準備、解析、初始化、使用、卸載

類加載過程

包含了5個階段:加載、驗證、準備、解析、初始化。

  1. 加載

加載過程完成以下三件事:

  • 通過類的完全限定名稱獲取定義該類的二進制字節流。
  • 將該字節流表示的靜態存儲結構轉換爲方法區的運行時存儲結構。
  • 在內存中生成一個代表該類的 Class 對象,作爲方法區中該類各種數據的訪問入口。
  1. 驗證

確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  1. 準備

類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存。

實例變量不會在這階段分配內存,它會在對象實例化時隨着對象一起被分配在堆中。應該注意到,實例化不是類加載的一個過程,類加載發生在所有實例化操作之前,並且類加載只進行一次,實例化可以進行多次。

初始值一般爲 0 值,例如下面的類變量 value 被初始化爲 0 而不是 123。

public static int value = 123;

如果類變量是常量,那麼它將初始化爲表達式所定義的值而不是 0。例如下面的常量 value 被初始化爲 123 而不是 0。

public static final int value = 123;

即一般初始化值爲0,常量初始化爲定義值。

  1. 解析

將常量池的符號引用替換爲直接引用的過程。
其中解析過程在某些情況下可以在初始化階段之後再開始,這是爲了支持 Java 的動態綁定。

  1. 初始化

初始化階段才真正開始執行類中定義的 Java 程序代碼。初始化階段是虛擬機執行類構造器 <clinit>() 方法的過程。在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員通過程序制定的主觀計劃去初始化類變量和其它資源。

類初始化時機

  1. 主動引用

虛擬機規範中並沒有強制約束何時進行加載,但是規範嚴格規定了有且只有下列五種情況必須對類進行初始化(加載、驗證、準備都會隨之發生):

  • 遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,如果類沒有進行過初始化,則必須先觸發其初始化。
  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;
  • 當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化;
  1. 被動引用

以上 5 種場景中的行爲稱爲對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用。被動引用的常見例子包括:

  • 通過子類引用父類的靜態字段,不會導致子類初始化。
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定義
  • 通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
System.out.println(ConstClass.HELLOWORLD);

類與類加載器

兩個類相等,需要類本身相等,並且使用同一個類加載器進行加載。這是因爲每一個類加載器都擁有一個獨立的類名稱空間。

這裏的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果爲 true,也包括使用 instanceof 關鍵字做對象所屬關係判定結果爲 true。

類加載器分類

從 Java 虛擬機的角度來講,只存在以下兩種不同的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader),使用 C++ 實現,是虛擬機自身的一部分;

  • 所有其它類的加載器,使用 Java 實現,獨立於虛擬機,繼承自抽象類 java.lang.ClassLoader。

從 Java 開發人員的角度看,類加載器可以劃分得更細緻一些:

  • 啓動類加載器(Bootstrap ClassLoader)此類加載器負責將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啓動類加載器,直接使用 null 代替即可。

  • 擴展類加載器(Extension ClassLoader)這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的所有類庫加載到內存中,開發者可以直接使用擴展類加載器。

  • 應用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委派模型

應用程序是由三種類加載器互相配合從而實現類加載,除此之外還可以加入自己定義的類加載器。

下圖展示了類加載器之間的層次關係,稱爲雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啓動類加載器外,其它的類加載器都要有自己的父類加載器。這裏的父子關係一般通過組合關係(Composition)來實現,而不是繼承關係(Inheritance)。

image.png
  1. 工作過程

一個類加載器首先將類加載請求轉發到父類加載器,只有當父類加載器無法完成時才嘗試自己加載

  1. 好處

使得 Java 類隨着它的類加載器一起具有一種帶有優先級的層次關係,從而使得基礎類得到統一。

  1. 實現

以下是抽象類 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);
    }
}

自定義類加載器實現

以下代碼中的 FileSystemClassLoader 是自定義類加載器,繼承自 java.lang.ClassLoader,用於加載文件系統上的類。它首先根據類的全名在文件系統上查找類的字節代碼文件(.class 文件),然後讀取該文件內容,最後通過 defineClass() 方法來把這些字節代碼轉換成 java.lang.Class 類的實例。

java.lang.ClassLoader 的 loadClass() 實現了雙親委派模型的邏輯,自定義類加載器一般不去重寫它,但是需要重寫 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

運行時數據區域

image.png

程序計數器

記錄正在執行的虛擬機字節碼指令的地址(如果正在執行的是本地方法則爲空)。

Java虛擬機棧

每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。

 
image.png

可以通過 -Xss 這個虛擬機參數來指定每個線程的 Java 虛擬機棧內存大小,在 JDK 1.4 中默認爲 256K,而在 JDK 1.5+ 默認爲 1M:

java -Xss2M HackTheJava

該區域可能拋出以下異常:

  • 當線程請求的棧深度超過最大值,會拋出 StackOverflowError 異常;
  • 棧進行動態擴展時如果無法申請到足夠內存,會拋出 OutOfMemoryError 異常。

本地方法棧

本地方法棧與 Java 虛擬機棧類似,它們之間的區別只不過是本地方法棧爲本地方法服務。

本地方法一般是用其它語言(C、C++ 或彙編語言等)編寫的,並且被編譯爲基於本機硬件和操作系統的程序,對待這些方法需要特別處理。

image.png

所有對象都在這裏分配內存,是垃圾收集的主要區域("GC 堆")

現代的垃圾收集器基本都是採用分代收集算法,其主要的思想是針對不同類型的對象採取不同的垃圾回收算法。可以將堆分成兩塊:

  • 新生代(Young Generation)
  • 老年代(Old Generation)
    **堆不需要連續內存,並且可以動態增加其內存,增加失敗會拋出 OutOfMemoryError 異常。
    **
    可以通過 -Xms 和 -Xmx 這兩個虛擬機參數來指定一個程序的堆內存大小,第一個參數設置初始值,第二個參數設置最大值。
java -Xms1M -Xmx2M HackTheJava

方法區

用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

和堆一樣不需要連續的內存,並且可以動態擴展,動態擴展失敗一樣會拋出 OutOfMemoryError 異常

對這塊區域進行垃圾回收的主要目標是對常量池的回收和對類的卸載,但是一般比較難實現。

運行時常量池

運行時常量池是方法區的一部分
Class 文件中的常量池(編譯器生成的字面量和符號引用)會在類加載後被放入這個區域。
除了在編譯期生成的常量,還允許動態生成,例如 String 類的 intern()。

直接內存

在 JDK 1.4 中新引入了 NIO 類,它可以使用 Native 函數庫直接分配堆外內存,然後通過 Java 堆裏的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在堆內存和堆外內存來回拷貝數據。

垃圾收集

垃圾收集主要是針對堆和方法區進行。程序計數器、虛擬機棧和本地方法棧這三個區域屬於線程私有的,只存在於線程的生命週期內,線程結束之後就會消失,因此不需要對這三個區域進行垃圾回收。

判斷一個對象是否可被回收

引用計數算法

爲對象添加一個引用計數器,當對象增加一個引用時計數器加 1,引用失效時計數器減 1。引用計數爲 0 的對象可被回收。

問題:在兩個對象出現循環引用的情況下,此時引用計數器永遠不爲 0,導致無法對它們進行回收。正是因爲循環引用的存在,因此 Java 虛擬機不使用引用計數算法

public class Test {

    public Object instance = null;

    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        doSomething();
    }
}

在上述代碼中,a 與 b 引用的對象實例互相持有了對象的引用,因此當我們把對 a 對象與 b 對象的引用去除之後,由於兩個對象還存在互相之間的引用,導致兩個 Test 對象無法被回收。

可達性分析算法

以 GC Roots 爲起始點進行搜索,可達的對象都是存活的,不可達的對象可被回收。

Java 虛擬機使用該算法來判斷對象是否可被回收,GC Roots 一般包含以下內容:

  • 虛擬機棧中局部變量表中引用的對象
  • 本地方法棧中 JNI 中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中的常量引用的對象

方法區的回收

因爲方法區主要存放永久代對象,而永久代對象的回收率比新生代低很多,所以在方法區上進行回收性價比不高。

主要是對常量池的回收和對類的卸載。
爲了避免內存溢出,在大量使用反射和動態代理的場景都需要虛擬機具備類卸載功能。

類的卸載條件很多,需要滿足以下三個條件,並且滿足了條件也不一定會被卸載:

  • 該類所有的實例都已經被回收,此時堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 Class 對象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。

finalize()

類似 C++ 的析構函數,用於關閉外部資源。但是 try-finally 等方式可以做得更好,並且該方法運行代價很高,不確定性大,無法保證各個對象的調用順序,因此最好不要使用

當一個對象可被回收時,如果需要執行該對象的 finalize() 方法,那麼就有可能在該方法中讓對象重新被引用,從而實現自救。自救只能進行一次,如果回收的對象之前調用了 finalize() 方法自救,後面回收時不會再調用該方法。

引用類型

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否可達,判定對象是否可被回收都與引用有關。

Java 提供了四種強度不同的引用類型。

  1. 強引用

被強引用關聯的對象不會被回收。

使用 new 一個新對象的方式來創建強引用。

Object obj = new Object();
  1. 弱引用

被軟引用關聯的對象只有在內存不夠的情況下才會被回收

使用 SoftReference 類來創建軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使對象只被軟引用關聯
  1. 弱引用

被弱引用關聯的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發生之前

使用 WeakReference 類來創建弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
  1. 虛引用

又稱爲幽靈引用或者幻影引用,一個對象是否有虛引用的存在,不會對其生存時間造成影響,也無法通過虛引用得到一個對象。
爲一個對象設置虛引用的唯一目的是能在這個對象被回收時收到一個系統通知

使用 PhantomReference 來創建虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;

垃圾收集算法

  1. 標記 - 清除
image.png

在標記階段,程序會檢查每個對象是否爲活動對象,如果是活動對象,則程序會在對象頭部打上標記。

在清除階段,會進行對象回收並取消標誌位,另外,還會判斷回收後的分塊與前一個空閒分塊是否連續,若連續,會合並這兩個分塊。回收對象就是把對象作爲分塊,連接到被稱爲 “空閒鏈表” 的單向鏈表,之後進行分配時只需要遍歷這個空閒鏈表,就可以找到分塊。

在分配時,程序會搜索空閒鏈表尋找空間大於等於新對象大小 size 的塊 block。如果它找到的塊等於 size,會直接返回這個分塊;如果找到的塊大於 size,會將塊分割成大小爲 size 與 (block - size) 的兩部分,返回大小爲 size 的分塊,並把大小爲 (block - size) 的塊返回給空閒鏈表。

不足

  • 標記和清除過程效率都不高;
  • 會產生大量不連續的內存碎片,導致無法給大對象分配內存。
  1. 標記 - 整理
image.png

讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

優點:

  • 不會產生內存碎片

不足:

  • 需要移動大量對象,處理效率比較低。
  1. 複製
image.png

將內存劃分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了就將還存活的對象複製到另一塊上面,然後再把使用過的內存空間進行一次清理。

主要不足是隻使用了內存的一半

現在的商業虛擬機都採用這種收集算法回收新生代,但是並不是劃分爲大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象全部複製到另一塊 Survivor 上,最後清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛擬機的 Eden 和 Survivor 大小比例默認爲 8:1,保證了內存的利用率達到 90%。如果每次回收有多於 10% 的對象存活,那麼一塊 Survivor 就不夠用了,此時需要依賴於老年代進行空間分配擔保,也就是借用老年代的空間存儲放不下的對象。

問題: 爲什麼要分代?

其實不分代完全可以,分代的唯一理由就是優化GC性能。你先想想,如果沒有分代,那我們所有的對象都在一塊,GC的時候我們要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而我們的很多對象都是朝生夕死的,如果分代的話,我們把新創建的對象放到某一地方,當GC的時候先把這塊存“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。

年輕代代GC

HotSpot JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例爲8:1,爲啥默認會是這個比例,接下來我們會聊到。一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。

因爲年輕代中的對象基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另外一塊上面。複製算法不會產生內存碎片。

在GC開始的時候,對象只會存在於Eden區和名爲“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有對象移動到年老代中。

  1. 分代收集

現在的商業虛擬機採用分代收集算法,它根據對象存活週期將內存劃分爲幾塊,不同塊採用適當的收集算法。

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

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

垃圾收集器

image.png

以上是 HotSpot 虛擬機中的 7 個垃圾收集器,連線表示垃圾收集器可以配合使用。

  • 單線程與多線程:單線程指的是垃圾收集器只使用一個線程,而多線程使用多個線程;
  • 串行與並行:串行指的是垃圾收集器與用戶程序交替執行,這意味着在執行垃圾收集的時候需要停頓用戶程序;並行指的是垃圾收集器和用戶程序同時執行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式執行。

內存分配與回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,因爲新生代對象存活時間很短,因此 Minor GC 會頻繁執行,執行的速度一般也會比較快。
  • Full GC:回收老年代和新生代,老年代對象其存活時間長,因此 Full GC 很少執行,執行速度會比 Minor GC 慢很多。

內存配策略

  1. 對象優先在Eden區分配

大多數情況下,對象在新生代 Eden 上分配,當 Eden 空間不夠時,發起 Minor GC。

  1. 大對象直接進入老年代

大對象是指需要連續內存空間的對象,最典型的大對象是那種很長的字符串以及數組。經常出現大對象會提前觸發垃圾收集以獲取足夠的連續空間分配給大對象。

-XX:PretenureSizeThreshold,大於此值的對象直接在老年代分配,避免在 Eden 和 Survivor 之間的大量內存複製。

  1. 長期存活到對象進入老年代

爲對象定義年齡計數器,對象在 Eden 出生並經過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。

-XX:MaxTenuringThreshold 用來定義年齡的閾值。

  1. 動態對象年齡判定

虛擬機並不是永遠要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大於 Survivor 空間的一半,則年齡大於或等於該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。

  1. 空間分配擔保

在發生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果條件成立的話,那麼 Minor GC 可以確認是安全的。

如果不成立的話虛擬機會查看 HandlePromotionFailure 的值是否允許擔保失敗,如果允許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次 Minor GC;如果小於,或者 HandlePromotionFailure 的值不允許冒險,那麼就要進行一次 Full GC。

Full GC 的觸發條件

對於 Minor GC,其觸發條件非常簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對複雜,有以下條件:

  1. 調用System.gc()

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

  1. 老年代空間不足

老年代空間不足的常見場景爲大對象直接進入老年代、長期存活的對象進入老年代等。

爲了避免以上原因引起的 Full GC,應當儘量不要創建過大的對象以及數組。除此之外,可以通過 -Xmn 虛擬機參數調大新生代的大小,讓對象儘量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。

  1. 空間分配擔保失敗

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

使用複製算法的 Minor GC 需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC。具體內容請參考上面的第 5 小節。

  1. JDK1.7及以前的永久代空間

在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的爲一些 Class 的信息、常量、靜態變量等數據。

當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被佔滿,在未配置爲採用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼虛擬機會拋出 java.lang.OutOfMemoryError。

爲避免以上原因引起的 Full GC,可採用的方法爲增大永久代空間或轉爲使用 CMS GC。

  1. Concurrent Mode Failure

執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足(可能是 GC 過程中浮動垃圾過多導致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,並觸發 Full GC。

執行引擎

類加載器將字節碼載入內存之後,執行引擎以Java 字節碼指令爲單元,讀取Java字節碼。問題是,現在的java字節碼機器是讀不懂的,因此還必須想辦法將字節碼轉化成平臺相關的機器碼。這個過程可以由解釋器來執行,也可以由即時編譯器(JIT Compiler)來完成。

image.png
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章