天天看點

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

一、概念

1、JVM記憶體模型

首先老規矩,祭上一張自己畫的記憶體模型圖

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

畫的比較簡陋,簡單介紹一下,整個JVM占用的記憶體可分為兩個大區,分别是線程共享區和線程私有區,線程共享區和JVM同生共死,所有線程均可通路此區域;而線程私有區顧名思義每個線程各自占有,與各自線程同生共死。這兩個大區内部根據JVM規範定義又分為以下幾個區:

方法區(Method Area)

方法區主要是放一下類似類定義、常量、編譯後的代碼、靜态變量等,在JDK1.7中,HotSpot VM的實作就是将其放在永久代中,這樣的好處就是可以直接使用堆中的GC算法來進行管理,但壞處就是經常會出現記憶體溢出,即PermGen Space異常,是以在JDK1.8中,HotSpot VM取消了永久代,用元空間取而代之,元空間直接使用本地記憶體,理論上電腦有多少記憶體它就可以使用多少記憶體,是以不會再出現PermGen Space異常。

堆(Heap)

幾乎所有對象、數組等都是在此配置設定記憶體的,在JVM記憶體中占的比例也是極大的,也是GC垃圾回收的主要陣地,平時我們說的什麼新生代、老年代、永久代也是指的這片區域,至于為什麼要進行分代後面會解釋。

虛拟機棧(Java Stack)

當JVM在執行方法時,會在此區域中建立一個棧幀來存放方法的各種資訊,比如傳回值,局部變量表和各種對象引用等,方法開始執行前就先建立棧幀入棧,執行完後就出棧。

本地方法棧(Native Method Stack)

和虛拟機棧類似,不過差別是專門提供給Native方法用的。

程式計數器(Program Counter Register)

占用很小的一片區域,我們知道JVM執行代碼是一行一行執行位元組碼,是以需要一個計數器來記錄目前執行的行數。

2、堆記憶體

堆記憶體是JVM記憶體中占用較大的一塊區域,對象都在此地配置設定記憶體。在堆中,又分為新生代及老年代,新生代中又分三個區域,分别是Eden,Survivor To,Survivor From。堆記憶體是JVM調優的重點區域,也是這篇部落格重點讨論的内容。

3、提出問題

可能看到這裡我們都會産生這樣一個經典問題

為何堆記憶體要進行分代?

最簡單的回收方式(标記-回收算法)

假設堆記憶體不進行分代,那麼垃圾回收應該如何進行呢?我們可以大膽想象一下,在一大片記憶體空間中我們配置設定了若幹個對象(假設圖中一個黑色方塊代表一個位元組,配置設定的對象均占用兩個位元組)

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

此時堆記憶體滿了,是時候來一波垃圾回收操作了,通過某種分析算法,我們分析到某幾個對象是需要進行回收(下述此類對象稱之為回收對象),我們讓無用對象就地清除,回收後的結果如下:

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

我們可以看到,回收後的記憶體支離破碎的,雖然現在還有八個位元組的記憶體空間,但隻要有三個位元組或以上的對象需要申請記憶體,那麼這片支離破碎依舊無法為其配置設定記憶體,因為沒有連續的空間。

第一次演化(複制算法)

既然我們沒法回收出連續的空間,那我們可以從一開始就把記憶體分兩個大區,平時隻用其中一個區,如下圖

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

當左邊記憶體區滿時,就開始一波回收操作,找到那些無需回收的對象(下述稱此類對象為存活對象),将它們工工整整地複制到右邊的區域中,接着将左邊的區域來次大清理,清理後的結果如下:

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

這樣當需要再配置設定記憶體給對象時,就使用右邊的區域,而右邊的區域此時也有8個位元組的連續空間供配置設定,當右邊滿了,再如法炮制,将存活對象複制到左邊再将右邊回收。貌似這樣就解決了問題了,但是總感覺有什麼地方不對,是的沒錯,每次隻使用一半的記憶體,未免也太浪費了!

第二次演化(标記-壓縮算法)

我們依舊不分區,将整個記憶體用滿, 開始回收垃圾時,我們将存活對象全部移動到左邊,然後對邊界外的記憶體進行清理,如下圖

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

這算是一種折中的做法,起碼比第一次演化中的做法更加充分使用記憶體,也比最開始的做法的空閑記憶體更加連續。

在這裡我們可以再往下思考,在第一次演化中,如果每次回收對象特别多,而存活對象特别少,那麼隻需要通過少數的複制操作和一次清除就可以實作回收,此時效率會特别高。而在第二次演化中,如果每次回收對象較少,而存活對象較多,則可以采取此政策進行回收確定最終剩餘的空間是連續的空間。

到這裡其實并不足夠完善,畢竟上述幾種演化都有缺點和優點,有沒有辦法可以取長補短呢?

在開發中,其實我們可以發現,大多數對象都是在方法體中new出來,new完使用後就不再使用了,此時該對象即可進行回收。是以這一類的對象有個特點就是朝生夕死。假如在方法執行完,該對象的引用還被持有着,證明該對象是比較重要的對象,越到後面要回收則越來越困難。這個情況不就剛好符合上述兩種演化的情況,當對象剛出生時,我們可以将其使用演化一的方式進行回收,當使用演化一的方式回收不了的對象,則證明該對象為比較重要的對象,我們就可以采用演化二的方式進行回收。這樣我們可以對我們的記憶體進行分區

第一次分區

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

當new對象時,記憶體均從上圖右上方的區域申請,當右上方的記憶體區域滿時,則進行一次複制算法進行垃圾回收。從上面的思考我們知道,絕大多數新對象都有朝生夕死的特點,是以在這次的垃圾回收中,存活的對象寥寥無幾,然後存活的對象全部塞到右下方區域。在下一次垃圾回收到來時,根據上述分析,之前存活的對象絕大多數還會繼續存活,我們将經曆過一次垃圾回收的對象年齡+1,可見大多數的對象都熬不過兩歲,一般在一歲時就被回收了。而當對象經曆了多次垃圾回收仍然存活,此時它很難被回收了,我們可以将其移到左邊的區域,另外右邊上下倆區域都滿了時,則通過垃圾回收将存活對象的那一邊區域也移動到左邊區域中。當左邊區域滿時,可通過标記-壓縮算法進行垃圾回收。在這種分區方式中,左側區域稱之為老年代,而右側區域則為新生代。新生代使用複制算法進行一次垃圾回收,稱之為Minor GC,而複制完後如果老年代區域不夠,也會觸發老年代使用标記-壓縮算法進行垃圾回收,稱之為Major GC,一般Major GC會伴随着Minor GC,是以也稱為Full GC。

在上述分區中,新生代仍然隻有一半的區域可以用,之前使用一半區域的原因是考慮到有可能所有對象都是存活對象,這樣才足夠完全複制,但現在有老年代的存在,再考慮到此區域每一次回收時僅有少數對象需要複制,分區方式是否還有優化的空間呢?

第二次分區

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

這個分區是在第一次分區的基礎上,将新生代分為三部分,分别是伊甸園、幸存區S0,幸存區S1,伊甸園記憶體占比為8:1:1,S0與S1大小相同。對象的一生如下:

①所有對象都在伊甸園出生,當伊甸園占滿時,開始進行一次Minor GC,此次GC會将已存活的對象複制到S0區中

②伊甸園區又被占滿,此時又進行一次Minor GC,伊甸園存活的對象又複制到S0區。

③在若幹次GC後,幸存區S0也滿了,此時Minor GC會對伊甸園和幸存區S0的

做一次垃圾回收,将兩個區存活的對象複制到幸存區S1中,再把伊甸園和S0清空,最後把S1的記憶體與S0交換,此時S1又騰空了,S0剩下一些老對象。

④又經曆若幹次GC,幸存區S0已經放滿了經曆過N次GC都回收不了的老對象,此時會将老對象複制到老年代中,騰空幸存區。

⑤并非當幸存區被老對象占滿才複制到老年代中,當老對象年齡達到15歲,即經曆過15次GC都還活着的,也會複制到老年代中,另外伊甸園中如果誕生了一個比幸存區還大的對象,那麼該對象回收不了時,也會直接送入到老年代中。

⑥又經曆過若幹次GC後,老年代也滿了,那麼此時它會進行一次Major GC。

動圖示範

上述過程使用文字描述可能比較抽象,下面用動圖簡單來示範一次Minor GC。

關于JVM記憶體模型的了解一、概念二、垃圾回收涉及的算法三、JVM常用參數四、附錄-示範代碼

二、垃圾回收涉及的算法

在垃圾回收中,涉及的算法主要有以下五個

  • 引用計數算法
  • 可達性分析
  • 标記-回收算法
  • 标記-壓縮算法
  • 複制算法

    前兩個算法用于判斷對象是否需要回收,其原理簡單講,引用計數算法就是計算對象被誰引用,一旦有其它對象引用此對象,引用次數加一,而GC時引用次數大于零的對象則判斷為存活對象,但此算法無法解決循環引用問題,如A引用B,B引用A,此時A與B均無法回收,是以現在JVM不采用此算法;而可達性算法則從GCRoot出發,若A引用B,B引用C,則通過A可以到達C,此時ABC三個對象均不進行回收。後面的三個算法為回收政策,其思路在第一章有提及,在這裡就不加贅述,下面總結一下三個算法優缺點:

算法 優點 缺點
标記-回收算法 暫無 标記和清除效率低、可用空間不連續
标記-壓縮算法 實作簡單,運作高效 記憶體空間利用不充分
複制算法 記憶體空間使用率高 性能較低

三、JVM常用參數

  • Xss:每個線程的棧大小
  • Xms:堆空間的初始值
  • Xmx:堆空間最大值、預設為實體記憶體的1/4,一般Xms與Xmx最好一樣
  • Xmn:年輕代的大小
  • XX:NewRatio :新生代和年老代的比例
  • XX:SurvivorRatio :伊甸園區和幸存區的占用比例
  • XX:PermSize:設定記憶體的永久儲存區域(1.8已廢除)
  • XX:MetaspaceSize:1.8使用此參數替代上述參數
  • XX:MaxPermSize:設定最大記憶體的永久儲存區域(1.8已廢除)
  • XX:MaxMetaspaceSize:1.8使用此參數替代上述參數

四、附錄-示範代碼

此代碼為第一章中動圖所使用的測試代碼

public class OutOfMemoryErrorTest {
    public static void main(String[] args) throws Throwable {
        Random r = new Random();
        List<TestObject[]> testObjectList = new ArrayList<TestObject[]>();
        while (true) {
            try {
                TestObject[] testObjects = new TestObject[];
                // 模拟30%左右的對象為有用對象
                if (r.nextInt() > ) {
                    testObjectList.add(testObjects);
                }
                Thread.sleep();
            } catch (Throwable t) {
                throw t;
            }
        }
    }
}

class TestObject {

    private String name;

    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}