JVM 內存劃分 棧區 堆區 方法區

如今用一張圖來介紹每一個區域存儲的內容。java

運行時數據區怎麼理解?
JVM運行時首先須要類加載器(classLoader)加載所需類的字節碼文件。加載完畢交由執行引擎執行,在執行過程當中須要一段空間來存儲數據(類比CPU與主存)。這段內存空間的分配和釋放過程正是咱們須要關心的運行時數據區。程序員

運行時數據區
運行時數據區都包括,程序計數器,方法區(包含常量池),虛擬機棧,本地方法棧,堆 。 JVM自己就是一臺虛擬的計算機,目的是爲了實現一次編譯到處執行。算法

程序計數器
程序計數器是一塊較小的內存空間。他能夠看作是當前線程所執行的字節碼行號指示器。字節碼解釋器工做就是經過改變這個計數器的值來選擇下一條須要執行的字節碼指令。分支,循環,跳轉,異常,線程恢復等基礎功能都須要依賴這個計數器來完成。
因爲JVM虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。當切換到另一條線程時,若不保存當前未執行完線程的執行位置,下次處理機再執行這條線程時,又要從新開始執行。這種狀況顯然是不能容忍的。所以,爲了線程切換後能正確的恢復到執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲‘線程私有’內存,
若是線程正在執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址,若是正在執行的是native方法,這個計數器則爲(undefined),此內存區域是爲一個在java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域數組

java虛擬機棧
與程序計數器同樣java虛擬機也是線程私有的,他的生命週期與線程相同,虛擬機棧描述的是java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame,能夠這麼理解棧幀,虛擬機棧包含N個棧幀每一個棧幀包含局部變量表,操做數棧,動態連接,方法出口等信息)。每一個方法從調用到執行完成這個過程,就對應這一個棧幀在虛擬機棧中的入棧到出棧的過程。
常常有人將java內存分爲堆,棧內存,這實際上是很粗糙的,java內存的劃分遠比這複雜。只能說明傳統的程序員最關注的,與對象內存分配關係最密切的就是這兩塊內存堆,棧。棧就是如今說的虛擬機棧,或者說是虛擬機棧中棧幀的局部變量表部分。
局部變量表存放了編譯期可知的各類基本數據類型(boolean,byte,char,short,int,float,long,double),對象引用(reference類型,他不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)
其中64位長度的long和double類型會佔用2個局部變量空間,其他的數據類型只會佔用1個局部變量空間。局部變量表所需的內存空間在編譯期間完成內存分配,當進入一個方法時,這個方法須要在幀中分配多大的內存空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
在java虛擬機規範中,對這個區域規定了兩種異常狀態:若是線程請求的棧的深度大於虛擬機容許的深度,將拋出StackOverFlowError異常(棧溢出),若是虛擬機棧能夠動態擴展(如今大部分java虛擬機均可以動態擴展,只不過java虛擬機規範中也容許固定長度的java虛擬機棧),若是擴展時沒法申請到足夠的內存空間,就會拋出OutOfmMemoryError異常(沒有足夠的內存)服務器

本地方法棧
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,他們之間的區別不過是虛擬機棧爲虛擬機執行java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的本地Native方法服務,在虛擬機規範中對本地方法棧中的使用方法,語言,與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(例如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣本地方法棧也會拋出StackOverFlowError和OutOfmMemoryError異常數據結構

java堆
對於大多數應用來講,java堆(java Heap)是java虛擬機管理內存中的最大一塊。java堆是全部線程共享的一塊內存管理區域,在虛擬機啓動時建立。此內存區域惟一目的就是存放對象的實例。幾乎全部對象實例都在堆中分配內存。這一點在java虛擬機規範中的描述是:全部對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展與逃逸技術逐漸成熟,棧上分配,標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也不是變的那麼「絕對」了。
java堆是垃圾回收器管理的主要區域,所以不少時候也被稱爲GC堆(Garbage Collected Heap)。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法,因此java堆中還能夠細分爲:新生代和年老代:在細緻一點的劃分能夠分爲:Eden空間,From Survivor空間,To Survivor空間等。從內存分配的角度來看,線程共享的java堆中可能劃分出多個線程私有的分配緩衝區 ,不過不管如何劃分,都與存放內容無關,不管哪一個區域存放的都是對象實例。進一步劃分的目的是爲了更好的回收內存,或者更快的分配內存。
根據java虛擬機規範的規定,java堆能夠處在物理上不連續的內存空間,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實現上既能夠實現成固定大小,也能夠是可擴展的大小,不過當前主流的虛擬機都是按照可擴展來實現的(經過-Xmx和-Xms控制)。若是在堆中沒有內存實例完成分配,而且堆也沒法在擴展時將會拋出OutOfMemoryError異常。多線程

方法區
方法區和java堆同樣,是各個線程共享的內存區域,他用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。雖然java虛擬機規範把方法區描述爲堆的一部分,可是他還有個別名叫作Non-heap(非堆),目的應該是與java堆區分開來。
java虛擬機規範對方法區的限制很是寬鬆,除了和java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集。相對而言,垃圾收集在這個區域是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣永久存在了。這區域的內存回收目標重要是針對常量池的回收和類型的卸載,通常來講這個內存區域的回收‘成績’比較難以使人滿意。尤爲是類型的卸載條件很是苛刻,可是這部分的回收確實是必要的。在sun公司的bug列表中,曾出現過的若干個嚴重的bug就是因爲低版本的HotSpot虛擬機對此區域未完成回收致使的內存溢出。app

這裏有三個概念須要清楚
1 常量池(Constant Pool): 常量池數據編譯器被肯定,是class文件中的一部分,存儲了類,方法,接口等中的常量,固然也包括字符串常量。
常量池:能夠理解爲Class文件之中的資源倉庫,它是Class文件結構中與其餘項目資源關聯最多的數據類型
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。
字面量:文本字符串、聲明爲final的常量值等;
符號引用:類和接口的徹底限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符
2 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲了編譯器產生的字符串類型數據jvm

3 運行時常量池(Runtime Constant Pool): 方法區的一部分,全部線程共享。虛擬機加載class文件後把常量池中的數據存放到運行時常量池中
這裏須要注重提一下在JDK1.6以前字符串常量池是存在於方法區之中,在JDK1.7和以上字符串常量池存在了堆之中。
這是官網翻譯後的中文說明:在JDK 7中,在Java堆的永久生成中再也不分配interned字符串,而是在Java堆的主要部分(稱爲young和old generation)中分配,以及應用程序建立的其餘對象。此更改將致使更多的數據駐留在主Java堆中,而在永久生成中數據更少,所以可能須要調整堆大小。因爲這種變化,大多數應用程序在堆使用上只會看到相對較小的差別,可是更大的應用程序加載了許多類,或者大量使用了string . intern()方法將看到更顯著的差別。若是還有疑問可進行測試,測試參考http://blog.csdn.net/u014039577/article/details/50377805函數

直接內存
直接內存並非虛擬機運行內存的一部分,也不是java虛擬機規範中定義的內存區域。可是這部份內存區域也被頻繁的使用,也可能致使OutOfMemoryError異常出現,
在jdk1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)和緩衝區(Buffer)的I/O方式,他可使用本地的函數庫直接分配堆外內存,而後經過一個存儲在java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做,這樣能在一些場景中顯著提升性能,由於避免了在java堆中和Native堆中來回複製數據。
顯然本機直接內存的分配不會受到java堆大小的限制,可是既然是內存。確定還會受到本機總內存的限制。服務器管理員在配置虛擬機內存參數時,會根據實際內存設置-Xmx等參數信息。但常常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理和操做系統級的限制),從而致使動態擴展時OutOfMemoryError異常。

=============================================================================================

案例演示:

demo1

數據準備

main類

//運行時, jvm 把AppMain的信息都放入方法區 
public class AppMain {
    //main方法自己放入方法區
    public static void main(String[] args) {
        Sample test1 = new Sample("測試1");
        test1.printName();
    }
}

Sample 類

public class Sample {
    private String name;

    //new Sample實例後,引用放入棧區,  對象放入堆
    public  Sample(String name){
        this.name = name;
    }

    //printName方法自己放入 方法區
    public void printName(){
        System.out.println(name);
    }
}

在JVM中的棧,堆,方法區的交互

這裏寫圖片描述

=============================================================================================

demo2

public class baseTest {
    public static void main(String[] args) {
        //建立Car類實例,開始經過JVM向內存加載
        Car car1 = new Car(4, "red");
        car1.show();

        Car car2 = new Car(2, "black");
        car2.show();
    }
}

class Car{
    private int wheelNum;//成員變量(堆)
    private String color;//成員變量(堆)

    public Car() {

    }

    public Car(int w, String c) {//形參w,c爲局部變量(棧)
        this.wheelNum = w;
        this.color = c;
    }

    public void show(){//方法名show放到方法區
        System.out.println("car:wheelNum-"+wheelNum+"color-"+color);
    }
}

=============================================================================================

接下來使用一個小例子來對堆棧進行更深一步的瞭解

String str = 「a」;
String strr = 「bc」;
String str1 = "abc";     //定義字符串變量str1
        String str2 = "abc";      //定義字符串變量str2
        String str3 = new String("abc"); //以new的方式定義字符串變量str3
        String str4 = new String("abc");//以new的方式定義字符串變量str4

String str5 = str + strr;
String str6 = 「a」 + 「bc」;
       結果:
         * str1 ==str2    true;     ①
         * str2 ==str3    false;     ②
         * str3 ==str4    false;    ③
         *str1 == str5  false;       ④
         *str1.equals(str5)   true;   ⑤
         *str1==str6        true;   ⑥
講解:

==’比較的是地址
     Str1 和 str2顯然指向的是String中常量池中的一個地址。(何靈鴻上次發的String的兩種建立方式)

equals比較的是內容

①  因爲地址相同,因此true
②  因爲str3 使用的new ,至關於在String類堆中建立了一個堆地址。而str2的地址則是在常量池中。地址對不上,所以false
③  和②的原理是同樣的,至關於new了兩個對象,也就是各自擁有了本身的地址。所以false。
④  str5使用的是字符串變量的相加。當看String的源碼的時候,你會發現str.append()方法。本質上是先new 一個Stringbuilder對象,而後使用這個對象進行append,最後Builder對象toStirng回到String類型。也就是地址發生了變化。

結果: ⑤  比較的是內容,因此爲true ⑥  也許看到這個會感到迷茫,一開始我也迷茫。後來一想,這種狀況str6則是在常量池中進行的拼接(若存在,則直接指向;若不存在,拼接建立)

相關文章
相關標籤/搜索