java 虛擬機內存划分,類加載過程以及對象的初始化


涉及關鍵詞:
虛擬機運行時內存 java內存划分 類加載順序  類加載時機  類加載步驟  對象初始化順序  構造代碼塊順序 構造方法 順序 內存區域   java內存圖  堆 方法區 虛擬機棧 本地方法棧 程序計數器  局部變量表   棧幀  java堆 運行時常量池   直接內存
 
 本文從三個部分理解java的初始化

1).java虛擬機運行時的內存區域

2).類的加載過程

3).初始化過程

 

 java虛擬機運行時內存空間區域分配

 
 
虛擬機棧中每個方法執行都會創建棧幀,每個棧幀中有局部變量表
方法區中有運行時常量
 

 
線程私有的,也就是每個線程都需要程序計數器 
 
 

java虛擬機棧 也是線程私有的
虛擬機棧描述的是java方法執行的內存模型,每個方法執行的同時都會創建棧幀
用於存儲局部變量表/操作數棧/動態鏈接/方法出口等信息
一般所說的棧就是指的這里

本地方法棧跟虛擬機棧類似
只不過是運行的本地方法,虛擬機實現中有的直接把方法合二為一

可以右鍵新標簽頁面打開看大圖

 


java堆是java虛擬機管理的最大一塊內存,所有線程共享
啟動時創建
唯一目的就是存放對象實例
幾乎所有的對象實例都是在這里分配內存
垃圾回收的主要管理區域

 
 
 
 

 


 

 

方法區是與堆一樣的線程共享的
存儲被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯后的代碼等數據

 
 
 

 

 
 

運行時常量池是方法區的一部分
Class文件中有一項信息是常量池,用於存放編譯器生成的各種字面量和符號引用
這部分內容類加載之后進入方法區的運行時常量池中存放

 
 
 不是虛擬機運行的內存區域
 
 
 
 

類的加載


java代碼編譯成class文件之后,就形成了類的信息-類的二進制字節流
想要使用,肯定要加載

 

 


生命周期

 

 


加載/驗證/准備/初始化/卸載 5個階段順序是確定的
解析不確定,可能在初始化階段之后,為了支持java的運行時綁定

加載時機

 

1) new關鍵字實例化對象/讀取或者配置類的靜態字段/調用類的靜態方法
2) java.lang.reflect 包的方法對類進行反射調用 如果沒有初始化 觸發
3) 初始化類的時候,發現父類沒有初始化 觸發父類初始化
4)虛擬機啟動需要指定一個main方法的主類 先初始化這個類

加載

 

 

 

而且,對於非數組類的加載階段,准確的說是加載階段中獲取類的二進制字節流的動作行為
是多樣性的
可以使用系統提供的引導類加載器
也可以用戶自定義的類加載器
開發人員可以通過定義自己的類加載器去控制字節流的獲取方式(重寫類加載器的loadCalss()方法)

數組類不同
數組類本身不通過類加載器創建 由java虛擬機直接創建
但是數組的元素類型 最終是靠類加載器去創建的

驗證
確保Class文件的字節流中包含的信息符合當前虛擬機要求
並且不會危害虛擬機
因為字節碼文件可以隨便編寫,由其他語言編譯出來,或者直接十六進制編輯器直接書寫
所以需要校驗

文件格式校驗
是否符合Class文件格式的規范
魔數/主次版本號/編碼/文件完整性....相當於是格式上的硬校驗

元數據校驗
字節碼描述的信息進行語義分析,確保其描述的信息符合語言規范要求
比如是否有父類 是否繼承了不允許被繼承的類等等
針對字節碼描述的信息

字節碼驗證
通過數據流和控制流分析,確定程序語義是合法的,符合邏輯的
第二階段對元數據信息中的數據類型做完校驗后,這個階段將對類的方法體進行校驗分析
保證被校驗類的方法在運行時不會做出危害虛擬機安全的事情

符號引用驗證
虛擬機將符號引用轉化為直接引用的時候 解析過程中發生
符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)

准備

正式為類變量分配內存並設置類變量初始值的階段
這些變量所使用的內存都將在方法區中進行分配
注意不包括實例變量 實例變量將會在對象實例化時隨着對象一起分配在java堆中
基本數據類型的初始化

 

 


通常情況下是這樣,如果是常量
public static final int value = 123; 准備階段就會設置


解析

虛擬機將常量池內的符號引用替換為直接引用的過程
符號引用:
一組符號來描述所引用的目標,說白了是邏輯意義上的
符號可以是任何形式的字面量,只要能無歧義的定位到目標即可
符號引用與虛擬機實現的內存布局無關,引用的目標不一定加載到內存中
雖然各種虛擬機實現不同
但是能夠接受的符號引用必須是一致的

直接引用:
可以是直接指向目標的指針,相對偏移量或者一個能間接定位到目標的句柄
也就是物理上的,能夠真的定位到指定的內存區域
跟虛擬機的實現的內存布局相關的
同一個符號引用不同虛擬機可能有不同的直接引用,而且一般是不同的

 


CONSTANT_Class_info
CONSTANT_Fieldref_info
CONSTANT_Methodref_info
CONSTANT_InterfaceMethodref_info
CONSTANT_MethodType_info
CONSTANT_MethodHandle_info
CONSTANT_InvokeDynamic_info

 

 初始化

 

在接下來是初始化,初始化也屬於類加載的一步,不過這一步驟是程序員最關心的,單獨拿出來說

類加載過程的最后一步,到了這個階段才真正開始執行類中定義的Java程序代碼(或者說是字節碼)

初始化階段是執行類構造器 <clinit>()方法的過程

 

所有的-->  變量靜態語句塊

<clinit>() 對於類或者接口並不是必須的,如果一個類沒有靜態語句塊
也沒有對變量的賦值操作
編譯器可以不為這個類生成<clinit>()方法

 

接口中不能使用靜態語句塊 但是仍然有變量初始化,所以接口與類一樣,也會生成這個方法
但是與類不同的是,不需要先執行父接口的<clinit>()方法 
只有當父接口定義的變量使用時,父接口才會初始化


虛擬機會保證一個類的<clinit>()方法在多線程環境中能夠被正確的加鎖同步

 

 

從加載到對象初始化全過程

 

一加載時機

1) new關鍵字實例化對象/讀取或者配置類的靜態字段/調用類的靜態方法
2) java.lang.reflect 包的方法對類進行反射調用 如果沒有初始化 觸發
3) 初始化類的時候,發現父類沒有初始化 觸發父類初始化
4)虛擬機啟動需要指定一個main方法的主類 先初始化這個類

也就是在上面這些情況的時候 會發生類的加載

滿足加載時機之后,然后經過類加載的幾個過程

 

ps:  對於下面的初始化      同級別也就是相同優先級的變量的順序是按照代碼書寫順序來的

二初始化靜態(類變量)

然后就是准備階段中:

類變量 也就是static變量分配內存 並且 初始化數據默認值  注意實例對象的變量此時沒有操作

另外如果是final 修飾的常量,此時一並直接賦值

 

三 構造器方法  <clinit>() 

此時所有類的構造器方法執行

而且父類早於子類,所以最早執行的肯定是Object的

此方法把所有類的靜態變量也就是類變量的賦值動作執行結束,而且靜態代碼塊也已經執行結束,而且順序是父類早於子類

也就是說至此,所有的靜態變量都已經分配內存空間,也都是已經設置好的值了,包括父類的所有靜態變量

靜態變量以及靜態代碼塊的執行都是在這里,顯然他們是早於構造方法的執行的

但是如果靜態變量賦值或者代碼塊中賦值中使用到了其他的方法,那么這個方法將會提早執行

如果使用new 構建了對象,不僅僅是構造方法,實例化的步驟都會執行的

而且如果是構造方法,那么new對象實例化的時候還會再次執行

 

 

輸出結果:

1).main方法所在的類會被加載,所以會加載In這個類,

2).然后會處理static類型的變量,以及static代碼塊

3).這個變量賦值使用了new 所以會調用構造方法,如果是調用其他方法,一樣先執行

靜態變量和靜態代碼塊優先級相同,代碼塊在下面,所以先打印了  ""構造方法""   然后打印了""靜態方法""

4).此時類加載結束了進入主函數,主函數中先打印了分隔符"-----------------"

5).然后new對象又調用了構造方法,看起來怪怪的,其實邏輯很清晰

 

 

四  對象實例化

只有需要產生對象的時候才會有對象實例化,僅僅是加載類的話,上面的前三步就結束了

而且雖然說是一般最后但是也不一定,比如上面提到的如果靜態變量調用new 就會提前觸發

1.在堆上分配對象足夠的內存空間

2.然后就是空間擦除清零,也即是設置為默認值

3.然后按照實例字段定義的順序,順序執行賦值初始化   初始化代碼塊 和直接定義變量的初始化 優先級別一樣 按照定義順序進行先后

4.實例的構造方法調用

對象的實例化是一個整體的,調用了new 就會按部就班的執行這些步驟

 

補充說明:

  1. 靜態的初始化僅僅是類加載的時候發生,僅僅發生一次,類的加載時機看上面的---加載時機
  2. 靜態變量也就是類變量都有默認的初始化值的,局部變量都沒有默認值的,想要使用必須賦值,否則報錯
  3. static不能修飾局部變量 
  4. 靜態變量不能向前引用,比如先使用了值,接下來才定義
  5. 成員屬性值的初始化方式:並不是只能定義的時候賦值的

    類內聲明時直接賦值
    構造方法 -----如果構造方法中只是初始化了部分屬性值的話,其他的值還是默認值的
    調用成員方法進行初始化(方法可以有參數,不過參數必須是已經初始化了的)
    初始化塊---只要構造對象,初始化塊就會執行的,而且早於構造方法

   6.每個類都有默認的構造方法,如果你不定義他永遠有一個默認的,如果定義了,默認的就不存在了,當你還需要new 對象(  ) 這種形式的話就不行了,按需添加

  7.數組的初始化定義的是一個引用,需要顯式的初始化,否則引用為null,數組類型和普通的類加載是不一樣的

  8.相同優先級別的根據定義的順序決定初始化順序;不同的優先級別的,不管你怎么寫,優先級別高的始終會早於優先級低的

    比如靜態的你寫到構造方法下面還是靜態的先執行;(特殊情況是上面提到的static變量用new 對象賦值)

    初始化代碼塊總會早於構造方法的執行

  9.繼承結構中除非有特殊情況,否則順序一般都是下面這樣子的

    先執行靜態的初始化
    所有的靜態初始化結束
    執行最頂級初始化塊
    執行最頂級構造方法

    ......
    執行子類初始化塊
    執行子類構造方法

  10,如果對象中有其他類的成員變量,這個變量的靜態,初始化塊,構造方法的順序(他們三個是一起的不分割的),跟這個類本身的初始化塊的優先級是一樣的,按照定義的順序

  

      

      比如  Test 中有T1   T1的靜態初始化塊,初始化塊,構造方法是一起的,然后他們和Test的初始化塊的順序是不固定的

      

 

 

 好了,把這些點都記住的話,基本上就可以徹底理清楚了

初始化的過程是很復雜的,所以要掌握好優先級和規則

否則 包含的變量又有很多父類 等 各個類里面調用各種方法初始化就會讓人徹底懵逼了

 


免責聲明!

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



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