Java棧,PC寄存器,本地方法棧,堆,方法區和運行常量池

最近在看《深刻理解Java虛擬機》,書中給了幾個例子,比較好的說明了幾種OOM(OutOfMemory)產生的過程,大部分的程序員在寫程序時不會太關注Java運行時數據區域的結構:java

感受有必要經過幾個實在的例子來加深對這幾個區域的瞭解。程序員

Java程序運行時,數據會分區存放,JavaStack(Java棧)、 heap(堆)、method(方法區)。小程序

虛擬機的內部結構

一、Java棧

Java棧的區域很小,只有1M,特色是存取速度很快,因此在stack中存放的都是快速執行的任務,基本數據類型的數據,和對象的引用(reference)。app

駐留於常規RAM(隨機訪問存儲器)區域。但可經過它的「棧指針」獲取處理的直接支持。棧指針若向下移,會建立新的內存;若向上移,則會釋放那些內存。這是一種特別快、特別有效的數據保存方式,僅次於寄存器。建立程序時,Java編譯器必須準確地知道堆棧內保存的全部數據的「長度」以及「存在時間」。這是因爲它必須生成相應的代碼,以便向上和向下移動指針這一限制無疑影響了程序的靈活性,因此儘管有些Java數據要保存在棧裏——特別是對象句柄,但Java對象並不放到其中。函數

JVM只會直接對JavaStack(Java棧)執行兩種操做:①以幀爲單位的壓棧或出棧;②經過-Xss來設置, 若不夠會拋出StackOverflowError異常。測試

1.每一個線程包含一個棧區,棧中只保存基本數據類型的數據和自定義對象的引用(不是對象),對象都存放在堆區中
2.每一個棧中的數據(原始類型和對象引用)都是私有的,其餘棧不能訪問。
3.棧分爲3個部分:基本數據類型的變量區、執行環境上下文、操做指令區(存放操做指令)。this

棧是存放線程調用方法時存儲局部變量表,操做,方法出口等與方法執行相關的信息,Java棧所佔內存的大小由Xss來調節,方法調用層次太多會撐爆這個區域。spa

二、程序計數器(ProgramCounter)寄存器

PC寄存器( PC register ):每一個線程啓動的時候,都會建立一個PC(Program Counter,程序計數器)寄存器。PC寄存器裏保存有當前正在執行的JVM指令地址。 每個線程都有它本身的PC寄存器,也是該線程啓動時建立的。保存下一條將要執行的指令地址的寄存器是 :PC寄存器。PC寄存器的內容老是指向下一條將被執行指令的地址,這裏的地址能夠是一個本地指針,也能夠是在方法區中相對應於該方法起始指令的偏移量。線程

三、本地方法棧

Nativemethodstack(本地方法棧):保存native方法進入區域的地址。翻譯

四、堆

類的對象放在heap(堆)中,全部的類對象都是經過new方法建立,建立後,在stack(棧)會建立類對象的引用(內存地址)。

一種常規用途的內存池(也在RAM(隨機存取存儲器 )區域),其中保存了Java對象。和棧不一樣:「內存堆」或「堆」最吸引人的地方在於編譯器沒必要知道要從堆裏分配多少存儲空間,也沒必要知道存儲的數據要在堆裏停留多長的時間。所以,用堆保存數據時會獲得更大的靈活性。要求建立一個對象時,只需用new命令編輯相應的代碼便可。執行這些代碼時,會在堆裏自動進行數據的保存。固然,爲達到這種靈活性,必然會付出必定的代價:在堆裏分配存儲空間時會花掉更長的時間。

JVM將全部對象的實例(即用new建立的對象)(對應於對象的引用(引用就是內存地址))的內存都分配在上,堆所佔內存的大小由-Xmx指令和-Xms指令來調節,sample以下所示:

public class HeapOOM {              
    static class OOMObject{}          
    /**       
     * @param args       
     */       
    public static void main(String[] args) {           
        List list = new ArrayList();// List類和ArrayList類都是集合類,
                                    // 可是ArrayList能夠理解爲順序表,
                                    // 屬於線性表。                      
        while (true) {               
            list.add(new OOMObject());           
        }       
    }      
}


加上JVM參數-verbose:gc -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError,就能很快報出OOM異常(內存溢出異常):

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

而且能自動生成Dump。

五、方法區

method(方法區)又叫靜態區,存放全部的①類(class),②靜態變量(static變量),③靜態方法,④常量和⑤成員方法。

1.又叫靜態區,跟堆同樣,被全部的線程共享。

2.方法區中存放的都是在整個程序中永遠惟一的元素。這也是方法區被全部的線程共享的緣由。

(順便展開靜態變量和常量的區別: 靜態變量本質是變量,是整個類全部對象共享的一個變量,其值一旦改變對這個類的全部對象都有影響;常量一旦賦值後不能修改其引用,其中基本數據類型的常量不能修改其值。)

Java裏面是沒有靜態變量這個概念的,不信你本身在某個成員方法裏面定義一個static int i = 0;Java裏只有靜態成員變量。它屬於類的屬性。至於他放哪裏?樓上說的是靜態區。我不知道到底有沒有這個翻譯。可是深刻JVM裏是翻譯爲方法區的。虛擬機的體系結構:①Java棧,② 堆,③PC寄存器,④方法區,⑤本地方法棧,⑥運行常量池。而方法區保存的就是一個類的模板,堆是放類的實例(即對象)的。棧是通常來用來函數計算的。隨便找本計算機底層的書都知道了。棧裏的數據,函數執行完就不會存儲了。這就是爲何局部變量每一次都是同樣的。就算給他加一後,下次執行函數的時候仍是原來的樣子。

方法區的大小由-XX:PermSize和-XX:MaxPermSize來調節,類太多有可能撐爆永久代。靜態變量常量也有可能撐爆方法區。 

六、運行常量池

這兒的「靜態」是指「位於固定位置」。程序運行期間,靜態存儲的數據將隨時等候調用。可用static關鍵字指出一個對象特定元素是靜態的。但Java對象自己永遠都不會置入靜態存儲空間。

這個區域屬於方法區。該區域存放類和接口的常量,除此以外,它還存放成員變量和成員方法的全部引用。當一個成員變量或者成員方法被引用的時候,JVM就經過運行常量池中的這些引用來查找成員變量成員方法內存中的的實際地址

七、舉例分析

例子以下:

爲了更清楚地搞明白程序運行時,數據區裏的狀況,咱們來準備2個小道具(2個很是簡單的小程序)。

// AppMain.java
public class AppMain {                         //運行時,JVM把AppMain的信息都放入方法區    

    public static void main(String[] args) { //main成員方法自己放入方法區。    
        Sample test1 = new  Sample( " 測試1 " );   //test1是引用,因此放到棧區裏,Sample是自定義對象應該放到堆裏面    
        Sample test2 = new  Sample( " 測試2 " );         
        test1.printName();    
        test2.printName();    
    }
    
} 

// Sample.java       
public class Sample {   //運行時,JVM把appmain的信息都放入方法區。            

    private  name;      //new Sample實例後,name引用放入棧區裏,name對象放入堆裏。     

    public  Sample(String name) {    
        this.name = name;    
    }          
        
    public   void  printName() {// printName()成員方法自己放入方法區裏。    
        System.out.println(name);    
    }    

}

OK,讓咱們開始行動吧,出發指令就是:「java AppMain」,包包裏帶好咱們的行動向導圖。



系統收到了咱們發出的指令,啓動了一個Java虛擬機進程,這個進程首先從classpath中找到AppMain.class文件,讀取這個文件中的二進制數據,而後把Appmain類的類信息存放到運行時數據區的方法區中。這一過程稱爲AppMain類的加載過程。

接着,JVM定位到方法區中AppMain類的Main()方法的字節碼,開始執行它的指令。這個main()方法的第一條語句就是:

Sample test1 = new Sample("測試1");

語句很簡單啦,就是讓JVM建立一個Sample實例,而且呢,使引用變量test1引用這個實例。貌似小case一樁哦,就讓咱們來跟蹤一下JVM,看看它到底是怎麼來執行這個任務的:

一、Java虛擬機一看,不就是創建一個Sample類的實例嗎,簡單,因而就直奔方法區(方法區存放已經加載的類的相關信息,如類、靜態變量和常量)而去,先找到Sample類的類型信息再說。結果呢,嘿嘿,沒找到@@,這會兒的方法區裏尚未Sample類呢(即Sample類的類信息尚未進入方法區中)。可JVM也不是一根筋的笨蛋,因而,它發揚「本身動手,豐衣足食」的做風,立馬加載了Sample類, 把Sample類的相關信息存放在了方法區中。

二、Sample類的相關信息加載完成後。Java虛擬機作的第一件事情就是在堆中爲一個新的Sample類的實例分配內存,這個Sample類的實例持有着指向方法區的Sample類的類型信息的引用(Java中引用就是內存地址)。這裏所說的引用,實際上指的是Sample類的類型信息在方法區中的內存地址,其實,就是有點相似於C語言裏的指針啦~~,而這個地址呢,就存放了在Sample類的實例的數據區中。

三、在JVM中的一個進程中,每一個線程都會擁有一個方法調用棧,用來跟蹤線程運行中一系列的方法調用過程,棧中的每個元素被稱爲棧幀,每當線程調用一個方法的時候就會向方法棧中壓入一個新棧幀。這裏的幀用來存儲方法的參數、局部變量和運算過程當中的臨時數據。OK,原理講完了,就讓咱們來繼續咱們的跟蹤行動!位於「=」前的test1是一個在main()方法中定義的變量,可見,它是一個局部變量,所以,test1這個局部變量會被JVM添加到執行main()方法的主線程的Java方法調用棧中。而「=」將把這個test1變量指向堆區中的Sample實例,也就是說,test1這個局部變量持有指向Sample類的實例的引用(即內存地址)

OK,到這裏爲止呢,JVM就完成了這個簡單語句的執行任務。參考咱們的行動向導圖,咱們終於初步摸清了JVM的一點點底細了,COOL!

接下來,JVM將繼續執行後續指令,在堆區裏繼續建立另外一個Sample類的實例,而後依次執行它們的printName()方法。當JVM執行test1.printName()方法時,JVM根據局部變量test1持有的引用,定位到堆中的Sample類的實例,再根據Sample類的實例持有的引用,定位到方法區中Sample類的類型信息(包括①類,②靜態變量,③靜態方法,④常量和⑤成員方法),從而獲取printName()成員方法的字節碼,接着執行printName()成員方法包含的指令。

相關文章
相關標籤/搜索