深入java內存分配

一、Java內存分配
1、 Java有幾種存儲區域?
* 寄存器
     -- 在CPU內部,開發人員不能通過代碼來控制寄存器的分配,由編譯器來管理
* 棧
     -- 在Windows下, 棧是向低地址擴展的數據結構,是一塊連續的內存的區域,即棧頂的地址和棧的最大容量是系統預先規定好的。
     -- 優點:由系統自動分配,速度較快。
     -- 缺點:不夠靈活,但程序員是無法控制的。
     -- 存放基本數據類型、開發過程中就創建的對象(而不是運行過程中)
* 堆
     -- 是向高地址擴展的數據結構,是不連續的內存區域
     -- 在堆中,沒有堆棧指針,爲此也就無法直接從處理器那邊獲得支持
     -- 堆的好處是有很大的靈活性。如Java編譯器不需要知道從堆裏需要分配多少存儲區域,也不必知道存儲的數據在堆裏會存活多長時間。
* 靜態存儲區域與常量存儲區域
     -- 靜態存儲區用來存放static類型的變量
     -- 常量存儲區用來存放常量類型(final)類型的值,一般在只讀存儲器中
* 非RAM存儲
     -- 如流對象,是要發送到另外一臺機器上的
     -- 持久化的對象,存放在磁盤上
2、 java內存分配
     -- 基礎數據類型直接在棧空間分配;
     -- 方法的形式參數,直接在棧空間分配,當方法調用完成後從棧空間回收;
     -- 引用數據類型,需要用new來創建,既在棧空間分配一個地址空間,又在堆空間分配對象的類變量;
     -- 方法的引用參數,在棧空間分配一個地址空間,並指向堆空間的對象區,當方法調用完後從棧空間回收;
     -- 局部變量 new 出來時,在棧空間和堆空間中分配空間,當局部變量生命週期結束後,棧空間立刻被回收,堆空間區域等待GC回收;
     -- 方法調用時傳入的 literal 參數,先在棧空間分配,在方法調用完成後從棧空間釋放;
     -- 字符串常量在 DATA 區域分配 ,this 在堆空間分配;
     -- 數組既在棧空間分配數組名稱, 又在堆空間分配數組實際的大小!
3Java內存模型
* Java虛擬機將其管轄的內存大致分三個邏輯部分:方法區(Method Area)、Java棧和Java堆。
    -- 方法區是靜態分配的,編譯器將變量在綁定在某個存儲位置上,而且這些綁定不會在運行時改變。
        常數池,源代碼中的命名常量、String常量和static 變量保存在方法區。
    -- Java Stack是一個邏輯概念,特點是後進先出。一個棧的空間可能是連續的,也可能是不連續的。
        最典型的Stack應用是方法的調用,Java虛擬機每調用一次方法就創建一個方法幀(frame),退出該方法則對應的  方法幀被彈出(pop)。棧中存儲的數據也是運行時確定的?
    -- Java堆分配(heap allocation)意味着以隨意的順序,在運行時進行存儲空間分配和收回的內存管理模型。
        堆中存儲的數據常常是大小、數量和生命期在編譯時無法確定的。Java對象的內存總是在heap中分配。
4Java內存分配實例解析
     常量池(constant pool)指的是在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量。
     常 量池在運行期被JVM裝載,並且可以擴充。String的intern()方法就是擴充常量池的一個方法;當一個String實例str調用 intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其引用,如果沒有,則在常量池中增加一個 Unicode等於str的字符串並返回它的引用。
     例:
     String s1=new String("kvill");
     String s2=s1.intern();
     System.out.println( s1==s1.intern() );//false
     System.out.println( s1+" "+s2 );// kvill kvill
     System.out.println( s2==s1.intern() );//true
     這個類中事先沒有聲名”kvill”常量,所以常量池中一開始是沒有”kvill”的,當調用s1.intern()後就在常量池中新添加了一 個”kvill”常量,原來的不在常量池中的”kvill”仍然存在。s1==s1.intern()爲false說明原來的“kvill”仍然存 在;s2現在爲常量池中“kvill”的地址,所以有s2==s1.intern()爲true。

String 常量池問題
(1) 字符串常量的"+"號連接,在編譯期字符串常量的值就確定下來, 拿"a" + 1來說,編譯器優化後在class中就已經是a1。
     String a = "a1";  
     String b = "a" + 1;  
     System.out.println((a == b)); //result = true 
     String a = "atrue";  
     String b = "a" + "true";  
     System.out.println((a == b)); //result = true 
     String a = "a3.4";  
     String b = "a" + 3.4;  
     System.out.println((a == b)); //result = true
(2) 對於含有字符串引用的"+"連接,無法被編譯器優化。
     String a = "ab";  
     String bb = "b";  
     String b = "a" + bb;  
     System.out.println((a == b)); //result = false
     由於引用的值在程序編譯期是無法確定的,即"a" + bb,只有在運行期來動態分配並將連接後的新地址賦給b。
(3) 對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝並存儲到自己的常量池中或嵌入到它的字節碼流中。所以此時的"a" + bb和"a" + "b"效果是一樣的。
     String a = "ab";  
     final String bb = "b";  
     String b = "a" + bb;  
     System.out.println((a == b)); //result = true
(4) jvm對於字符串引用bb,它的值在編譯期無法確定,只有在程序運行期調用方法後,將方法的返回值和"a"來動態連接並分配地址爲b。
     String a = "ab";  
     final String bb = getbb();  
     String b = "a" + bb;  
     System.out.println((a == b)); //result = false  
     private static string getbb() { 
       return "b";  
     }
(5) String 變量採用連接運算符(+)效率低下。
     String s = "a" + "b" + "c"; 就等價於String s = "abc"; 
     String a = "a"; 
     String b = "b"; 
     String c = "c"; 
     String s = a + b + c; 
     這個就不一樣了,最終結果等於: 
       Stringbuffer temp = new Stringbuffer(); 
       temp.append(a).append(b).append(c); 
       String s = temp.toString(); 
(6) Integer、Double等包裝類和String有着同樣的特性:不變類。 
     String str = "abc"的內部工作機制很有代表性,以Boolean爲例,說明同樣的問題。 
     不變類的屬性一般定義爲final,一旦構造完畢就不能再改變了。 
     Boolean對象只有有限的兩種狀態:true和false,將這兩個Boolean對象定義爲命名常量: 
     public static final Boolean TRUE = new Boolean(true); 
     public static final Boolean FALSE = new Boolean(false); 
     這兩個命名常量和字符串常量一樣,在常數池中分配空間。 Boolean.TRUE是一個引用,Boolean.FALSE是一個引用,而"abc"也是一個引用!由於Boolean.TRUE是類變量 (static)將靜態地分配內存,所以需要很多Boolean對象時,並不需要用new表達式創建各個實例,完全可以共享這兩個靜態變量。其JDK中源 代碼是: 
     public static Boolean valueOf(boolean b) { 
       return (b ? TRUE : FALSE); 
     } 
     基本數據(Primitive)類型的自動裝箱(autoboxing)、拆箱(unboxing)是JSE 5.0提供的新功能。 Boolean b1 = 5>3; 等價於Boolean b1 = Boolean.valueOf(5>3); //優於Boolean b1 = new Boolean (5>3); 
    static void foo(){ 
        boolean isTrue = 5>3;  //基本類型 
        Boolean b1 = Boolean.TRUE; //靜態變量創建的對象 
        Boolean b2 = Boolean.valueOf(isTrue);//靜態工廠 
        Boolean b3 = 5>3;//自動裝箱(autoboxing) 
        System.out.println("b1 == b2 ?" +(b1 == b2)); 
        System.out.println("b1 == b3 ?" +(b1 == b3)); 
        Boolean b4 = new Boolean(isTrue);////不宜使用 
        System.out.println("b1 == b4 ?" +(b1 == b4));//浪費內存、有創建實例的時間開銷 
    } //這裏b1、b2、b3指向同一個Boolean對象。
(7) 如果問你:String x ="abc";創建了幾個對象? 
     準確的答案是:0或者1個。如果存在"abc",則變量x持有"abc"這個引用,而不創建任何對象。 
     如果問你:String str1 = new String("abc"); 創建了幾個對象? 
     準確的答案是:1或者2個。(至少1個在heap中)
(8) 對於int a = 3; int b = 3;
     編譯器先處理int a = 3;首先它會在棧中創建一個變量爲a的引用,然後查找有沒有字面值爲3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接着處 理int b = 3;在創建完b的引用變量後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。
5、堆(Heap)和非堆(Non-heap)內存
     按照官方的說法:“Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。堆是在 Java 虛擬機啓動時創建的。”
     可以看出JVM主要管理兩種類型的內存:堆和非堆。
     簡單來說堆就是Java代碼可及的內存,是留給開發人員使用的;
     非堆就是JVM留給自己用的,所以方法區、JVM內部處理或優化所需的內存(如JIT編譯後的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼都在非堆內存中。 
堆內存分配
     JVM初始分配的內存由-Xms指定,默認是物理內存的1/64;
     JVM最大分配的內存由-Xmx指定,默認是物理內存的1/4。
     默認空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制;空餘堆內存大於70%時,JVM會減少堆直到-Xms的最小限制。
     因此服務器一般設置-Xms、-Xmx相等以避免在每次GC 後調整堆的大小。 
非堆內存分配
     JVM使用-XX:PermSize設置非堆內存初始值,默認是物理內存的1/64;
     由XX:MaxPermSize設置最大非堆內存的大小,默認是物理內存的1/4。 
例子
     -Xms256m
     -Xmx1024m
     -XX:PermSize=128M
     -XX:MaxPermSize=256M

發佈了16 篇原創文章 · 獲贊 7 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章