java內存模型詳解


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

 

 

一.我們為什么要了解JAVA內存                                              

  因為虛擬機幫我們JAVA程序員管理着內存,我們在new Object()申請了內存創建對象之后,便不需要再去delete/free來釋放內存。也因此不容易出現內存泄漏和內存溢出的問題,看起來一切都很美好。

  但是,如果一個程序員不了解虛擬機是怎么管理內存的,那么在排查內存相關的錯誤是便會成為一個巨大的難題。

 

二.內存區域有哪些                                                                       

  內存區域分為兩種,一種隨着虛擬機的進程啟動而存在。另一種則依賴用戶進程的啟動和結束而建立和銷毀。

  1.程序計數器

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

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

  該內存區域是唯一一個在JAVA虛擬機規范中沒有規定任何OutOfMemoryErrorOOM)情況的區域。

  

  2.虛擬機棧

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

 

  3.本地方法棧

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

 

  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 范圍內的數據。因此:

  a1、a2超過了127,在堆中分配內存,兩者指向不同對象,返回false;

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

  第3、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虛擬機使用的就是這種方式。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM