Java中棧,堆,常量池的東西

Java內存分配之堆、棧和常量池

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"; String bb = "b"; String b = "a" + bb; System.out.println((a == b)); //result = false 【2】
String a
= "ab"; final String bb = "b"; String b = "a" + bb; System.out.println((a == b)); //result = true 【3】
String a
= "ab"; final String bb = getBB(); String b = "a" + bb; System.out.println((a == b)); //result = false private static String getBB() { return "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()將等待垃圾回收。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章