Java虛擬機三:JVM的類加載機制

1.什麼是類的加載

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

å«ç¿»äºï¼è¿ç¯æç« ç»å¯¹è®©ä½ æ·±å»ç解javaç±»çå è½½æºå¶

需要注意的點:

  • 類加載器並不需要等到某個類被“首次主動使用”時再加載它JVM規範允許類加載器在預料某個類將要被使用時就預先加載它如果在預先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤
  • Class對象是存放在堆區的,不是方法區。類的元數據纔是存在方法區的。類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的。
  • JDK8移除了永久代,轉而使用元空間來實現方法區,創建的Class實例依舊在java heap(堆)中

編寫一個新的java類時,JVM就會幫我們編譯成class對象,存放在同名的.class文件中。在運行時,當需要生成這個類的對象,JVM就會檢查此類是否已經裝載內存中。若是沒有裝載,則把.class文件裝入到內存中。若是裝載,則根據class文件生成實例對象。創建對象的過程其實包含兩個部分:第一部分將class文件進行類加載過程。第二部分創建對象。

2.class文件類加載過程

類從磁盤加載到內存中經歷的三個階段,加載、連接、與初始化 【重點】,其中連接包含:驗證、準備、解析3 個階段。

Javaè¿é¶æç¨ä¹JVMçç±»å è½½æºå¶

①加載:查找並加載類的二進制數據(把class文件裏面的信息加載到內存裏面)

檢測類是否被加載過,jvm會先去方法區中找有沒有相應的.class類結構信息存在,如果有直接使用,如果沒有,則把相應的類的class加載到方法區。

連接就是將已經讀入到內存的類的二進制數據合併到虛擬機的運行時環境中去。三個階段說明

驗證:對加載的類進行驗證,確保被加載的類的正確性

③準備【重點】正式爲類變量分配內存並設置類變量初始值,這些內存都將在方法區中分配。

這裏需要注意兩個關鍵點,即內存分配的對象以及初始化的類型。

  • 內存分配的對象:要明白首先要知道Java 中的變量有類變量以及類成員變量兩種類型,類變量指的是被 static 修飾的變量,而其他所有類型的變量都屬於類成員變量。在準備階段,JVM 只會爲類變量分配內存,而不會爲類成員變量分配內存。類成員變量的內存分配需要等到初始化階段纔開始(初始化階段下面會講到)。

舉個例子:例如下面的代碼在準備階段,只會爲 LeiBianLiang屬性分配內存,而不會爲 ChenYuanBL屬性分配內存。

public static int LeiBianLiang = 666;

public String ChenYuanBL = "jvm";

  • 初始化的類型:在準備階段,JVM 會爲類變量分配內存,併爲其初始化(JVM 只會爲類變量分配內存,而不會爲類成員變量分配內存,類成員變量自然這個時候也不能被初始化)。但是這裏的初始化指的是爲變量賦予 Java 語言中該數據類型的默認值,而不是用戶代碼裏初始化的值。

例如下面的代碼在準備階段之後,LeiBianLiang 的值將是 0,而不是 666。

public static int LeiBianLiang = 666;

但如果一個變量是常量(被 static final 修飾)的話,那麼在準備階段,屬性便會被賦予用戶希望的值。例如下面的代碼在準備階段之後,ChangLiang的值將是 666,而不再會是 0。

public static final int ChangLiang = 666;

原因:而 final 關鍵字在 Java 中代表不可改變的意思,意思就是說 ChangLiang的值一旦賦值就不會在改變了。既然一旦賦值就不會再改變,那麼就必須一開始就給其賦予用戶想要的值,因此被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運行階段發生變化,所以就沒有必要在準備階段對它賦予用戶想要的值。

④解析:解析階段就是jvm將常量池的符號引用替換爲直接引用。

符號引用:以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可

直接引用:指向該類的該方法在方法區中的內存位置的指針

簡單的來說就是我們編寫的代碼中,當一個變量引用某個對象的時候,這個引用在.class文件中是以符號引用來存儲的。在解析階段就需要將其解析爲直接引用。如果有了直接引用,那引用的目標必定已經在內存中存在。

⑤初始化【重點】

初始化階段,才真正開始執行類中定義的java程序代碼。主要有以下步驟:

  1. 爲類的靜態變量賦予正確的初始值。
  2. 執行類的靜態代碼塊。

按照順序自上而下運行類中的變量賦值語句和靜態語句,並且只有類或接口被Java程序首次主動使用時才初始化他們。如果有父類,則首先按照順序運行父類中的變量賦值語句和靜態語句。原則:先加載父類在加載子類,先加載靜態在加載非靜態。

類的主動使用包括以下六種【重點】:

1、 創建類的實例,也就是new的方式

2、 訪問某個類或接口的靜態變量,或者對該靜態變量賦值(凡是被final修飾不不不其實更準確的說是在編譯器把結果放入常量池的靜態字段除外)

3、 調用類的靜態方法

4、 反射(如 Class.forName(“com.gx.yichun”))

5、 初始化某個類的子類,則其父類也會被初始化

6、 Java虛擬機啓動時被標明爲啓動類的類( JavaTest ),還有就是Main方法的類會首先被初始化

⑥使用:當 JVM 完成初始化階段之後,JVM 便開始從入口方法開始執行用戶的程序代碼。

⑦卸載:當用戶程序代碼執行完畢後,JVM 便開始銷燬創建的 Class 對象,最後負責運行的 JVM 也退出內存。

3、創建對象過程

上面分析類加載的過程,前五個階段完成了類的加載,加載和初始化針對類變量即靜態變量和靜態方法,本部分將以new對象的過程進行詳細說明:

類的加載過程,以Person person = new Person()爲列進行說明:

①檢測類是否被加載,JVM會先去方法區找有沒有相應的Person.class類存在,如果有直接使用。如果沒有,則把相應的類加載到JVM中(加載到方法區)。

②進行驗證(檢查)、準備(類變量分配內存以及初始化)、解析(符號引用替換爲直接引用->指向方法區內存位置)、初始化(類變量真正賦值),完成類的加載過程。

③類加載完之後,在堆內存中開闢空間分配內存地址。

④將分配到的內存空間中的數據類型都 初始化爲零值(不包括對象頭)

目的:確保對象的實列在Java代碼中可以不賦初始值就可以直接使用,程序能訪問這些字段的零值

虛擬機要對 對象頭進行必要的設置 ,例如這個對象是哪個類的實例(即所屬類)、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息都存放在對象的對象頭中。

調用對象的init()方法 ,根據傳入的屬性值給對象屬性賦值。

⑦在線程 棧中新建對象引用 ,並指向堆中剛剛新建的對象實例。

注意:虛擬機 爲新生的對象分配內存 目前常用的有兩種方式,根據使用的垃圾收集器的不同使用不同的分配機制:

  • 指針碰撞(Bump the Pointer):假設Java堆的內存是絕對規整的,所有用過的內存都放一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅把那個指針向空閒空間那邊挪動一段與對象大小相等的距離。
  •  空閒列表(Free List):如果Java堆中的內存並不是規整的,已使用的內存和空間的內存是相互交錯的,虛擬機必須維護一個空閒列表,記錄上哪些內存塊是可用的,在分配時候從列表中找到一塊足夠大的空間劃分給對象使用。

4、相關概念

雙親委派模型

當一個類加載器收到類加載請求時,它首先不會自己去加載這個類的信息,而是把該請求轉發給父類加載器,將類加載請求向上傳遞。所以所有的類加載請求都會被傳遞到父類加載器中,只有當父類加載器中沒有找到所需的類,子類加載器纔會自己嘗試去加載該類。

當前類加載器和所有父類加載器都無法加載該類時,拋出ClassNotFindException異常。

Javaè¿é¶æç¨ä¹JVMçç±»å è½½æºå¶

爲什麼要自定義類加載器?

  1. 可以從指定位置加載class文件,比如說從數據庫、雲端加載class文件
  2. 加密:Java代碼可以被輕易的反編譯,因此,如果需要對代碼進行加密,那麼加密以後的代碼,就不能使用Java自帶的ClassLoader來加載這個類了,需要自定義ClassLoader,對這個類進行解密,然後加載。

JVM類加載機制

  • 全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
  • 父類委託,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
  • 緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲什麼修改了Class後,必須重啓JVM,程序的修改纔會生效

 

文章參考:

https://www.cnblogs.com/gjmhome/p/11401397.html

https://www.toutiao.com/a6757598325442085384/?timestamp=1585356521&app=news_article&group_id=6757598325442085384&req_id=202003280848400101290320760C68F5F6

https://www.toutiao.com/a6767005022854054403/?timestamp=1585357983&app=news_article&group_id=6767005022854054403&req_id=20200328091302010129026037166BFB92

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章