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

  1. 寄存器:最快的存儲區,位於不一樣於其餘存儲區的地方——處理器內部。寄存器的數量極其有限,因此寄存器由編譯器根據需求  進行分配。你不能直接控制,也不能在程序中感受到寄存器存在的任何跡象。
  2. 棧:存放基本類型的數據和對象的引用,但對象自己不存放在棧中,而是存放在堆中
  3. 堆:一種通用性的內存池(也存在於RAM中),用於存放因此的JAVA對象。堆不一樣於堆棧的好處是:編譯器不須要知道要從堆裏分配多少存儲區域,也沒必要知道存儲的數據在堆裏存活多長時間。所以,在堆裏分配存儲有很大的靈活性。當你須要建立一個對象的時候,只須要new寫一行簡單的代碼,當執行這行代碼時,會自動在堆裏進行存儲分配。固然,爲這種靈活性必需要付出相應的代碼。用堆進行存儲分配比用堆棧進行存儲存儲須要更多的時間。存放用new產生的數據
  4. 靜態域:存放在對象中用static定義的靜態成員
  5. 常量池:存放常量
  6. 非RAM(隨機存取存儲器)存儲:硬盤等永久存儲空間。若是數據徹底存活於程序以外,那麼它能夠不受程序的任何控制,在程序沒有運行時也能夠存在。 這裏咱們主要關心棧,堆和常量池,對於棧和常量池中的對象能夠共享,對於堆中的對象不能夠共享。棧中的數據大小和生命週期是能夠肯定的,當沒有引用指向數據時,這個數據就會消失。堆中的對象的由垃圾回收器負責回收,所以大小和生命週期不須要肯定,具備很大的靈活性。 

Java內存分配中的棧 

 在函數中定義的一些基本類型的變量數據對象的引用變量都在函數的棧內存中分配。當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當該變量退出該做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做他用。 面試

Java內存分配中的堆 

堆內存用來存放由new建立的對象和數組。 在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。在堆中產生了一個數組或對象後,還能夠 在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。引用變量就至關因而爲數組或對象起的一個名稱,之後就能夠在程序中使用棧中的引用變量來訪問堆中的數組或對象。引用變量就至關因而爲數組或者對象起的一個名稱。數組

常量池 (constant pool) 

常量池指的是在編譯期被肯定,並被保存在已編譯的.class文件中的一些數據。除了包含代碼中所定義的各類基本類型(如int、long等等)和對象型(如String及數組)的常量值(final)還包含一些以文本形式出現的符號引用,好比:  app

  1. 類和接口的全限定名;
  2. 字段的名稱和描述符; 
  3. 方法和名稱和描述符。

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

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

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

堆與棧 

Java的堆是一個運行時數據區,類的(對象從中分配空間。這些對象經過new、newarray、 anewarray和multianewarray等指令創建,它們不須要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優點是能夠動態地分配內存大小,生存期也沒必要事先告訴編譯器,由於它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些再也不使用的數據。但缺點是,因爲要在運行時動態 分配內存,存取速度較慢。 code

  棧的優點是,存取速度比堆要快,僅次於寄存器,棧數據能夠共享。但缺點是,存在棧中的數據大小與生存期必須是肯定的,缺少靈活性。棧中主要存放一些基本類型的變量數據(int, short, long, byte, float, double, boolean, char)和對象句柄(引用)。對象

 字符串內存分配

 對於字符串,其對象的引用都是存儲在棧中的,若是是編譯期已經建立好(直接用雙引號定義的)的就存儲在常量池中,若是是運行期(new出來的)才能肯定的就存儲在堆中。對於equals相等的字符串,在常量池中永遠只有一份,在堆中有多份。blog


這裏咱們主要關心棧,堆和常量池,對於棧和常量池中的對象能夠共享,對於堆中的對象不能夠共享。棧中的數據大小和生命週期是能夠肯定的,當沒有引用指向數據時,這個數據就會消失。堆中的對象的由垃圾回收器負責回收,所以大小和生命週期不須要肯定,具備很大的靈活性。 接口

        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()將等待垃圾回收。
相關文章
相關標籤/搜索