Java虛擬機(一)自動內存管理機制

運行時數據區域

這裏寫圖片描述

一、程序計數器

程序計數器是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。每條線程都會有一個獨立的程序計數器,是線程私有的。

二、Java虛擬機棧

虛擬機棧是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
StackOverflowError:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;
OutOfMemoryError異常:如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常;

三、本地方法棧

本地方法棧與虛擬機棧所發揮的作用是非常相似的,它們之間的區別是虛擬機棧執行Java方法,而本地方法棧則爲虛擬機使用的Native方法。

四、Java堆

Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例。Java堆是垃圾收集器管理的主要區域,現在收集器基本都採用分代收集算法,所以Java堆中還可以細分爲新生代和老年代。

五、方法區

方法區與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯編譯器編譯後的代碼等數據。運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

六、直接內存

在jdk1.4中新加入了NIO類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用操作。這樣能在一些場景中顯著提高性能,因爲避免了再Java堆和Native堆中來回複製數據。

對象的創建

Cat c = new Cat();

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
這裏寫圖片描述
在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。對象所需要的內存的大小在類加載完成後便可完全確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。分配內存的方法有兩種:

  • 指針碰撞(Bump the Pointer)

    假設Java堆中內存是絕對完整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就是僅僅把那個指針向空閒空間那邊挪動一段與對象大小相等的距離,這種分配方式就是指針碰撞。

  • 空閒列表(Free List)

    如果Java堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱爲空閒列表。

內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值,接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存儲在對象的對象頭中(Object Header)中。

在上面的工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,對象創建纔剛剛開始–方法還沒有執行,所有的字段都還爲零。所以,一般來說執行new指令之後會接着執行方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。

對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

對象頭

HotSpot虛擬機的對象頭包括兩部分信息:
第一部分用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID等。
對象頭的另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。如果對象是一個Java數組,那麼對象頭中還必須有一塊用於記錄數組長度的數據。

實例數據

實例數據是對象真正存儲的有效信息,也是在程序代碼所定義的各種類型的字段內容。

對齊填充

起着佔位符的作用

對象的訪問

建立對象是爲了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。
這裏寫圖片描述

虛擬機異常

一、Java堆溢出

Java堆用於存儲對象實例,只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量達到最大堆的容量限制後就會產生內存溢出異常。

package com.code.exception;

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
    static class OOMObject{
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true){
            list.add(new OOMObject());
        }
    }
}

內存溢出(out of memory)和內存泄漏(memory leak)區別:
- 內存溢出:是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;
- 內存泄漏:是指程序在申請內存後,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積後果很嚴重,無論多少內存,遲早會被佔光。memory leak會最終會導致out of memory

如果是內存泄漏,可以使用內存映像分析工具查看泄漏對象到GC Roots的引用鏈。於是能找到泄漏對象是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的。
如果不存在泄漏,那就應當檢查虛擬機的堆參數(-Xmx與Xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。

二、棧溢出

棧容量可以通過-Xss參數設定
兩種異常:
- 如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverFlowError異常。
- 如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

package com.code.exception;

/**
 * VM Args:-Xss128k.
 */
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void statckLeak(){
        stackLength++;
        statckLeak();
    }

    public static void main(String[] args) throws Throwable{
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try{
            oom.statckLeak();
        } catch(Throwable e){
            System.out.println("stack length:"+oom.stackLength);
            throw e;
        }
    }
}

結果:
Exception in thread "main" stack length:18699
java.lang.StackOverflowError
    at com.code.exception.JavaVMStackSOF.statckLeak(JavaVMStackSOF.java:10)
    at com.code.exception.JavaVMStackSOF.statckLeak(JavaVMStackSOF.java:10)
    at com.code.exception.JavaVMStackSOF.statckLeak(JavaVMStackSOF.java:10)
    at com.code.exception.JavaVMStackSOF.statckLeak(JavaVMStackSOF.java:10)
    at com.code.exception.JavaVMStackSOF.statckLeak(JavaVMStackSOF.java:10)
    at com.code.exception.JavaVMStackSOF.statckLeak(JavaVMStackSOF.java:10)
....

在單個線程下,無論是由於棧幀太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。

三、方法區溢出

方法區用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。對於這些區域的測試,基本的思路就是運行時產生大量的類去填滿方法區,直到溢出。

總結:

本篇講解了虛擬機中的內存是如何劃分的,哪部分區域、什麼樣的代碼和操作可能導致內存溢出異常。雖然Java有垃圾收集機制,但內存溢出異常依然會發生,本篇講解了各個區域出現內存溢出的原因,下一篇將講解Java垃圾回收機制爲了避免內存溢出的出現都做了哪些努力。

摘自《深入理解Java虛擬機》

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