1. 程式人生 > 實用技巧 >深入理解java虛擬機器--Java記憶體區域和記憶體溢位異常

深入理解java虛擬機器--Java記憶體區域和記憶體溢位異常

前言

對於Java程式設計師來說,在虛擬機器自動記憶體管理的機制下,不需要為每一個new操作去寫配對的delete/free程式碼,不容易出現記憶體洩漏和記憶體溢位的問題。不過,如果在編寫程式時沒有合理的建立物件,就會造成記憶體洩漏或者溢位這樣的問題,如果不了虛擬機器記憶體的區域劃分以及建立的物件時虛擬機器對它的記憶體分配情況,那麼排查這些錯誤就會比較困難。下面我們來具體介紹一下Java記憶體區域以及記憶體溢位的相關知識。

Java執行時資料區域

java虛擬機器在程式執行時把它管理的記憶體劃分為若干個資料區域,這些區域都有各自的用途,以及建立和銷燬時間,有的區域隨著虛擬機器程序的啟動而存在,有的區域則依賴使用者執行緒的啟動和結束而建立和銷燬。它包括一下幾個執行時資料區域,如下圖所示。

圖1 java執行時資料區圖

程式計數器

程式計數器是比較小的一塊兒記憶體區域,它可以看做是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器的概念模型了,位元組碼直譯器就是通過改變程式計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要這個程式計數器來完成。
它屬於執行緒隔離的資料區,因為虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的。在任何一個確定的時刻,一個處理器或者一個多核處理器的一個核心只能來執行一個執行緒中的指令。因此為了執行緒切換後能夠恢復到正確的執行位置,每個執行緒都需要有一個獨立的程式計數器,所以程式計數器它是執行緒私有的

JVM虛擬機器棧

虛擬機器棧也是執行緒私有的,它和程式計數器的生命週期是相同的。虛擬機器棧它描述的是java方法執行的記憶體模型:每個方法在執行的同時都換建立一個棧幀,用來儲存區域性變量表運算元棧動態連結方法出口等資訊。每個方法從呼叫到執行完成的過程,就對應一個棧幀在虛擬機器棧中入棧到出棧的過程。
區域性變量表儲存了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、long、float、double)、物件的引用(reference型別,它不是物件本身,它只是指向了物件起始地址的引用指標,或者指向一個代表物件的控制代碼或者其他與此物件相關的位置)和returnAddress型別

(指向一條位元組碼指令的地址)
在該區域中,規定了兩種異常狀態如果執行緒請求棧深度大於虛擬機器所允許的深度,則會丟擲StackOverFlowError異常如果虛擬機器可以動態擴充套件(當前大部分虛擬機器都能夠動態擴充套件),如果無法申請到足夠的記憶體,那麼就會丟擲OutOfMemoryError異常

本地方法棧

本地方法棧和虛擬機器棧非常類似,它是用來執行虛擬機器呼叫到的Native方法。在虛擬機器規範中對虛擬機器使用的語言、使用方式和資料結構並沒有強制的規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(sun hotspot)直接就把本地方法棧和虛擬機器棧合為一個。與虛擬機器棧一樣,它也會丟擲StackOverFlowError和OutOfMemoryError異常。

Java堆

java堆是虛擬機器管理的最大的一塊記憶體,在虛擬機器的啟動時建立。Java堆是被所有執行緒共享的一塊區域,幾乎所有物件的例項都存放在這塊區域
java堆是垃圾收集管理的主要區域,所以也被叫做GC堆。從記憶體回收的角度來看,由於現在的收集器基本都採用分代收集的演算法,所以java堆還可以細分為:新生代和老年代;再細分還有Eden空間、From Survivor空間、To Survivor空間。關於這些區域的分配和回收,我們會在之後的文章進行詳細介紹。
java堆可以是物理上不連續的一片儲存空間,只要邏輯上是連續的一片記憶體空間即可。在實現時,既可以固定大小的也可以是可擴充套件的,**當前主流的虛擬機器都是按照可擴充套件的來實現的(通過-Xmx和-Xms來控制)**。如果在堆中沒有完成記憶體的分配,則會丟擲OutOfMemoryError的異常。

方法區

方法區和java堆記憶體一樣,都是執行緒共享的記憶體區域。它是用來存放已被虛擬機器載入的類資訊常量靜態變數即時編譯器編譯後的程式碼等資料。方法區從記憶體回收的角度來講,可以被叫做永久代但其實兩者是不等價的,只是因為HotSpot虛擬機器的設計團隊把GC分代收集擴充套件至方法區,或者說使用永久代來實現方法區。其實這樣的方案更容易使方法區出現記憶體溢位,所以在jdk1.7的hotspot中,已經把原本放在方法區中的靜態變數、字串常量池等移到堆記憶體中。在jdk1.8中,永久代已經不存在,儲存的類資訊、編譯後的程式碼資料等已經移動到了元資料空間(MetaData Space)中,元空間並沒有處於堆記憶體上,而是直接佔用的本地記憶體(NativeMemory),而常量池被移動到了堆記憶體中
這個區域的垃圾回收目標主要是對常量池的回收和型別的解除安裝。尤其是型別的解除安裝的回收,要求很苛刻,但這部分的回收是很必要的。當方法區無法滿足記憶體的分配需求時,會丟擲OutOfMemoryError的異常。

執行時常量池

執行時常量池是方法區的一部分,class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項就是常量池,用於存放編譯期生成的字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池存放
(此處有待於確定)字串池裡的內容是在類載入完成,經過驗證,準備階段之後在堆中生成字串物件例項,然後將該字串物件例項的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的例項物件,具體的例項物件是在堆中開闢的一塊空間存放的。)。它屬於方法區的一部分,同樣也會因為記憶體沒有滿足分配需求而丟擲OOM的異常。

類被載入後各個型別變數的存放位置

下邊我們通過一段程式碼來分析一下:

public class Demo {
    public static void main(String aa[]) {
        A a =new A();
        a.print();
    }
}

//建一個A類
class A {
    //成員變數 num欄位名存放在方法區(元空間)中,值存在堆記憶體中
    private int num = 0;
    //欄位名name存放在方法區(元空間)中,字串值存放在方法區的字串常量池中(或者堆記憶體中)
    private String name = "default name";
    //欄位名temperature存放在方法區(元空間)中,數值存放在方法區的常量池中(或者堆記憶體中)
    private static double temperature = 36.5;

    public String getName() {
        return name;
    }

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

    public void print(){
          //臨時變數名和值存放在虛擬機器棧中
         int age = 10;
         //字串名存在虛擬機器中,值存在方法區的字串常量池中(或者堆記憶體中)
         String msg = "this is A";
        System.out.println(msg + " ,and my age is " + age);
    }
}

上邊程式碼中,當JVM載入了類Demo和A之後,這兩個類的類資訊被存放在方法區中(jdk1.8以後存放在元資料區域),比如符號引用(包括:1.類的全限定名,2.欄位名和描述符,3.方法名和描述符)這些都儲存在方法區的常量池中;而這個類在main方法被例項化之後,它的num欄位對應的值存放在堆記憶體中,temperaturename欄位因為是靜態變數,所以存放在方法區中;name字串的值存放在方法區的字串常量池中。當呼叫print方法時,裡邊的age是一個基礎型別的臨時變數,它的名字和值都存放在了虛擬機器棧中,而msg是一個字串,它的值存放在方法區的字串常量池(或者元資料空間)中;如果該方法中有一個類的例項,那麼它的引用存在虛擬機器棧中,而實際的例項物件資料存放在堆記憶體中。如果類中有一個類的成員變數,那麼這個類的引用和例項物件都存在堆記憶體中。

記憶體溢位異常

在講記憶體溢位之前,我們先來了解一下兩個概念。

記憶體洩漏

記憶體洩漏(memory leak)是指程式在申請記憶體後,無法釋放已經申請的記憶體空間,多次的記憶體洩漏就會導致記憶體被佔滿。

記憶體溢位

記憶體溢位(out of memory)是指程式在申請記憶體時,沒有足夠的記憶體空間供使用,導致OOM。

參考文獻

《深入理解Java虛擬機器》

----------------------------------------未完待續-----------------------------------------