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

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

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

2).類的加載過程程序員

3).初始化過程數組

 

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

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

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

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

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

能夠右鍵新標籤頁面打開看大圖

 


java堆是java虛擬機管理的最大一塊內存,全部線程共享
啓動時建立
惟一目的就是存放對象實例
幾乎全部的對象實例都是在這裏分配內存
垃圾回收的主要管理區域編輯器

 
 
 
 

 


 

 

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

 
 
 

 

 
 

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

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

類的加載


java代碼編譯成class文件以後,就造成了類的信息-類的二進制字節流
想要使用,確定要加載編碼

 

 


生命週期spa

 

 


加載/驗證/準備/初始化/卸載 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的初始化塊的順序是不固定的

      

 

 

 好了,把這些點都記住的話,基本上就能夠完全理清楚了

初始化的過程是很複雜的,因此要掌握好優先級和規則

不然 包含的變量又有不少父類 等 各個類裏面調用各類方法初始化就會讓人完全懵逼了

相關文章
相關標籤/搜索