首頁  >  文章  >  Java  >  探索Java記憶體分配

探索Java記憶體分配

一个新手
一个新手原創
2017-09-06 10:28:121195瀏覽

引子

這兩天有個同事抓耳撓腮地糾結:Java到底是值傳遞還是引用傳遞。百思不得其姐,他將這個問題拋給大家一起討論。於是,有的人說傳值,有的人說傳引用;不管哪方都覺得自己的理解是正確無誤的。我覺得:要回答這個問題不妨先擱置這個問題,先往這個問題的上游走走──Java記憶體分配。一提到記憶體分配,我想不少人的腦海裡都會浮現一句話:引用放在棧裡,物件放在堆裡,棧指向堆。嗯哼,這句話聽起來沒有錯;但我們繼續追問一下:這個棧是什麼棧?是龍門客棧麼?非也!它其實是Java虛擬機器堆疊。呃,到了此處,好學的童鞋忍不住要追問了:啥是Java虛擬機棧呢?不急,我們一起來瞅瞅。


JVM的生命週期

我們知道:每個Java程式都在Java虛擬機器上運行;也就是說:一個執行階段的Java虛擬機器負責執行一個Java程式。當啟動一個Java程式時,一個虛擬機器實例也就隨之誕生了;當程式執行完畢後這個虛擬機器實例也就隨之消亡。例如:在一台電腦上同時執行五個Java程序,那麼系統將提供五個Java虛擬機器實例;每個Java程式獨自運行於它自己所對應的Java虛擬機器實例中。

Java虛擬機中有兩種線程,分別是:守護線程與非守護線程。守護線程通常由虛擬機器本身使用,例如執行垃圾收集的線程。非守護線程,通常指的是我們自己的線程。當程式中所有的非守護執行緒都終止時,虛擬機器執行個體會自動退出。


JVM執行階段資料區

既然Java虛擬機負責執行Java程序,那我們就先來看看Java虛擬機體系結構,請參見下圖:


探索Java記憶體分配

在這裡可以看到:class檔案由類別載入器載入JVM中運作。此處,我們將重點放在藍色線框中JVM的Runtime Data Areas(運行時資料區),它表示JVM在運行期間對記憶體空間的劃分和分配。在這個資料區內分為以下幾個主要區域:Method Area(方法區),Heap(堆疊),Java Stacks(Java 堆疊),Program Counter Register(程式計數器),Native Method Stack(本地方法堆疊),現各區域的主要作用及其特徵如下詳細介紹。

Method Area(方法區)

Method Area(方法區)是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類別信息、常數、靜態變數、編譯器編譯後的程式碼等資料。根據Java 虛擬機器規格的規定,當方法區無法滿足記憶體分配需求時,將拋出OutOfMemoryError(OOM)異常。為了進一步了解Method Area(方法區),我們來看在該區域內包含哪些具體組成部分。

(1) 執行時間常數池

Class檔案中除了有類別的版本、欄位、方法、介面等描述等與類別緊密相關的資訊之外,還有一個常數池用於存放編譯期產生的各種字面量和符號引用;該常數池將在類別載入後被存放到方法區的運行時常數池中。換句話說:在運行時常數池中存放了該類別使用的常數的一個有序集合,它在java程式的動態連接中起著非常重要的作用。在該集合中包括直接常數(string,integer和,floating point等)和對其他類型、字段和方法的符號引用。外界可透過索引存取運行時常數池中的資料項,這一點和存取數組非常類似。當然,執行時間常數池是方法區的一部分,它也會受到方法區記憶體的限制,當執行時間常數池無法再申請到記憶體時也會拋出OutOfMemoryError(OOM)例外。

(2) 類型資訊

在該部分包含:

  • ##類型的完全限定名

  • 類型的直接超類別的全限定名稱

  • 類型是類別類型還是介面類型

  • 類型的存取修飾符(public、abstract、final等)

  • 直接超介面的全限定名的有序列表

##( 3) 字段資訊

字段資訊用於描述該類別中聲明的所有字段(局部變數除外),它包含以下具體資訊:

##字段名稱
  • 欄位類型
  • 欄位的修飾符
  • ##欄位的順序

  • (4) 方法資訊

方法資訊用於描述該類別中宣告的所有方法,它包含以下具體資訊:

#方法名稱

  • 方法的傳回類型

  • #方法輸入參數的數,類型,順序

  • 方法的修飾符

  • #運算元堆疊

  • 在幀堆疊中的局部變數區的大小

#(5) 類別變數

該部分用於存放類別中static修飾的變數。

(6) 指向類別載入器的參考

類別由類別載入器載入,JVM會在方法區保留指向該類別載入器的參考。

(7) 指向Class實例的參考

在類別被載入器載入的過程中,虛擬機會建立一個代表該類別的Class對象,同時JVM會保留在方法區指向該Class的參考。

Program Counter Register(程式計數器)

Program Counter Register(程式計數器)在Runtime Data Areas(運行時資料區)只佔據非常小的記憶體空間,它用於儲存下一條即將執行的字節碼指令的位址。

Java Stacks(Java 堆疊)

Java Stacks(Java 堆疊)也稱為虛擬機器堆疊(VM Stack),也就是我們通常說的堆疊。它用來描述的Java 方法執行的記憶體模型:每個方法執行的時候都會同時建立一個堆疊幀(Stack Frame)用於儲存局部變數表、操作堆疊、動態連結、方法出口等資訊。每一個方法被呼叫直到執行完成的過程,就對應一個堆疊幀在虛擬機器棧中從入棧到出棧的過程。 Java Stacks(Java 堆疊)的生命週期與執行緒相同;當一個執行緒執行完畢那麼該堆疊也被清空。

Native Method Stack(本機方法堆疊)

Native Method Stack(本機方法堆疊)與Java Stacks(Java 堆疊)非常類似,它用於儲存呼叫本機方法(C/C++)所涉及的局部變數表、操作棧等資訊。

Heap(堆)

Heap(堆)在虛擬機器啟動時創建,用於存放物件實例,幾乎所有的物件實例都在這裡分配記憶體。所以,Heap(堆)是Java 虛擬機器所管理的記憶體中最大的一塊,也是垃圾回收器管理的重點區域。

小結

在此對JVM執行時間資料區做一個小結:

  • Method Area(方法區)和Heap(堆)是被所有執行緒共享的記憶體區域。

  • Java Stacks(Java 堆疊)和Program Counter Register(程式計數器)以及Native Method Stack(本地方法堆疊)是各執行緒私有的記憶體區域。

  • 建立一個對象,該物件的參考存放在Java Stacks(Java 堆疊)中,真正的物件實例存放於Heap(堆疊)中。這也是大家常說的:棧指向堆。

  • 除了剛才提到的JVM運行時資料區所涉及到的記憶體以外,我們還需要關注直接記憶體(Direct Memory)。請注意:直接記憶體(Direct Memory)並不是虛擬機器運行時資料區的一部分,也不是Java虛擬機規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError(OOM)異常出現。例如,在使用NIO時它可以使用Native 函數庫直接分配堆外內存,然後透過儲存在Java 堆裡面的DirectByteBuffer物件作為這塊記憶體的參考進行操作。類似的操作,可避免了在Java 堆和Native 堆中來回複製數據,從而提高效能。


Java呼叫方法時的參數傳遞機制

#在呼叫Java方法傳遞參數的時候,到底是傳值還是傳引用呢?面對眾多的爭論,我們還是來瞅瞅程式碼,畢竟程式碼不會說謊。我們先來看一個很簡單的範例:交換兩個int型別的數據,程式碼如下:

package cn.com;/**
 */public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();        
        int number1=9527;        
        int number2=1314;
        System.out.println("main方法中,数据交换前:number1="+number1+" , number2="+number2);
        testMemory.swapData(number1, number2);
        System.out.println("main方法中,数据交换后:number1="+number1+" , number2="+number2);
    }    private void swapData(int a,int b) {
        System.out.println("swapData方法中,数据交换前:a="+a+" , b="+b);        
        int temp=a;
        a=b;
        b=temp;
        System.out.println("swapData方法中,数据交换后:a="+a+" , b="+b);
    }

}

我們在main方法中宣告的兩個變數number1=9527 , number2=1314;然後將這兩個數作為參數傳遞給了方法swapData(int a,int b),並在該方法內交換資料。至於程式碼本身無需再過多的解釋了;不過,請思考輸出的結果是什麼?在您考慮之後,請參閱如下列印資訊:

main方法中,資料交換前:number1=9527 , number2=1314
 swapData方法中,資料交換前:a=9527 , b=1314
 swapData方法中,資料交換後:a=1314 , b=9527
 main方法中,資料交換後:number1=9527 , number2=1314

嗯哼,这和你想的一样么?为什么会是这样呢?还记得刚才讨论Java Stacks(Java 栈)时说的么:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。结合示例的代码:main( )方法在一个栈帧中,swapData( )在另外一个栈帧中;两者彼此独立互不干扰。在main( )中调用swapData( )传入参数时它的本质是:将实际参数值的副本(复制品)传入其它方法内而参数本身不会受到任何影响。也就是说,这number1和number2这两个变量仍然存在于main( )方法所对应的栈帧中,但number1和number2这两个变量的副本(即int a和int b)存在于swapData( )方法所对应的栈帧中。故,在swapData( )中交换数据,对于main( )是没有任何影响的。这就是Java中调用方法时的传值机制——值传递。

嗯哼,刚才这个例子是关于基本类型的参数传递。Java对于引用类型的参数传递一样采用了值传递的方式。我们在刚才的示例中稍加改造。首先,我们创建一个类,该类有两个变量number1和number2,请看代码:

package cn.com;/**
 */public class DataObject {

    private int number1;    
    private int number2;    
    public int getNumber1() {        
    return number1;
    }    public void setNumber1(int number1) {        
            this.number1 = number1;
    }    public int getNumber2() {       
            return number2;
    }    public void setNumber2(int number2) {        
            this.number2 = number2;
    }

}

好了,现在我们来测试交换DataObject类对象中的两个数据:

package cn.com;/**
  */public class TestMemory {

    public static void main(String[] args) {
        TestMemory testMemory=new TestMemory();
        DataObject dataObject=new DataObject();
        dataObject.setNumber1(9527);
        dataObject.setNumber2(1314);
        System.out.println("main方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
        testMemory.swapData(dataObject);
        System.out.println("main方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());
    }    private void swapData(DataObject dataObject) {
        System.out.println("swapData方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());        
        int temp=dataObject.getNumber1();
        dataObject.setNumber1(dataObject.getNumber2());
        dataObject.setNumber2(temp);
        System.out.println("swapData方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2());

    }

}

简单地描述一下代码:在main( )中定义一个DataObject类的对象并为其number1和number2赋值;然后调用swapData(DataObject dataObject)方法,在该方法中交换数据。请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:

main方法中,数据交换前:number1=9527 , number2=1314
 swapData方法中,数据交换前:number1=9527 , number2=1314
 swapData方法中,数据交换后:number1=1314 , number2=9527
 main方法中,数据交换后:number1=1314 , number2=9527

嗯哼,为什么是这样呢?我们通过DataObject dataObject=new DataObject();创建一个对象;该对象的引用dataObject存放于栈中,而该对象的真正的实例存放于堆中。在main( )中调用swapData( )方法传入dataObject作为参数时仍然传递的是值,只不过稍微特殊点的是:该值指向了堆中的实例对象。好了,再结合栈帧来梳理一遍:main( )方法存在于与之对应的栈帧中,在该栈帧中有一个变量dataObject它指向了堆内存中的真正的实例对象。swapData( )收到main( )传递过来的变量dataObject时将其存放于其本身对应的栈帧中,但是该变量依然指向堆内存中的真正的实例对象。也就是说:main( )方法中的dataObject和swapData( )方法中的dataObject指向了堆中的同一个实例对象!所以,在swapData( )中交换了数据之后,在main( )会体现交换后的变化。在此,我们可以进一步的验证:在该swapData( )方法的最后一行添加一句代码dataObject=null ;我们发现打印信息并没有任何变化。因为这句代码仅仅使得swapData( )所对应的栈帧中的dataObject不再指向堆内存中的实例对象但不会影响main( )所对应的栈帧中的dataObject依然指向堆内存中的实例对象。

通过这两个示例,我们进一步验证了:Java中调用方法时的传递机制——值传递。当然,有的人说:基础类型传值,对象类型传引用。其实,这也没有什么错,只不过是表述方式不同罢了;只要明白其中的道理就行。如果,有些童鞋非纠缠着个别字眼不放,那我只好说:PHP是世界上最好的语言。

以上是探索Java記憶體分配的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn