JVM實戰---類載入的過程

来源:https://www.cnblogs.com/JavaEdge/archive/2019/08/09/11324787.html
-Advertisement-
Play Games

任何程式都需要載入到記憶體才能與CPU進行交流 同理, 位元組碼.class文件同樣需要載入到記憶體中,才可以實例化類 的使命就是提前載入.class 類文件到記憶體中 在載入類時,使用的是Parents Delegation Model(溯源委派載入模型) Java的類載入器是一個運行時核心基礎設施模塊, ...


任何程式都需要載入到記憶體才能與CPU進行交流
同理, 位元組碼.class文件同樣需要載入到記憶體中,才可以實例化類
ClassLoader的使命就是提前載入.class 類文件到記憶體中
在載入類時,使用的是Parents Delegation Model(溯源委派載入模型)

Java的類載入器是一個運行時核心基礎設施模塊,主要是在啟動之初進行類的載入、鏈接、初始化
Java 類載入過程

第一步,Load階段

讀取類文件產生二進位流,並轉為特定數據結構,初步校驗cafe babe魔法數、常量池、文件長度、是否有父類等,然後創建對應類的java.lang.Class實例

第二步,Link階段

包括驗證、準備、解析三個步驟

  • 驗證是更詳細的校驗,比如final是否合規、類型是否正確、靜態變數是否合理等
  • 準備階段是為靜態變數分配記憶體,並設定預設值,解析類和方法確保類與類之間的相互引用正確性,完成記憶體結構佈局

    第三步,Init 階段

    執行類構造器 方法,如果賦值運算是通過其他類的靜態方法來完成的,那麼會馬上解析另外一個類,在虛擬機棧中執行完畢後通過返回值進行賦值

類載入是一個將.class位元組碼文件實例化成Class對象併進行相關初始化的過程。
在這個過程中,JVM會初始化繼承樹上還沒有被初始化過的所有父類,並且會執行這個鏈路上所有未執行過的靜態代碼塊、靜態變數賦值語句等。
某些類在使用時,也可以按需由類載入器進行載入。

全小寫的class是關鍵字,用來定義類
而首字母大寫的Class,它是所有class的類
這句話理解起來有難度,類已經是現實世界中某種事物的抽象,為什麼這個抽象還是另外一個類Class的對象?
示例代碼如下:




● 第1處說明:
Class類下的newInstance()在JDK9中已經置為過時,使用getDeclaredConstructor().newInstance()的方式
著重說明一下new與newInstance的區別

  • new是強類型校驗,可以調用任何構造方法,在使用new操作的時候,這個類可以沒有被載入過
  • 而Class類下的newInstance是弱類型,只能調用無參構造方法
    • 如果沒有預設構造方法,就拋出InstantiationException異常;
    • 如果此構造方法沒有許可權訪問,則拋 IllegalAccessException異常

Java 通過類載入器把類的實現與類的定義進行解耦,所以是實現面向介面編程、依賴倒置的必然選擇。

● 第2處說明:
可以使用類似的方式獲取其他聲明,如註解、方法等
類的反射信息

● 第3處說明: private 成員在類外是否可以修改?
通過setccessible(true),即可使用Class類的set方法修改其值
如果沒有這一步,則拋出如下異常:

類載入器

類載入器是如何定位具體的類文件並讀取的呢?

在類載入器家族中存在著類似人類社會的權力等級制度

  • 最高層的Bootstrap
    在JVM啟動時創建的,通常由與操作系統相關的本地代碼實現,是最根基的類載入器,負責裝載最核心的Java類,比如Object、System、 String ,Java運行時的rt.jar等jar包
  • JDK9的Platform ClassLoader
    負責載入\lib\ext目錄中的,或者java.ext.dirs系統變數指定的路徑中的所以類庫;
    載入一些擴展的系統類,比如XML、加密、壓縮相關的功能類等;
    JDK9之前是Extension ClassLoader.
  • 第三層 Application ClassLoader
    應用類載入器,主要是載入用戶定義的CLASSPATH路徑下的類

第二、三層類載入器為Java語言實現,用戶也可以自定義類載入器
查看本地類載入器的方式如下:

在JDK8環境中,執行結果如下

AppClassLoader的Parent為Bootstrap,它是通過C/C++實現的,並不存在於JVM體系內,所以輸出為 null

低層次的當前類載入器,不能覆蓋更高層次類載入器已經載入的類
如果低層次的類載入器想載入一個未知類,要非常禮貌地向上逐級詢問:“請問,這個類已經載入了嗎?”
被詢問的高層次類載入器會自問兩個問題

  • 我是否已載入過此類
  • 如果沒有,是否可以載入此類

只有當所有高層次類載入器在兩個問題的答案均為“否”時,才可以讓當前類載入器載入這個未知類
左側綠色箭頭向上逐級詢問是否已載入此類,直至Bootstrap ClassLoader,然後向下逐級嘗試是否能夠載入此類,如果都載入不了,則通知發起載入請求的當前類載入器,准予載入
在右側的三個小標簽里,列舉了此層類載入器主要載入的代表性類庫,事實上不止於此

通過如下代碼可以查看Bootstrap 所有已載入類庫

執行結果

Bootstrap載入的路徑可以追加,不建議修改或刪除原有載入路徑
在JVM中增加如下啟動參數,則能通過Class.forName正常讀取到指定類,說明此參數可以增加Bootstrap的類載入路徑:

-Xbootclasspath/a:/Users/sss/book/ easyCoding/byJdk11/src

如果想在啟動時觀察載入了哪個jar包中的哪個類,可以增加

-XX:+TraceClassLoading

此參數在解決類衝突時非常實用,畢竟不同的JVM環境對於載入類的順序並非是一致的
有時想觀察特定類的載入上下文,由於載入的類數量眾多,調試時很難捕捉到指定類的載入過程,這時可以使用條件斷點功能
比如,想查看HashMap的載入過程,在loadClass處打個斷點,並且在condition框內輸入如圖
設置條件斷點

JVM如何確立每個類在JVM的唯一性

類的全限定名和載入這個類的類載入器的ID

在學習了類載入器的實現機制後,知道雙親委派模型並非強制模型,用戶可以自定義類載入器,在什麼情況下需要自定義類載入器呢?

  • 隔離載入類
    在某些框架內進行中間件與應用的模塊隔離,把類載入到不同的環境
    比如,阿裡內某容器框架通過自定義類載入器確保應用中依賴的jar包不會影響到中間件運行時使用的jar包
  • 修改類載入方式
    類的載入模型並非強制,除Bootstrap外,其他的載入並非一定要引入,或者根據實際情況在某個時間點進行按需進行動態載入
  • 擴展載入源
    比如從資料庫、網路,甚至是電視機機頂盒進行載入
  • 防止源碼泄露
    Java代碼容易被編譯和篡改,可以進行編譯加密。那麼類載入器也需要自定義,還原加密的位元組碼。

實現自定義類載入器的步驟

  • 繼承ClassLoader
  • 重寫findClass()方法
  • 調用defineClass()方法

一個簡單的類載入器實現的示例代碼如下

由於中間件一般都有自己的依賴jar包,在同一個工程內引用多個框架時,往往被迫進行類的仲裁
按某種規則jar包的版本被統一指定, 導致某些類存在包路徑、類名相同的情況,就會引起類衝突,導致應用程式出現異常
主流的容器類框架都會自定義類載入器,實現不同中間件之間的類隔離,有效避免了類衝突。

1 載入的定位

“載入”是“類載入”(Class Loading)過程的第一步

1.1 載入的過程

在載入的過程中,JVM主要做3件事情

  • 通過一個類的全限定名來獲取定義此類的二進位位元組流(class文件)
    在程式運行過程中,當要訪問一個類時,若發現這個類尚未被載入,並滿足類初始化的條件時,就根據要被初始化的這個類的全限定名找到該類的二進位位元組流,開始載入過程
  • 將這個位元組流的靜態存儲結構轉化為方法區的運行時數據結構
  • 在記憶體中創建一個該類的java.lang.Class對象,作為方法區該類的各種數據的訪問入口

程式在運行中所有對該類的訪問都通過這個類對象,也就是這個Class對象是提供給外界訪問該類的介面

1.2 載入源

JVM規範對於載入過程給予了較大的寬鬆度.一般二進位位元組流都從已經編譯好的本地class文件中讀取,此外還可以從以下地方讀取

  • zip包
    Jar、War、Ear等
  • 其它文件生成
    由JSP文件中生成對應的Class類.
  • 資料庫中
    將二進位位元組流存儲至資料庫中,然後在載入時從資料庫中讀取.有些中間件會這麼做,用來實現代碼在集群間分發
  • 網路
    從網路中獲取二進位位元組流.典型就是Applet.
  • 運行時計算生成
    動態代理技術,用PRoxyGenerator.generateProxyClass為特定介面生成形式為"*$Proxy"的代理類的二進位位元組流.

    1.3 類和數組載入過程的區別

    數組也有類型,稱為“數組類型”.如:
String[] str = new String[10];

這個數組的數組類型是Ljava.lang.String,而String只是這個數組的元素類型

當程式在運行過程中遇到new關鍵字創建一個數組時,由JVM直接創建數組類,再由類載入器創建數組中的元素類型.

而普通類的載入由類載入器創建.既可以使用系統提供的引導類載入器,也可以由用戶自定義的類載入器完成(即重寫一個類載入器的loadClass()方法)

1.4 載入過程的註意點

  • JVM規範並未給出類在方法區中存放的數據結構
    類完成載入後,二進位位元組流就以特定的數據結構存儲在方法區中,但存儲的數據結構是由虛擬機自己定義的,虛擬機規範並沒有指定
  • JVM規範並沒有指定Class對象存放的位置
    在二進位位元組流以特定格式存儲在方法區後,JVM會創建一個java.lang.Class類的對象,作為本類的外部訪問介面
    既然是對象就應該存放在Java堆中,不過JVM規範並沒有給出限制,不同的虛擬機根據自己的需求存放這個對象
    HotSpot將Class對象存放在方法區.
  • 載入階段和鏈接階段是交叉的
    類載入的過程中每個步驟的開始順序都有嚴格限制,但每個步驟的結束順序沒有限制.也就是說,類載入過程中,必須按照如下順序開始:

    載入 -> 鏈接 -> 初始化

但結束順序無所謂,因此由於每個步驟處理時間的長短不一就會導致有些步驟會出現交叉

2 驗證

驗證階段比較耗時,它非常重要但不一定必要(因為對程式運行期沒有影響),如果所運行的代碼已經被反覆使用和驗證過,那麼可以使用-Xverify:none參數關閉,以縮短類載入時間

2.1 驗證的目的

保證二進位位元組流中的信息符合虛擬機規範,並沒有安全問題

2.2 驗證的必要性

雖然Java語言是一門安全的語言,它能確保程式猿無法訪問數組邊界以外的記憶體、避免讓一個對象轉換成任意類型、避免跳轉到不存在的代碼行.也就是說,Java語言的安全性是通過編譯器來保證的.

但是我們知道,編譯器和虛擬機是兩個獨立的東西,虛擬機只認二進位位元組流,它不會管所獲得的二進位位元組流是哪來的,當然,如果是編譯器給它的,那麼就相對安全,但如果是從其它途徑獲得的,那麼無法確保該二進位位元組流是安全的。

通過上文可知,虛擬機規範中沒有限制二進位位元組流的來源,在位元組碼層面上,上述Java代碼無法做到的都是可以實現的,至少語義上是可以表達出來的,為了防止位元組流中有安全問題,需要驗證!

2.3 驗證的過程

  • 文件格式驗證
    驗證位元組流是否符合Class文件格式的規範,並且能被當前的虛擬機處理.
    本驗證階段是基於二進位位元組流進行的,只有通過本階段驗證,才被允許存到方法區
    後面的三個驗證階段都是基於方法區的存儲結構進行,不會再直接操作位元組流.

通過上文可知,載入開始前,二進位位元組流還沒進方法區,而載入完成後,二進位位元組流已經存入方法區
而在文件格式驗證前,二進位位元組流尚未進入方法區,文件格式驗證通過之後才進入方法區
也就是說,載入開始後,立即啟動了文件格式驗證,本階段驗證通過後,二進位位元組流被轉換成特定數據結構存儲至方法區中,繼而開始下階段的驗證和創建Class對象等操作
這個過程印證了:載入和驗證是交叉進行的

  • 元數據驗證
    對位元組碼描述信息進行語義分析,確保符合Java語法規範.
  • 位元組碼驗證
    本階段是驗證過程的最複雜的一個階段.
    本階段對方法體進行語義分析,保證方法在運行時不會出現危害虛擬機的事件.
    位元組碼驗證將對類的方法進行校驗分析,保證被校驗的方法在運行時不會做出危害虛擬機的事,一個類方法體的位元組碼沒有通過位元組碼驗證,那一定有問題,但若一個方法通過了驗證,也不能說明它一定安全
  • 符號引用驗證
    發生在JVM將符號引用轉化為直接引用的時候,這個轉化動作發生在解析階段,對類自身以外的信息進行匹配校驗,確保解析能正常執行.

    3 準備

    完成兩件事情
  • 為已在方法區中的類的靜態成員變數分配記憶體
  • 為靜態成員變數設置初始值
    初始值為0、false、null等
public static final int value = 123;

準備階段後 a 的值為 0,而不是 123,要在初始化之後才變為 123,但若被final修飾的常量如果有初始值,那麼在編譯階段就會將初始值存入constantValue屬性中,在準備階段就將constantValue的值賦給該欄位(此處將value賦為123).

4 解析

解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程.

5 初始化

真正開始執行類中定義的Java程式代碼(或者說是位元組碼)
初始化階段就是執行類構造器clinit()的過程.

clinit()方法由編譯器自動產生,收集類中static{}代碼塊中的類變數賦值語句和類中靜態成員變數的賦值語句。在準備階段,類中靜態成員變數已經完成了預設初始化,而在初始化階段,clinit()方法對靜態成員變數進行顯示初始化。

5.1 初始化過程的註意點

  • clinit()方法是IDE自動收集類中所有類變數的賦值動作和靜態語句塊中的語句合併產生的,IDE收集的順序是由語句在源文件中出現的順序所決定的.
  • 靜態代碼塊只能訪問到出現在靜態代碼塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問.
public class Test {
    static {
        i=0;
        System.out.println(i);//編譯失敗:"非法向前引用"
    }
    static int i = 1;
}
  • 實例構造器init()需要顯示調用父類構造函數,而類的clinit()不需要調用父類的類構造函數,虛擬機會確保子類的clinit()方法執行前已經執行完畢父類的clinit()方法.因此在JVM中第一個被執行的clinit()方法的類肯定是java.lang.Object.
  • 如果一個類/介面中沒有靜態代碼塊,也沒有靜態成員變數的賦值操作,那麼編譯器就不會為此類生成clinit()方法.
  • 介面也需要通過clinit()方法為介面中定義的靜態成員變數顯示初始化。
  • 介面中不能使用靜態代碼塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成clinit()方法.不同的是,執行介面的clinit()方法不需要先執行父介面的clinit()方法.只有當父介面中的靜態成員變數被使用到時才會執行父介面的clinit()方法.
  • 虛擬機會保證在多線程環境中一個類的clinit()方法別正確地加鎖,同步.當多條線程同時去初始化一個類時,只會有一個線程去執行該類的clinit()方法,其它線程都被阻塞等待,直到活動線程執行clinit()方法完畢.

其他線程雖會被阻塞,只要有一個clinit()方法執行完,其它線程喚醒後不會再進入clinit()方法.同一個類載入器下,一個類型只會初始化一次.

參考

  • 《碼到成功》

  • 《深入理解Java虛擬機第二版》


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 方法一:通過修改.bashrc或.bash_profile文件來實現  通過修改home目錄下的.bashrc或.bash_profile文件來實現。這兩個文件選擇其中一個在末尾加入如下一行,具體操作如下: [GeekDevOps@GeekDevOps /]$ cd ~ [GeekDevOps@G... ...
  • 1、下載docker源碼包 docker官網地址: https://download.docker.com/linux/static/stable/ 選擇.tgz的包下載,例如:https://download.docker.com/linux/static/stable/x86_64/docker... ...
  • #!/bin/bash fileNum=`ls /bin/testShell/apiCheck_shell/logs/ | grep $(date "+%Y-%m-%d") | wc -w`day=$(date "+%Y-%m-%d")-${fileNum}# 設置併發的進程數thread_num= ...
  • 前段時間工作中需要將經過我司平臺某些信息核驗數據提取後上傳到客戶的FTP伺服器上,以便於他們進行相關的信息比對核驗。由於包含這些信息的主機只有4台,採取的策略是將生成的4個文件彙集到一個主機上,然後在這台主機上將文件上傳的目標ftp伺服器。 1,建立主機A到其他三台主機之間的信任關係,以便於遠程拷貝 ...
  • 一、多命令順序執行 && || 相當於其他高級語言中的 ? : 二、管道符 [命令1] | [命令2] 命令1的正確輸出作為命令2的操作對象 分屏顯示結果 netstat -an 命令可查看系統所有的網路連接 可使用管道符,netstat -an | grep ESTABLISHED查看正在連接系統 ...
  • 使用 unzip XXX.zip 方式解壓的時候會出現中文亂碼 很多人推薦以下方式: 但是unzip已經不支持了 親測有效的方式為: 7z安裝參考:https://www.cnblogs.com/crazytata/p/10235812.html 相關參考鏈接:https://www.cnblogs ...
  • 剛開始學習linux操作系統是總是很茫然,無所適從,以下是自己總結的工作經驗,僅供參考! 一、準備資源 安裝前需要準備的資源有linux系統centos7.6發行版系統鏡像,vmware workstations15 模擬安裝虛擬機工具。 二、安裝步驟 1, 安裝VMware workstation ...
  • 1 電腦的基本硬體組成 早期,DIY一臺電腦,要先有三大件 CPU 記憶體 主板 1.1 CPU 電腦最重要的核心配件,中央處理器(Central Processing Unit)。 電腦的所有“計算”都是由CPU來進行的。 CPU是一個超級精細的印刷電路版 1.2 記憶體(Memory) 你撰 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...