Java內存模型詳解

借用一句話:Java與C++之間有一堵內存動態分配和垃圾收集技術圍成的高牆,牆外面的人想進來,牆裏面的人卻想出去。java

 

 

一.咱們爲何要了解JAVA內存                                              

  由於虛擬機幫咱們JAVA程序員管理着內存,咱們在new Object()申請了內存建立對象以後,便不須要再去delete/free來釋放內存。也所以不容易出現內存泄漏和內存溢出的問題,看起來一切都很美好。程序員

  可是,若是一個程序員不瞭解虛擬機是怎麼管理內存的,那麼在排查內存相關的錯誤是便會成爲一個巨大的難題。算法

 

 

二.內存區域有哪些        

 

                                                              

  內存區域分爲兩種,一種隨着虛擬機的進程啓動而存在。另外一種則依賴用戶進程的啓動和結束而創建和銷燬。數組

  1.程序計數器

  一塊較小的線程私有的內存空間,能夠看做是當前線程的所執行的字節碼的行號指示器。緩存

  若是線程正在執行的是一個JAVA方法,那麼計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是native方法,那麼計數器值爲空(Undefined)。函數

  該內存區域是惟一一個在JAVA虛擬機規範中沒有規定任何OutOfMemoryErrorOOM)狀況的區域。post

  

  2.虛擬機棧

  線程私有的,每一個Java方法在執行時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用到完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。性能

 

  3.本地方法棧

  線程私有,同虛擬機棧,爲native方法服務。在HotSpot虛擬機中,直接把虛擬機棧和本地方法棧合二爲一。spa

 

  4.堆

  線程共享的區域。存放實例的區域,幾乎全部的對象實例都在這裏分配內存。同時,由於空間固定,而用戶可能須要不斷生成實例,故該區域仍是垃圾收集的主要區域。垃圾收集將在後面提到。線程

  Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可。

 

  5.方法區

  線程共享的區域,存儲已被虛擬機加載的類信息、常量、靜態變量等數據。

  不少人稱之爲「永生代」,由於HotSpot使用永生代來實現方法區。Java規範中對方法區的限制十分寬鬆,能夠選擇不實現垃圾收集

 

  6.運行時常量池

  方法區的一部分,用於存放編譯器生成的各類字面量和符號引用,在類加載完成後進入方法區的運行時常量池中存放。

  關於這快區域,有一個須要注意的地方。代碼以下:

  

複製代碼
public class t18 {

    public static void main(String[] args){

        Integer a1 = 128 ;
        Integer a2 = 128 ;
        System.out.println(a1==a2);
        Integer b1 = 127;
        Integer b2 = 127;
        Integer b3 = 1 + b1;
        Integer b4 = a1 -1 ;
        System.out.println(b1==b2);
        System.out.println(b3==a1);
        System.out.println(b4 == b1);
    }
}
複製代碼

  上面代碼的運行結果爲 false ,true ,false ,true 。這是不少人第一次見到時都沒法理解的,由於這裏涉及到了常量池的知識。JVM會把一些int,String等數據進行在常量池中緩存,可是重點在於,對於int型數據,只會緩存 -128~127 範圍內的數據。所以:

  a一、a2超過了127,在堆中分配內存,二者指向不一樣對象,返回false;

  b一、b2都指向常量池中的127,故b一、b2指向地址相同,返回true;

  第三、4個同理,Integer b3 = 1+b1  ----> Integer b3 =Integer.valueOf(1+b1)。

  

  7.直接內存

  JDK1.4後加入了NIO (new I/O)類,引入了基於通道與緩衝區的IO方式,可使用native函數庫分配機器內存,如電腦8g內存,JVM可使用電腦的剩餘內存,只須要在java堆中存儲DirectByteBuffer對象做爲內存的引用進行操做。這樣在某些場景中提升性能。

三.在new一個對象時發生了什麼                                                  

  1. 當虛擬機遇到一條new 指令時,首先回去檢查可否在常量池中定位,並檢查這個類是否已經被加載、解析、初始化過,若是沒有,那麼必須先執行類的加載過程。
  2. 類加載完成後,接下來將會爲對象分配內存,即把一塊肯定大小的內存從java堆中劃分出來。若是java堆是連續且規整的,已分配過的內存放在一邊,空閒的在另外一邊。中間的指針做爲分界點的指示器,那麼分配內存就是將指針向空閒的方向移動所須要的距離,(使用Serial、PalNew等帶規整過程的垃圾收集器);若是java堆是不規整的,那麼虛擬機就必須維護一個記錄,分配內存的同時須要更新記錄,(如使用CMS這種基於標記-清除算法的收集器)。
  3. 將分配到的內存空間賦予初值,如整形變量置0,bool型置false。保證了對象字段在代碼中能夠不付初值就能夠直接使用。然而在實際編寫代碼中,建議採用賦初值的形式,保持一個良好的代碼習慣。另外
     String s ;
     System.out.println(s); //未初始化,編譯器報錯

    該代碼會報錯,而不是輸出null,切記切記。

  4. 初始化對象的對象頭數據,每一個java對象都有對象頭(Object Header),裏面記錄了對象是哪一個類的實例、如何找到類的元數據信息、哈希碼、GC年齡、偏向鎖等信息。
  5. 執行init方法,把對象按照程序員的一員進行初始化,這樣,一個可用的對象才完成new操做。

四.一個對象在內存中有哪些部分                                                

  以HotSpot虛擬機爲例,對象在內存中存儲的區域能夠分爲三個部分

  1.對象頭(Object Header)

  對象頭包括兩部分,一部分用於存儲對象自身的運行時數據,官方稱之爲「Mark Word」,包括:HashCode、GC年齡、鎖狀態、線程持有鎖、偏向鎖線程id、偏向時間戳等。佔一個字長(32bit或64bit,取決於虛擬機)。

  另外一部分是類型指針,對象指向的類元數據指針,經過這個來肯定該對象是哪一個類的實例。另外若是一個對象是一個數組,那麼還有一塊用於記錄數組長度的數據。

  2.對象數據

  即實例中存儲的,程序員設計的應該存儲的數據。

  3.對齊填充

  不是必須的,僅僅起着佔位的做用,HotSpot內存管理規定對象的起始地址必須是8字節的整數倍,換句話說對象的大小必須是9字節的整數倍,所以,當實例大小沒有對齊時,須要經過對齊填充來補全。

五.如何訪問定位對象                                                                        

  建立對象是爲了使用對象,java虛擬機使用上的reference數據來操做上的具體對象,目前的訪問方式主流有兩種:

  1.使用句柄訪問

  Java堆中會劃分出一塊內存做爲句柄池,reference中存儲的是對象的句柄地址,而句柄中包含了對象的實例數據與類型數據各自的地址信息。

  即訪問時refenrence(存句柄地址) --> 句柄池(堆中,存對象地址) --> 具體對象(堆或方法區中)。

  2.使用直接指針訪問

  直接訪問,reference(存對象地址)-->具體對象(堆中或方法區中),一次跳轉。HotSpot虛擬機使用的就是這種方式。

相關文章
相關標籤/搜索