首頁  >  文章  >  Java  >  Java講解之記憶體分配之堆、堆疊與常數池分析

Java講解之記憶體分配之堆、堆疊與常數池分析

巴扎黑
巴扎黑原創
2017-08-01 10:56:351493瀏覽

Java記憶體分配主要包括以下幾個區域:

1. 暫存器:我們無法在程式中控制

2. 堆疊:存放基本類型的資料和物件的引用,但物件本身不存放在堆疊中,而是存放在堆中

3. 堆:存放用new產生的資料

4. 靜態域:存放在物件中用static定義的靜態成員

5. 常數池:存放常數

6. 非RAM(隨機存取記憶體)儲存:硬碟等永久儲存空間

******* ************************************************** ********

Java記憶體分配中的堆疊

#  在函數中定義的一些基本類型的變數資料物件的參考變數都在函數的堆疊記憶體中分配。當在一段程式碼區塊定義變數時,Java就在堆疊中為這個變數分配記憶體空間,當變數退出該作用域後,Java會自動釋放掉為該變數所分配的記憶體空間,該記憶體空間可以立即被另作他用。

Java記憶體分配中的堆疊

#  堆記憶體用來存放由new所建立的對象和數組。 在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。

   在堆中產生了一個數組或物件後,還可以在堆疊中定義一個特殊的變量,讓堆疊中這個變數的值等於數組或物件在堆疊記憶體中的首地址,棧中的這個變數就成了數組或物件的引用變數。引用變數就等於是為數組或物件起的名稱,以後就可以在程式中使用堆疊中的引用變數來存取堆疊中的陣列或物件。引用變數就等於是為數組或物件取的名稱。

  引用變數是普通的變量,定義時在堆疊中分配,並引用變數在程式運行到其作用域之外後被釋放。而陣列和物件本身在堆中分配,即使程式運行到使用new 產生陣列或物件的語句所在的程式碼區塊之外,陣列和物件本身佔據的記憶體不會被釋放,陣列和物件在沒有引用變數指向它的時候,才變成垃圾,不能在被使用,但仍然佔據記憶體空間不放,在隨後的一個不確定的時間被垃圾回收器收走(釋放掉)。這也是 Java 比較佔記憶體的原因。 

  實際上,堆疊中的變數指向堆疊記憶體中的變量,這就是Java中的指標!  

常數池(constant pool)

  常數池指的是在編譯期被確定,並保存在已編譯的.class檔案中的一些資料。除了包含程式碼中所定義的各種基本型別(如int、long等等)和物件型(如String及陣列)的常數值(final)也包含一些以文字形式出現的符號引用,例如: 

  1. 類別和介面的全限定名稱;

  2. 欄位的名稱和描述符; 

  3. 方法和名稱和描述符。

  虛擬機器必須為每個被裝載的類型維護一個常數池。常量池就是該類型所用到常數的一個有序集和,包括直接常數(string,integer和 floating point常數)和對其他類型,字段和方法的符號引用。

  對於String常數,它的值是在常數池中的。而JVM中的常數池在記憶體當中是以表的形式存在的, 對於String類型,有一張固定長度的CONSTANT_String_info表用來儲存文字字串值,注意:該表只儲存文字字串值,不儲存符號引用。說到這裡,對常數池中的字串值的儲存位置應該有一個比較明了的理解了。

  在程式執行的時候,常數池會儲存在Method Area,而不是堆中。

堆疊與堆疊

  Java的堆疊是一個執行時間資料區,類別的(物件從中分配空間。這些物件透過new、newarray、anewarray和multianewarray等指令建立,它們不需要程式碼來明確的釋放。大小生存期也不必事先告訴編譯器,因為它是在運行時動態分配記憶體的,Java的垃圾收集器會自動收走這些不再使用的資料。 ,由於要在運行時動態分配內存,因此存取速度較慢

  堆疊的優點是,存取速度比堆疊要快,僅次於暫存器,堆疊資料可以共享。但缺點是,存在堆疊中的資料大小與存活期必須是確定的,缺乏彈性。堆疊中主要存放一些基本型別的變數資料(int, short, long, byte, float, double, boolean, char)和物件句柄(引用)。

******************************************** **********************

 

  這裡我們主要關心棧,堆和常數池,對於棧和常數池中的物件可以共用,對於堆中的物件不可以共用。堆疊中的資料大小和生命週期是可以確定的,當沒有引用指向資料時,這個資料就會消失。堆中的物件的由垃圾回收器負責回收,因此大小和生命週期不需要確定,具有很大的靈活性。

字串記憶體分配:

  對於字串,其物件的參考都是儲存在堆疊中的,如果是編譯期已經創建好(直接用雙引號定義的)的就存儲在常數池中,如果是運行期(new出來的)才能確定的就存儲在堆中。對於equals相等的字串,在常數池中永遠只有一份,在堆中有多份

如以下程式碼:


        String s1 = "china";
        String s2 = "china";
        String s3 = "china";

        String ss1 = new String("china");
        String ss2 = new String("china");
        String ss3 = new String("china");

#  這裡解釋一下黃色這3個箭頭,對於透過new產生一個字符串(假設為“china”)時,會先去常量池中查找是否已經有了“china”對象,如果沒有則在常數池中創建一個此字符串對象,然後堆中再創建一個常數池中此”china」物件的拷貝物件。

  這也就是有道面試題:Strings=newString(“xyz”);產生幾個物件?一個或兩個,如果常量池中原來沒有”xyz”,就是兩個。

  存在於.class檔案中的常數池,在運作期間被JVM裝載,並且可以擴充。 String的intern()方法就是擴充常數池的一個方法;當一個String實例str呼叫intern()方法時,Java 會尋找常數池中是否有相同Unicode的字串常數,如果有,則傳回其的引用,如果沒有,則在常數池中增加一個Unicode等於str的字串並傳回它的參考

如下程式碼:


##

        String s0= "kvill";   
        String s1=new String("kvill");   
        String s2=new String("kvill");   
        System.out.println( s0==s1 );     
        s1.intern();   
        s2=s2.intern(); //把常量池中"kvill"的引用赋给s2   
        System.out.println( s0==s1);   
        System.out.println( s0==s1.intern() );   
        System.out.println( s0==s2 );

輸出結果:

false
false
#true
true

String常數池問題的幾個例子:


【1】
String a = "ab"= "b"= "a" +== b)); = "ab" String bb = "b"= "a" +== b)); = "ab" String bb == "a" +== b));   "b"
分析:

#  【1】中,JVM對於字串引用,由於在字串的"+"連接中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即"a" + bb無法被編譯器優化,只有在程式運行期來動態分配並將連接後的新地址賦給b。所以上面程式的結果也就為false。

  【2】和【1】中唯一不同的是bb字串加了final修飾,對於final修飾的變量,它在編譯時被解析為常數值的一個本地拷貝儲存到自己的常數池中或嵌入到它的字節碼流中。所以此時的"a" + bb和"a" + "b"效果是一樣的。故上面程式的結果為true。

  【3】JVM對於字串引用bb,它的值在編譯期無法確定,只有在程式執行期呼叫方法後,將方法的傳回值和"a"來動態連線並指派位址為b,故上面程式的結果為false。

結論:

  字串是一個特殊包裝類別,其引用是存放在堆疊裡的,而物件內容必鬚根據創建方式不同定(常數池和堆).有的是編譯期就已經創建好,存放在字串常數池中,而有的是運行時才被創建.使用new關鍵字,存放在堆中。

基礎類型的變數和常數在記憶體中的分配

  對於基礎類型的變數和常數,變數和引用存儲在堆疊中,常數儲存在常數池中。

如下列程式碼:


#

        int i1 = 9;        int i2 = 9;        int i3 = 9;        final int INT1 = 9;        final int INT2 = 9;        final int INT3 = 9;

  编译器先处理int i1 = 9;首先它会在栈中创建一个变量为i1的引用,然后查找栈中是否有9这个值,如果没找到,就将9存放进来,然后将i1指向9。接着处理int i2 = 9;在创建完i2的引用变量后,因为在栈中已经有9这个值,便将i2直接指向9。这样,就出现了i1与i2同时均指向9的情况。最后i3也指向这个9。

成员变量和局部变量在内存中的分配

  对于成员变量和局部变量:成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。 形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。 成员变量存储在堆中的对象里面,由垃圾回收器负责回收。   如以下代码:


class BirthDate {    private int day;    private int month;    private int year;    public BirthDate(int d, int m, int y) {
        day = d;
        month = m;
        year = y;
    }    // 省略get,set方法………}public class Test {    public static void main(String args[]) {        int date = 9;
        Test test = new Test();
        test.change(date);
        BirthDate d1 = new BirthDate(7, 7, 1970);
    }    public void change(int i) {
        i = 1234;
    }
}

  对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:    

  1. main方法开始执行:int date = 9; date局部变量,基础类型,引用和值都存在栈中。

  2. Test test = new Test();test为对象引用,存在栈中,对象(new Test())存在堆中。 

  3. test.change(date);  i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。

  4. BirthDate d1= new BirthDate(7,7,1970); d1为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。 

  5. main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(), new BirthDate()将等待垃圾回收。

以上是Java講解之記憶體分配之堆、堆疊與常數池分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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