1. 程式人生 > >Java底層的記憶體分配策略

Java底層的記憶體分配策略

Java記憶體分配的主要區域:

1、棧:存放基本型別的資料和物件的引用,物件本身是存放在堆中

2、堆:存放用new產生的物件資料

3、常量池:存放常量

通常常量池有兩種形態:

執行時常量池:則是jvm虛擬機器在完成類裝載操作後,將*.class檔案中的常量池載入到記憶體中,並儲存在方法區中,我們常說的常量池,就是指方法區中的執行時常量池。

(方法區存放了一些常量、靜態變數、類資訊等,可以理解成class檔案在記憶體中的存放位置)。

靜態常量池:即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字串(數字)字面量,還包含類、方法的資訊,佔用class檔案絕大部分空間。

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

5. 暫存器:是速度最快的儲存場所,因為暫存器位於處理器內部,這一點和其他的儲存媒介都不一樣。不過暫存器個數是有限的。在記憶體中的暫存器區域是由編譯器根據需要來分配的。我們程式開發人員不能夠通過程式碼來控制這個暫存器的分配。

棧(Stack):

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

堆(Heap):

堆記憶體用來存放由new建立的物件和陣列。 在堆中分配的記憶體,由Java虛擬機器的自動垃圾回收器來管理。

通常在堆中產生了一個數組或物件後,還會在棧中建立一個特殊的變數,並且讓棧中這個變數的取值等於陣列或物件在堆記憶體中的首地址,棧中的這個變數我們就通常稱為引用變數。引用變數可理解為給陣列或者物件起的一個名稱,在以後程式執行中可以使用棧中的引用變數來訪問堆中儲存的陣列或物件。

指標:棧中的變數是指向堆記憶體中的變數

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

常量池(constant pool):

Java中的常量池技術,是為了方便快捷地建立某些物件而出現的,當需要一個物件時,就可以從池中取一個出來(如果池中沒有則建立一個),則在需要重複建立相等變數時節省了很多時間。Java中基本型別的包裝類的大部分都實現了常量池技術,這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點數(Double、Float)型別的包裝類則沒有實現。另外Byte,Short,Integer,Long,Character這5種整型的包裝類也只是在對應值[-128,127]時才可使用常量池,也即物件不負責建立和管理大於[-128,127]範圍的這些類的物件。

特點:常量池是在編譯期被確定,會被儲存在已編譯的.class檔案中。 

除了包含程式碼中所定義的各種基本型別(如int、long等)和基本型別對應的包裝型別(Integer、Long等)以及物件型(如String及陣列)的常量值(final)還包含一些以文字形式出現的符號引用,比如:

1、類和介面的全限定名

2、欄位的名稱和描述符

3、方法的名稱和描述符

虛擬機器會為每種被裝載的型別維護一個常量池。常量池就是該型別所用到常量的一個有序集和,包括直接常量(string,integer和 floating point常量)和對其他型別,欄位和方法的符號引用。在程式執行的時候,常量池會儲存在方法區(Method Area),而不是堆中。

對於String常量,它的值是在常量池中(字串常量池)。而JVM中的常量池在記憶體當中是以表的形式存在的, 對於String型別,有一張固定長度的CONSTANT_STRING_INFO表用來儲存文字字串值,注意:該表只儲存文字字串值,不儲存符號引用。

比如:String s1 = "abc" 以這種方式宣告的,值就是儲存在常量池中。

String s2 = new String("abc")這種方式宣告的,會把 new 出的字串物件儲存在堆裡。並在棧裡會建立一個物件的引用,並指向堆裡字串物件儲存的首地址。

堆(Heap)與棧(Stack):

棧的優勢是,存取速度比堆要快,僅次於暫存器,存在棧裡的資料可以共享。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本型別的變數(int, short, long, byte, float, double, boolean, char)和物件控制代碼(物件引用)。當代碼執行出該變數作用域後,Java會自動釋放掉為該變數所分配的記憶體空間

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

總結:棧和常量池中的物件是可以共享,對於堆中的物件不可以共享。棧中的資料大小和生命週期(變數的作用域)是可以確定的,當沒有引用指向資料時,這個資料回收並釋放儲存空間。堆中的物件的由垃圾回收器負責回收,因此大小和生命週期不需要確定,具有很大的靈活性。

程式碼分析:

Integer(Byte,Short,Integer,Long,Character,Boolean類似)

【1】Integer e = 127; //先檢查 Integer常量池中是否有127 沒有則將127存在常量池中,並在棧建立一個物件引用指向對應的地址
Integer j = 127;  //Integer常量池中是否有127 有127會在棧建立一個物件引用並指向對應的地址
System.out.println(e==j);// true  e==j比較地址,兩者也是相等的。

【2】Integer e2 = 128;
Integer j2 = 128;
System.out.println(e2==j2);// false 

為什麼這裡等於false:之前說過常量池是有大小限制的。而Integer常量池的大小就是[-128,127]超過這個數值範圍,就會把資料儲存在堆中,並在棧建立一個物件引用指向對應的值的物件的首地址。所以這裡就相當於Integer e2 = new Integer(128); Integer j2 = new Integer(128); e2==j2是比較地址。當前是false。

如果是Integer e2 = -129;  Integer j2 = -129; e2==j2也是false不信可以自己試一下。

【3】int n2 = 128;
int m2 = 128;
System.out.println("n2==m2:"+String.valueOf(n2==m2));//true 這裡就是true。千萬別弄混,這中情況是儲存在棧裡面

String

【1】String a = “test”;

String b = “test”;

String b = b+"java";

【1】a,b同時指向常量池中的常量值"test",b=b+"java"之後,b原先指向一個常量,內容為"test”,通過對b進行+"java" 操作後,b之前所指向的那個值沒有改變,但此時b不指向原來那個變數值了,而指向了另一個String變數,內容為”test java“。原來那個變數還存在於記憶體之中,只是b這個變數不再指向它了。

【2】String ab = "ab";
String b = "b";
String ab2 = "a"+b;
String ab3 = "a"+"b";
System.out.println(ab==ab2);// false
System.out.println(ab==ab3);// true

【2】String ab2 = "a"+b; 由於在字串的"+"連線中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即"a" + bb無法被編譯器優化,只有在程式執行期來動態分配到堆中,並將連線後的新地址賦給棧裡的引用ab2。所以結果為ab==ab2比較地址時為false

String ab3 = "a"+"b";也存在加號為什麼為true?

String ab3 = "a" + "b";編譯器將這個"a" + "b"進行優化,作為常量表達式String b = "ab",然後儲存在字串常量池中,儲存時會先檢查常量池中是否存在"ab",之前已經有String ab = "ab"; 常量池中存在"ab",然後ab3指向了該地址,並不會去新開闢一個記憶體空間去儲存"ab",所以ab==ab3為true

【3】String ab = "ab";
final String b= "b";   
String ab4 = "a" + b;   
System.out.println(ab == ab2); // true 

【3】和【2】中唯一不同的是加了final修飾,對於final修飾的變數,它在編譯時被解析為常量值的一個本地拷貝儲存到自己的常量池中(嵌入到它的位元組碼流中)。所以此時的"a" + bb和"a" + "b"效果是一樣的。所以結果為true。

【4】String ab = "ab";
final String finalMethodb = getBB();   
String ab5 = "a" + finalMethodb;   
System.out.println((ab == ab4)); // false

private static String getBB() {  
        return "b";   
}

【4】JVM對於字串引用finalMethodb 就算是加了final修飾,它的值也是在編譯期無法確定,只有在程式執行期呼叫方法後,將方法的返回值和"a"來動態連線並分配地址為b,所以這裡連線後的值"ab"是儲存在堆裡的,ab4引用指向的是堆裡的地址,ab引用指向的是常量池中儲存的"ab"的地址。所以結果為false。

【5】String ab = "ab";
String ab6 = "ab";
String ab7 = new String("ab");
System.out.println(ab==ab5);// true
System.out.println(ab==ab6);// false

【5】ab和ab5引用指向的都是常量池中儲存"ab"的記憶體地址。所以相等。ab6引用指向的是堆中物件的儲存地址。所以ab==ab6為false

【6】String s0= "java";

String s1=new String("java");

String s2=s1.intern();//s1 檢查常量池,發現沒有就拷貝自己的字串進去

System.out.println(s2 == s1);//false

System.out.println( s2==s0);//true

System.out.println( s1==s1.intern());// false

【6】String的intern()方法會查詢在常量池中是否存在一份equal相等的字串,如果有則返回一個引用,沒有則新增自己的字串進入常量池,注意:只是字串值部分。這時會存在2份拷貝,常量池的部分被String類私有並管理,自己的那份按物件生命週期還在繼續使用。

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

int i1 = 9;
int i2 = 9;

final int INT1 = 10;
final int INT2 = 10; 

對於基礎型別的變數和常量,值和引用儲存在棧中,常量儲存在常量池中。i1和i2引用和值9都在棧裡儲存 INT1和INT2是在引用是在棧中,值10是在常量池中