JVM 深入筆記

JVM 深入筆記(1)內存區域是如何劃分的?

一個超短的前言

JVM 是一個從事 Java 開發的軟件工程師的修煉之路上必然要翻閱的一座山。當你瞭解了 Java 的基本語言特性,當你熟悉了 Java SDK 中的常用 API,當你寫過一些或大或小的程序後,你就會有去了解 JVM 的需求出現。如果你現在沒有這種感覺,那麼可能此時去了解 JVM 並不是一個好的時機,因爲你不會帶着問題去探索。

從本篇開始的系列博文,記錄本人的 JVM 深入學習總結,其中結合了本人自己的一些經驗,也參考了一些書籍和網絡資源,然後根據自己的理解寫出這些博文。如有版權問題,請伊妹兒我 :)

謹以此係列博文分享給我的朋友們。

1 JVM 簡史

屏蔽不同的硬件平臺或操作系統上的環境差異,通過一個向上層提供統一編程接口來實現Java程序可移植性的軟件層,我們稱之爲 Java 虛擬機(Java Virtual Machine,簡稱 JVM)。

雖然 Java 的發展史可以追溯到 1991年4月由著名的 James Gosling 領導的 Green Project 計劃,但是 JDK 1.0 版本的正式發佈是在 1996年的1月23日,該版本提供的 JVM 是一個純解釋執行的 Sun Classic VM,不過是以外部加載的方式來使用的。而該版本的 JDK 所包含的主要技術除了 JVM 之外,就是 Applet 和 AWT。當然,此前在 Java 還叫做 Oak 的時候就已經有了一個完整的編程語言的外形,而 1995年5月23日,Oak 正式更名爲 Java,並由 Sun 公司發佈了 Java 1.0 版本。

關於 Java 語言的背景,這裏就不多說了,主要還是介紹 JVM 的發展歷程。到 1998年發展出了 JDK 1.2,在該版本中 JVM 內置了 JIT (Just In Time) 編譯器,而 JDK 1.2 中也曾有過 Sun Classic VM、Hot Spot VM 和 Sun Exact VM 三種虛擬機。其中 Hot Spot VM 和 Extract VM 都內置 JIT 編譯器。1997年,Sun 收購了開發 Hot Spot VM 的名爲 Longview Technologies 的公司。也從此該虛擬機改叫 Sun Hot Spot VM,當然那麼一個前綴對於 Developers 來說是沒所謂的。從 JDK 1.3 開始,Sun Hot Spot VM 成爲 Sun 公司發佈的 JDK 的默認 JVM。

目前活躍的商用 JVM 有 Sun Hot Spot、BEA JRockit 和 IBM J9。不過要說的是,JRockit 的主人 BEA 被 Oracle 收購了,而 Hot Spot 的主人被 Sun 公司在 2010 年也被 Oracle 收購了。因此 Hot Spot 和 JRockit 都隸屬於 Oracle 公司。Oracle 曾稱將會將這個兩個 JVM 的優勢相融合,產生一款新的 JVM,屆時 Hot Spot 和 JRockit 也將進入歷史博物館了。JVM 的鼻祖 Sun Classic VM 早已被淘汰使用了,而 曾在 JDK 1.2 中靈光乍現過的 Sun Extract VM 也已經退出了歷史舞臺。另一個由 Apache 基金會主導的 Harmony 項目也有很大的影響,且間接由其催生的 Dalvik 虛擬機,爲 Google Android 的火爆發展做出了巨大的貢獻。在應用於手機、平板電腦、IVI、PDA 等設備上的嵌入式 JVM 領域,除了 Dalvik,還有 KVM、CDC Hot Spot、CLDC Hot Spot 等 JVM 也較有影響力。

從本文開始的系列博文《JVM 原理與實戰》中所有實驗性程序的環境,都是 Mac OS X 10.7.3,JDK 1.6.0 Update 29,Oracle Hot Spot 20.4-b02。

2 初識 JVM 內存區域劃分

大多數 JVM 將內存區域劃分爲 Method Area(Non-Heap), Heap, Program Counter Register, Java Method Stack,Native Method Stack 和 Direct Memomry(注意 Directory Memory 並不屬於 JVM 管理的內存區域)。前三者一般譯爲:方法區、堆、程序計數器。但不同的資料和書籍上對於後三者的中文譯名不盡相同,這裏將它們分別譯作:Java 方法棧、原生方法棧和直接內存區。對於不同的 JVM,內存區域劃分可能會有所差異,比如 Hot Spot 就將 Java 方法棧和原生方法棧合二爲一,我們可以同城爲方法棧(Method Stack)。

首先我們熟悉一下一個一般性的 Java 程序的工作過程。一個 Java 源程序文件,會被編譯爲字節碼文件(以 class 爲擴展名),然後告知 JVM 程序的運行入口,再被 JVM 通過字節碼解釋器加載運行。那麼程序開始運行後,都是如何涉及到各內存區域的呢?

概括地說來,JVM 每遇到一個線程,就爲其分配一個程序計數器、Java 方法棧和原生方法棧。當線程終止時,兩者所佔用的內存空間也會被釋放掉。棧中存儲的是棧幀,可以說每個棧幀對應一個“運行現場”。在每個“運行現場”中, 如果出現了一個局部對象,則它的實例數據被保存在堆中,而類數據被保存在方法區。

我們用上面這一小段文字就描述完了每個內存區域的基本功能。但是這還比較粗糙,下面分別介紹它們的存儲對象、生存期與空間管理策略。

2.1 程序計數器

  • 線程特性:私有
  • 存儲內容:字節碼文件指令地址(Java Methods),或 Undefined(Native Methods)
  • 生命週期:隨線程而生死
  • 空間策略:佔用內存很小

這個最簡單,就先撿它說吧。程序計數器,是線程私有(與線程共享相對)的,也就是說有 N 個線程,JVM 就會分配 N 個程序計數器。如果當前線程在執行一個 Java 方法,則程序計數器記錄着該線程所執行的字節碼文件中的指令地址。如果線程執行的是一個 Native 方法,則計數器值爲 Undefined。

程序計數器的生存期多長呢?顯然程序計數器是伴隨線程生而生,伴隨線程死而死的。而它所佔用的內存空間也很小。

2.2 Java 方法棧與原生方法棧

Java 方法棧也是線程私有的,每個 Java 方法棧都是由一個個棧幀組成的,每個棧幀是一個方法運行期的基礎數據結構,它存儲着局部變量表、操作數棧、動態鏈接、方法出口等信息。當線程調用調用了一 個 Java 方法時,一個棧幀就被壓入(push)到相應的 Java 方法棧。當線程從一個 Java 方法返回時,相應的 Java 方法棧就彈出(pop)一個棧幀。

其中要詳細介紹的是局部變量表,它保存者各種基本數據類型和對象 引用(Object reference)。基本數據類型包括 boolean、byte、char、short、int、long、float、double。對象引用,本質就是一個地址(也可以說是一個“指 針”),該地址是堆中的一個地址,通過這個地址可以找到相應的 Object(注意是“找到”,原因會在下面解釋)。而這個地址找到相應 Object 的方式有兩種。一種是該地址存儲着 Pointer to Object Instance Data 和 Pointer to Object Class Data,另一種是該地址存儲着 Object Instance Data,其中又包含有 Pointer to Object Class Data。如下兩圖所示。

圖2·直接方式

第一種方式,Java 方法棧中有 Handler Pool 和 Instance Pool。無論哪種方式,Object Class Data 都是存儲在方法區的,Object Instance Data 都是存儲在堆中的。

原生方法棧與 Java 方法棧相類似,這裏不再贅述。

2.3 堆

堆 是在啓動虛擬機的時候劃分出來的區域,其大小由參數或默認參數指定。當虛擬機終止運行時,會釋放堆內存。一個 JVM 只有一個堆,它自然是線程共享的。堆中存儲的是所有的 Object Instant Data 以及數組(不過隨着棧上分配技術、標量替換技術等優化手段的發展,對象也不一定都存儲在堆上了),這些 Instance 由垃圾管理器(Garbage Collector)管理,具體的算法會在後面提到。

堆可以是由不連續的物理內存空間組成的,並且既可以固定大小,也可以設置爲可擴展的(Scalable)。

2.4 方法區

通 過(2)中 Java 方法棧的介紹,你已經知道了 Object Class Data 是存儲在方法區的。除此之外,常量、靜態變量、JIT 編譯後的代碼也都在方法區。正因爲方法區所存儲的數據與堆有一種類比關係,所以它還被稱爲 Non-Heap。方法區也可以是內存不連續的區域組成的,並且可設置爲固定大小,也可以設置爲可擴展的,這點與堆一樣。

方法區內部 有一個非常重要的區域,叫做運行時常量池(Runtime Constant Pool,簡稱 RCP)。在字節碼文件中有常量池(Constant Pool Table),用於存儲編譯器產生的字面量和符號引用。每個字節碼文件中的常量池在類被加載後,都會存儲到方法區中。值得注意的是,運行時產生的新常量也 可以被放入常量池中,比如 String 類中的 intern() 方法產生的常量。

2.5 直接內存區

直接內存區並不是 JVM 管理的內存區域的一部分,而是其之外的。該區域也會在 Java 開發中使用到,並且存在導致內存溢出的隱患。如果你對 NIO 有所瞭解,可能會知道 NIO 是可以使用 Native Methods 來使用直接內存區的。

JVM 深入筆記(2)各內存區溢出場景模擬

《JVM 深入筆記(1)內存區域是如何劃分的?》一文已經介紹了 JVM 對內存區域的劃分與管理。在現實的編程過程中,會遇到一些 OutOfMemoryError (OOM) 的情形。通過模擬,我們可以直接點中這些場景的本質,從而在紛繁複雜的千萬行代碼中避免這樣去 coding。導致 OOM 的情況有多種,包括 Java 或 Native Method Stack 的內存不足或者棧空間溢出、Heap 內存溢出、Non-heap 內存溢出、Direct Memory 溢出。

1. Java Method Stack 棧溢出實驗

什麼時候會讓 Java Method Stack 棧溢出啊?棧的基本特點就是 FILO(First In Last Out),如果 in 的太多而 out 的太少,就好 overflow 了。而 Java Method Stack 的功能就是保存每一次函數調用時的“現場”,即爲入棧,函數返回就對應着出棧,所以函數調用的深度越大,棧就變得越大,足夠大的時候就會溢出。所以模擬 Java Method Stack 溢出,只要不斷遞歸調用某一函數就可以。

程序源碼-1

// Author: Poechant

// Blog: blog.csdn.net/poechant

// Email: zhognchao.ustc#gmail.com (#->@)

// Args: -verbose:gc -Xss128K

package com.sinosuperman.main;

public class Test {

    private int stackLength = 0;

    public void stackOverflow() {

        ++stackLength;

        stackOverflow();

    }

    public static void main(String[] args) throws Throwable {

        Test test = new Test();

        try {

            test.stackOverflow();

        } catch (Throwable e) {

            System.out.println("stack length: " + test.stackLength);

            throw e;

        }

    }

}

運行結果

stack length: 1052

Exception in thread "main" java.lang.StackOverflowError

at com.sinosuperman.main.Test.stackOverflow(Test.java:8)

at com.sinosuperman.main.Test.stackOverflow(Test.java:9)

at com.sinosuperman.main.Test.stackOverflow(Test.java:9)

at com.sinosuperman.main.Test.stackOverflow(Test.java:9)

at com.sinosuperman.main.Test.stackOverflow(Test.java:9)

...

2. Java Method Stack 內存溢出實驗

Heap 內存溢出

堆是用來存儲對象的,當然對象不一定都存在堆裏(由於逃逸技術的發展)。那麼堆如果溢出了,一定是不能被殺掉的對象太多了。模擬 Heap 內存溢出,只要不斷創建對象並保持有引用存在即可。

程序源碼-2

// Author: Poechant

// Blog: blog.csdn.net/poechant

// Email: zhongchao.ustc#gmail.com (#->@)

// Args: -verbose:gc -Xmx50m -Xms50m

package com.sinosuperman.main;

import java.util.ArrayList;

import java.util.List;

public class Test {

    private static class HeapOomObject {

}

public static void main(String[] args) {

    List<HeapOomObject> list = new ArrayList<HeapOomObject>();

    while (true) {

        list.add(new HeapOomObject());

    }

  }

}

運行結果

[GC 17024K->14184K(49088K), 0.1645899 secs]

[GC 26215K->29421K(49088K), 0.0795283 secs]

[GC 35311K(49088K), 0.0095602 secs]

[Full GC 43400K->37709K(49088K), 0.1636702 secs]

[Full GC 49088K->45160K(49088K), 0.1609499 secs]

[GC 45312K(49088K), 0.0265257 secs]

[Full GC 49088K->49087K(49088K), 0.1656715 secs]

[Full GC 49087K->49087K(49088K), 0.1656147 secs]

[Full GC 49087K->49062K(49088K), 0.1976727 secs]

[GC 49063K(49088K), 0.0287960 secs]

[Full GC 49087K->49087K(49088K), 0.1901410 secs]

[Full GC 49087K->49087K(49088K), 0.1673056 secs]

[Full GC 49087K->316K(49088K), 0.0426515 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at com.sinosuperman.main.Test.main(Test.java:14)

3. Method Area 內存溢出

也就是 Non-heap,是用來存儲 Object Class Data、常量、靜態變量、JIT 編譯後的代碼等。如果該區域溢出,則說明某種數據創建的實在是太多了。模擬的話,可以不斷創建新的 class,直到溢出爲止。

以下代碼使用到 cglib-2.2.2.jar 和 asm-all-3.0.jar。

程序源碼-3

package com.sinosuperman.main;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;

import net.sf.cglib.proxy.MethodInterceptor;

import net.sf.cglib.proxy.MethodProxy;

public class Test {

    static class MethodAreaOomObject {

    }

    public static void main(String[] args) {

        while(true){

            Enhancer enhancer = new Enhancer();

            enhancer.setSuperclass(MethodAreaOomObject.class);

             enhancer.setUseCache(false);

             enhancer.setCallback(new MethodInterceptor() {

                public Object intercept(Object obj, Method method, Object[] args,

                MethodProxy proxy) throws Throwable {

                    return proxy.invoke(obj, args);

                 }

            });

            enhancer.create();

        }

    }

}

運行結果

Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null

at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)

at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)

at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)

at com.sinosuperman.main.Test.main(Test.java:24)

Caused by: java.lang.reflect.InvocationTargetException

at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

at java.lang.reflect.Method.invoke(Method.java:597)

at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:384)

at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:219)

... 3 more

Caused by: java.lang.OutOfMemoryError: PermGen space

at java.lang.ClassLoader.defineClass1(Native Method)

at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)

at java.lang.ClassLoader.defineClass(ClassLoader.java:615)

... 8 more

4. Runtime Constant Pool in Method Area 內存溢出

在運行時產生大量常量就可以實現讓 Method Area 溢出的目的。運行是常量可以用 String 類的 intern 方法,不斷地產生新的常量。

程序源碼-4

package com.sinosuperman.main;

import java.util.ArrayList;

import java.util.List;

public class Test {

    public static void main(String[] args) {

        List<String> list = new ArrayList<String>();

        int i = 0;

        while (true) {

            list.add(String.valueOf(i++).intern());

        }

    }

}

運行結果

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

at java.lang.String.intern(Native Method)

at com.sinosuperman.main.Test.main(Test.java:12)

結語

在實際編碼中要儘量避免此類錯誤。不過大多數程序設計的結構比這裏的示例要複雜的多,使得問題被隱藏。但 JVM 的內存溢出問題本質上大都可歸結爲以上這幾種情況。

JVM深入筆記(3)垃圾標記算法

如果您還不瞭解 JVM 的基本概念和內存劃分,請先閱讀《JVM 深入筆記(1)內存區域是如何劃分的?》一文。然後再回來 :)

因爲 Java 中沒有留給開發者直接與內存打交道的指針(C++工程師很熟悉),所以如何回收不再使用的對象的問題,就丟給了 JVM。所以下面就介紹一下目前主流的垃圾收集器所採用的算法。不過在此之前,有必要先講一下 Reference。

1 引用(Reference)

你現在還是 JDK 1.0 或者 1.1 版本的開發者嗎?如果是的話,可以告訴你跳過“5 Reference”這一部分吧,甚至跳過本文。如果不是的話,下面這些內容還是有參考價值的。你可能會問,Reference 還有什麼可講的?還是有一點的,你知道 Reference 有四種分類嗎?這可不是孔乙己的四種“回”字寫法可以類比的。說引用,我們最先想到的一般是:

Object obj = new Object();

這種屬於 Strong Reference(JDK 1.2 之後引入),這類 ref 的特點就是,只要 ref 還在,目標對象就不能被幹掉。我們可以想一下爲什麼要幹掉一些對象?很簡單,因爲內存不夠了。如果內存始終夠用,大家都活着就好了。所以當內存不夠時,會 先幹掉一些“必死無疑的傢伙”(下面會解釋),如果這時候內存還不夠用,就該幹掉那些“可死可不死的傢伙”了。

JDK 1.2 之後還引入了 SoftReference 和 WeakReference,前者就是那些“可死可不死的傢伙”。當進行了一次內存清理(幹掉“必死無疑”的傢伙)後,還是不夠用,就再進行一次清理,這 次清理的內容就是 SoftReference 了。如果幹掉 Soft Reference 後還是不夠用,JVM 就拋出 OOM 異常了。

好像 WeakReference 還沒說呢?它是幹嘛的?其實它就是那些“必死無疑的傢伙”。每一次 JVM 進行清理時,都會將這類 ref 幹掉。所以一個 WeakReference 出生後,它的死期,就是下一次 JVM 的清理。

“回”字的最後一種寫法,是 PhantomReference,名字很恐怖吧(Phantom是鬼魂的意思,不僅含義恐怖,而且發音也恐怖——“墳頭”)。這類 ref 的唯一作用,就是當相應的 Object 被 clean 掉的時候,通知 JVM。

雖然有四種“回”字,但是 Strong Reference 卻沒有相應的類,java.lang.ref.Reference 只有三個子類。

你可能會發現,在 Reference 這一部分,我經常性地提到“清理”。什麼“清理”?就是下面要說的 Garbage Collection 中對”無用”對象的 clean。

這是 JVM 的核心功能之一,同時也是爲什麼絕大多數 Java 工程師不需要像 C++ 程序員那樣考慮對象的生存期問題。至於因此而同時導致 Java 工程師不能夠放任自由地控制內存的結果,其實是一個 Freedom 與 Effeciency 之間的 trade-off,而 C++ 工程師與 Java 工程師恰如生存在兩個國度的人,好像“幸福生活”的天朝人民與“水深火熱”的西方百姓之間的“時而嘲笑、時而豔羨”一般。

言歸正傳,Garbage Collector(GC)是 JVM 中篩選並清理 Garbage 的工具。那麼第一個要搞清楚的問題是,什麼是 Garbage?嚴謹的說,Garbage 就是不再被使用、或者認爲不再被使用、甚至是某些情況下被選作“犧牲品”的對象。看上去很羅嗦,那就先理解成“不再被使用”吧。這就出現了第二個問題,怎 麼判斷不再被使用?這就是下面首先要介紹的 Object Marking Algorithms。

2 對象標記算法(Object Marking Algorithms)

下面還是先從本質一點的東西開始說吧。一個對象變得 useless 了,其實就是它目前沒有稱爲任何一個 reference 的 target,並且認爲今後也不會成爲(這是從邏輯上說,實際上此刻沒有被引用的對象,今後也沒有人會去引用了??)

2.1 引用計數法(Reference Counting)

核心思想:很簡單。每個對象都有一個引用計數器,當在某處該對象被引用的時候,它的引用計數器就加一,引用失效就減一。引用計數器中的值一旦變爲0,則該 對象就成爲垃圾了。但目前的 JVM 沒有用這種標記方式的。爲什麼呢?

因爲引用計數法無法解決循環引用(對象引用關係組成“有向有環圖”的情況,涉及一些圖論的知識,在根搜索算法中會解釋)的問題。比如下面的例子:

package com.sinosuperman.jvm;

class _1MB_Data {

    public Object instance = null;

     private byte[] data = new byte[1024 * 1024 * 1];

}

public class CycledReferenceProblem {

    public static void main(String[] args) {

        _1MB_Data d1 = new _1MB_Data();

        _1MB_Data d2 = new _1MB_Data();

        d1.instance = d2;

        d2.instance = d1;

         d1 = null;

        d2 = null;

        System.gc();

     }

}

在這個程序中,首先在堆內存中創建了兩個 1MB 大小的對象,並且其中分別存儲的 instance 成員引用了對方。那麼即使 d1和 d2 被置爲 null 時,引用數並沒有變爲零。如果這是採用引用計數法來標記的話,內存就被浪費了,gc 的時候不會被回收。好悲催啊 :(

重複一下在《JVM 深入筆記(1)內存區域是如何劃分的?》中提到的運行環境:

**Mac OS X 10.7.3**,**JDK 1.6.0 Update 29**,**Oracle Hot Spot 20.4-b02**。

那麼我們來試試Oracle Hot Spot 20.4-b02是不是採用引用計數法來標記的。對了,別忘了爲CycledReferenceProblem使用的虛擬機開啓-XX:+PrintGCDetails參數,然後運行結果如下:

[Full GC (System) [CMS: 0K->366K(63872K), 0.0191521 secs] 3778K->366K(83008K), [CMS Perm : 4905K->4903K(21248K)], 0.0192274 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

Heap

par new generation total 19136K, used 681K [7f3000000, 7f44c0000, 7f44c0000)

eden space 17024K, 4% used [7f3000000, 7f30aa468, 7f40a0000)

from space 2112K, 0% used [7f40a0000, 7f40a0000, 7f42b0000)

to space 2112K, 0% used [7f42b0000, 7f42b0000, 7f44c0000)

concurrent mark-sweep generation total 63872K, used 366K [7f44c0000, 7f8320000, 7fae00000)

concurrent-mark-sweep perm gen total 21248K, used 4966K [7fae00000, 7fc2c0000, 800000000)

可以看到,在Full GC時,清理掉了 (3778-366)KB=3412KB 的對象。這一共有 3MB 多,可以確定其中包括兩個我們創建的 1MB 的對象嗎?貌似無法確定。好吧,那下面我們使用_2M_Data對象來重複上面的程序。

package com.sinosuperman.jvm;

class _2MB_Data {

    public Object instance = null;

     private byte[] data = new byte[1024 * 1024 * 2];

}

public class CycledReferenceProblem {

    public static void main(String[] args) {

        _2MB_Data d1 = new _2MB_Data();

         _2MB_Data d2 = new _2MB_Data();

        d1.instance = d2;

        d2.instance = d1;

         d1 = null;

         d2 = null;

         System.gc();

    }

}

運行結果如下:

[Full GC (System) [CMS: 0K->366K(63872K), 0.0185981 secs] 5826K->366K(83008K), [CMS Perm : 4905K->4903K(21248K)], 0.0186886 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]

Heap

par new generation total 19136K, used 681K [7f3000000, 7f44c0000, 7f44c0000)

eden space 17024K, 4% used [7f3000000, 7f30aa4b0, 7f40a0000)

from space 2112K, 0% used [7f40a0000, 7f40a0000, 7f42b0000)

to space 2112K, 0% used [7f42b0000, 7f42b0000, 7f44c0000)

concurrent mark-sweep generation total 63872K, used 366K [7f44c0000, 7f8320000, 7fae00000)

concurrent-mark-sweep perm gen total 21248K, used 4966K [7fae00000, 7fc2c0000, 800000000)

這次清理掉了 (5826-366)=5460KB 的對象。我們發現兩次清理相差 2048KB,剛好是 2MB,也就是 d1 和 d2 剛好各相差 1MB。我想這可以確定,gc 的時候確實回收了兩個循環引用的對象。如果你還不信,可以再試試 3MB、4MB,都是剛好相差 2MB。

這說明Oracle Hot Spot 20.4-b02虛擬機並不是採用引用計數方法。事實上,現在沒有什麼流行的 JVM 會去採用簡陋而問題多多的引用計數法來標記。不過要承認,它確實簡單而且大多數時候有效。

那麼,這些主流的 JVM 都是使用什麼標記算法的呢?

2.2. 根搜索算法(Garbage Collection Roots Tracing)

對,沒錯,就是“跟搜索算法”。我來介紹以下吧。

2.2.1 基本思想

其實思路也很簡單(算法領域,除了紅黑樹、KMP等等比較複雜外,大多數思路都很簡單),可以概括爲如下幾步:

選定一些對象,作爲 GC Roots,組成基對象集(這個詞是我自己造的,與其他文獻資料的說法可能不一樣。但這無所謂,名字只是個代號,理解算法內涵纔是根本);

由基對象集內的對象出發,搜索所有可達的對象;

其餘的不可達的對象,就是可以被回收的對象。

這裏的“可達”與“不可達”與圖論中的定義一樣,所有的對象被看做點,引用被看做有向連接,整個引用關係就是一個有向圖。在“引用計數法”中提到的循環引 用,其實就是有向圖中有環的情況,即構成“有向有環圖”。引用計數法不適用於“有向有環圖”,而根搜索算法適用於所有“有向圖”,包括有環的和無環的。那 麼是如何解決的呢?

2.2.2 GC Roots

如果你的邏輯思維夠清晰,你會說“一定與選取基對象集的方法有關”。是的,沒錯。選取 GC Roots 組成基對象集,其實就是選取如下這些對象:

《深入理解 Java 虛擬機:JVM 高級特性與最佳實踐》一書中提到的 GC Roots 爲:

方法區(Method Area,即 Non-Heap)中的類的 static 成員引用的對象,和 final 成員引用的對象;

Java 方法棧(Java Method Stack)的局部變量表(Local Variable Table)中引用的對象;

原生方法棧(Native Method Stack)中 JNI 中引用的對象。

但顯然不夠全面,[參考2]中提到的要更全面:(March 6th,2012 update)

由系統類加載器加載的類相應的對象:這些類永遠不會被卸載,且這些類創建的對象都是 static 的。注意用戶使用的類加載器加載的類創建的對象,不屬於 GC Roots,除非是 java.lang.Class 的相應實例有可能會稱爲其他類的 GC Roots。

正在運行的線程。

Java 方法棧(Java Method Stack)的局部變量表(Local Variable Table)中引用的對象。

原生方法棧(Native Method Stack)的局部變量表(Local Variable Table)中引用的對象。

JNI 中引用的對象。

同步監控器使用的對象。

由 JVM 的 GC 控制的對象:這些對象是用於 JVM 內部的,是實現相關的。一般情況下,可能包括系統類加載器(注意與“1”不一樣,“1”中是 objects created by the classes loaded by system class loaders,這裏是 the objects, corresponding instances of system class loaders)、JVM 內部的一些重要的異常類的對象、異常句柄的預分配對象和在類加載過程中自定義的類加載器。不幸的是,JVM 並不提供這些對象的任何額外的詳細信息。因此這些實現相關的內容,需要依靠分析來判定。

所以這個算法實施起來有兩部分,第一部分就是到 JVM 的幾個內存區域中“找對象”,第二部分就是運用圖論算法。

3. 廢話

JVM 的標記算法並不是 JVM 垃圾回收策略中最重要的。真正的核心,是回收算法,當然標記算法是基礎。

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