一文教你讀懂JVM的類加載機制

2020-09-04 java聯網架構師

Java運行程序又被稱為WORA(Write Once Run Anywhere,在仍何地方運行只需寫入一次),意味著我們程式設計師小哥哥可以在任何一個系統上開發Java程序,但是卻可以在所有系統上暢通運行,無需任何調整,大家都知道這是JVM的功勞,但具體是JVM的哪個模塊或者什麼機制實現這一功能呢?

JVM(Java Virtual Machine, Java虛擬機)作為運行java程序的運行時引擎,也是JRE(Java Runtime Environment, Java運行時環境)的一部分。

說起它想必不少小夥伴任處於似懂非懂的狀態吧,說實話,著實是塊難啃的骨頭。但古語有云:千裡之行,始於足下。我們今天主要談談,為什麼JVM無需了解底層文件或者文件系統即可運行Java程序?

--這主要是類加載機制在運行時將Java類動態加載到JVM的緣故。

當我們編譯.java文件時,Java編譯器會生成與.java文件同名的.class文件(包含字節碼)。當我們運行時,.class文件會進入到各個步驟,這些步驟共同描繪了整個JVM,上圖便是一張精簡的JVM架構圖。

今天,我們的主角就是類加載機制 - 說白了,就是將.class文件加載到JVM內存中,並將其轉化為java.lang.Class對象的過程。這對這個過程,我們可以細分為如下幾個階段:

  • 加載
  • 連接(驗證,準備,解析)
  • 初始化


注意: 正常場景下,加載的流程如上。但是Java語言本身支持運行時綁定,所以解析階段是用可能放在初始化之後進行的,稱為動態綁定或者晚期綁定。

I.類加載流程

1. 加載

加載:通過類的全局限定名找到.class文件,並利用.class文件創建一個java.lang.Class對象。

  • 根據類的全局限定名找到.class文件,生成對應的二進位字節流。
  • 將靜態存儲結構轉換為運行時數據結構,保存運行時數據結構到JVM內存方法區中。
  • JVM創建java.lang.Class類型的對象,保存於堆(Heap)中。利用該對象,可以獲取保存於方法區中的類信息,例如:類名稱,父類名稱,方法和變量等信息。

For Example:


package com.demo;import java.lang.reflect.Field;import java.lang.reflect.Method;public class ClassLoaderExample { public static void main(String[] args) { StringOp stringOp = new StringOp(); System.out.println(&34; + stringOp.getClass().getName()); for(Method method: stringOp.getClass().getMethods()) { System.out.println(&34; + method.getName()); } for (Field field: stringOp.getClass().getDeclaredFields()) { System.out.println(&34; + field.getName()); } }}

StringOp.class

package com.demo;public class StringOp { private String displayName; private String address; public String getDisplayName() { return displayName; } public String getAddress() { return address; }}

output:

Class Name: com.demo.StringOpMethod Name: getAddressMethod Name: getDisplayNameField Name: displayNameField Name: address

注意:對於每個加載的.class文件,僅會創建一個java.lang.Class對象.

StringOp stringOp1 = new StringOp();StringOp stringOp2 = new StringOp();System.out.println(stringOp1.getClass() == stringOp2.getClass()); //output: true

2. 連接

2.1 驗證

驗證:主要是確保.class文件的正確性,由有效的編譯器生成,不會對影響JVM的正常運行。通常包含如下四種驗證:

  • 文件格式:驗證文件的格式是否符合規範,如果符合規範,則將對應的二進位字節流存儲到JVM內存的方法區中;否則拋出java.lang.VerifyError異常。
  • 元數據:對字節碼的描述信息進行語義分析,確保符合Java語言規範。例如:是否有父類;是否繼承了不允許繼承的類(final修飾的類);如果是實體類實現接口,是否實現了所有的方法;等。。
  • 字節碼:驗證程序語義是否合法,確保目標類的方法在被調用時不會影響JVM的正常運行。例如int類型的變量是否被當成String類型的變量等。
  • 符號引用:目標類涉及到其他類的的引用時,根據引用類的全局限定名(例如:import com.demo.StringOp)能否找到對應的類;被引用類的欄位和方法是否可被目標類訪問(public, protected, package-private, private)。這裡主要是確保後續目標類的解析步驟可以順利完成。

2.2 準備

準備:為目標類的靜態欄位分配內存設置默認初始值(當欄位被final修飾時,會直接賦值而不是默認值)。需要注意的是,非靜態變量只有在實例化對象時才會進行欄位的內存分配以及初始化。

public class CustomClassLoader { //加載CustomClassLoader類時,便會為var1變量分配內存 //準備階段,var1賦值256 public static final int var1 = 256; //加載CustomClassLoader類時,便會為var2變量分配內存 //準備階段,var2賦值0, 初始化階段賦值128 public static int var2 = 128; //實例化一個CustomClassLoader對象時,便會為var1變量分配內存和賦值 public int var3 = 64; }

注意:靜態變量存在方法區內存中,實例變量存在堆內存中。

這裡簡單貼一下Java不同變量的默認值:

數據類型默認值int0float0.0flong0Ldouble0.0dshort(short)0char&39;byte(byte)0StringnullbooleanfalseArrayListnullHashMapnull

2.3 解析

解析:將符號引用轉化為直接引用的過程。

  • 符號引用(Symbolic Reference):描述所引用目標的一組符號,使用該符號可以唯一標識到目標即可。比如引用一個類:com.demo.CustomClassLoader,這段字符串就是一個符號引用,並且引用的對象不一定事先加載到內存中。
  • 直接引用(Direct Reference):直接指向目標的指針,相對偏移量或者一個能間接定位到目標的句柄。根據直接引用的定義,被引用的目標一定事先加載到了內存中。

3. 初始化

前面的準備階段時,JVM為目標類的靜態變量分配內存並設置默認初始值(final修飾的靜態變量除外),但到了初始化階段會根據用戶編寫的代碼重新賦值。換句話說:初始化階段就是JVM執行類構造器方法<clinit>()的過程。

<init>()<clinit>()從名字上來看,非常的類似,或許某些童鞋會給雙方畫上等號。然則,對於JVM來說,雖然兩者皆被稱為構造器方法,但此構造器非彼構造器。

  • <init>():對象構造器方法,用於初始化實例對象
    • 實例對象的constructor(s)方法,和非靜態變量的初始化;
    • 執行new創建實例對象時使用。
  • <clinit>():類構造器方法,用於初始化類
    • 類的靜態語句塊和靜態變量的初始化;
    • 類加載的初始化階段執行。

For Example:

public class ClassLoaderExample { private static final Logger logger = LoggerFactory.getLogger(ClassLoaderExample.class);//<clinit> private String property = &34;; //<init> //<clinit> static { System.out.println(&34;); } //<init> ClassLoaderExample() { System.out.println(&34;); } //<init> ClassLoaderExample(String property) { this.property = property; System.out.println(&34;); }}

查看對應的字節碼:

public ClassLoaderExample(); <init>

Code: 0 aload_0 //將局部變量表中第一個引用加載到操作樹棧 1 invokespecial 2 <custom> //將常量custom從常量池第二個位置推送至棧頂 7 putfield 4 <java/lang/System.out> //從java.lang.System類中獲取靜態欄位out13 ldc 6 <java/io/PrintStream.println> //調用java.io.PrintStream對象的println實例方法,列印棧頂的Instance Initializing...18 return //返回

public ClassLoaderExample(String property); <init>

Code: 0 aload_0 //將局部變量表中第一個引用加載到操作樹棧 1 invokespecial 2 <custom> //將常量custom從常量池第二個位置推送至棧頂 7 putfield 3 <com/kaiwu/ClassLoaderExample.property> //將入參property賦值給com.kaiwu.ClassLoaderExample實例對象的property欄位15 getstatic 5 <Instance Initializing...> //將常量Instance Initializing...從常量池第5個位置推送至棧頂20 invokevirtual FF0000; --tt-darkmode-color: 7 <com/kaiwu/ClassLoaderExample> //將com.kaiwu.ClassLoaderEexample的class_info常量從常量池第七個位置推送至棧頂 2 invokestatic 9 <com/kaiwu/ClassLoaderExample.logger> //設置com.kaiwu.ClassLoaderExample類的靜態欄位logger 8 getstatic 10 <Static Initializing...> //將常量Static Initializing...從常量池第10個位置推送至棧頂13 invokevirtual FF0000; --tt-darkmode-color: 34;ClassLoader of StringOp: &34;ClassLoader of Logging: &34;ClassLoader of String: &FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: FF0000; --tt-darkmode-color: #FF0C00;">null)。


總體而言,JVM的類加載機制並非想像中那麼複雜,若靜下心來,仔細琢磨一二,亦感其中妙趣。

以上為個人解讀與理解,如有不明之處,望各位大佬不吝賜教。

相關焦點

  • 大白話談JVM的class類加載機制
    整體的運行流程就是這樣,相信小夥伴們都很清楚這些,但是有關類加載器是如何把類加載到jvm內存中的,小夥伴們有考慮過嗎?今天我們主要就是聊這一部分。 JVM什麼時候加載類其實說到類加載的底層機制,這是一個很複雜的過程,但是對於我們平時的工作來講,只要懂得它的核心原理就可以了。
  • 一文徹底搞懂|JVM 類加載機制
    學習導圖一.為什麼要學習類加載機制?今天想跟大家嘮嗑嘮嗑Java的類加載機制,這是Java的一個很重要的創新點,曾經也是Java流行的重要原因之一。Oracle當初引入這個機制是為了滿足Java Applet開發的需求,JVM咬咬牙引入了Java類加載機制,後來的基於Jvm的動態部署,插件化開發包括大家熱議的熱修復,總之很多後來的技術都源於在JVM中引入了類加載器。如今,類加載機制也在各個領域大放異彩,在面試中,由類加載機制所衍生出來各類面試題也層出不窮。
  • 深入淺出JVM性能調優——JVM內存模型和類加載運行機制
    一、JVM內存模型運行一個 Java 應用程式,必須要先安裝 JDK 或者 JRE 包。因為 Java 應用在編譯後會變成字節碼,通過字節碼運行在 JVM 中,而 JVM 是 JRE 的核心組成部分。JVM 不僅承擔了 Java 字節碼的分析和執行,同時也內置了自動內存分配管理機制。
  • 關於JVM類加載機制,看這一篇就夠了
    類的加載機制Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這個過程被稱作虛擬機的類加載機制。.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;ClassLoader.loadClass(): 只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。
  • JVM加載class文件的原理機制詳解
    回到頂部3、JVM加載class文件的原理機制    Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬碟讀取到內存中。  類裝載方式,有兩種   1.隱式裝載, 程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中,   2.顯式裝載, 通過class.forname()等方法,顯式加載需要的類   隱式加載與顯式加載的區別:兩者本質是一樣?
  • JVM入門(一)類的加載過程
    如果我們是以jar的方式進行運行java程序,那麼執行 :java -jar *.jar com.xx.Hello 起中 Hello是這個jar中包含了main方法的類的名稱。無論是那種方式,當我們執行的時候,就會啟動jvm虛擬機去加載所要執行的Hello.class文件到虛擬機中。
  • java類加載機制和類加載器
    類加載機制java類從被加載到JVM到卸載出JVM,整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸載(Unloading)七個階段。其中驗證、準備和解析三個部分統稱為連接(Linking)。
  • 虛擬機系列 | JVM類加載機制
    一、類加載簡介類的加載機制是指把編譯後的.class類文件的二進位數據讀取到內存中,並為之創建一個java.lang.Class對象,用來封裝類在元數據空間的數據結構>引導類加載器Bootstrap-ClassLoader基於C/C++實現,負責加載Java的核心類庫JAVA_HOME\jre\lib\rt.jar,該加載器不繼承自ClassLoader抽象類,並且只加載包名為java、javax、sun等開頭類,一次保證對核心源碼的保護。
  • 深入解析JVM類加載器子系統,你還不了解的都在這裡
    一、類加載子系統的作用類加載子系統負責從文件系統或者網絡中加載Class文件,class文件在文件開頭有特定的文件標識;ClassLoader只負責class文件的加載,至於它是否可以運行,則由Execution Engine決定加載的類信息存放於一塊成為方法區的內存空間。
  • JVM性能調優——JVM內存模型和類加載運行機制
    JVM 不僅承擔了 Java 字節碼的分析和執行,同時也內置了自動內存分配管理機制。這個機制可以大大降低手動分配回收機制可能帶來的內存洩露和內存溢出風險,使 Java 開發人員不需要關注每個對象的內存分配以及回收,從而更專注於業務本身。
  • 你有真正理解 Java 的類加載機制嗎?|原力計劃
    作者 | 宜春責編 | Elle出品 | CSDN 博客你是否真的理解Java的類加載機制?點進文章的盆友不如先來做一道非常常見的面試題,如果你能做出來,可能你早已掌握並理解了Java的類加載機制,若結果出乎你的意料,那就很有必要來了解了解Java的類加載機制了。
  • 妙啊,一文解析虛擬機系列 | JVM類加載機制
    一、類加載簡介類的加載機制是指把編譯後的.class類文件的二進位數據讀取到內存中,並為之創建一個java.lang.Class對象,用來封裝類在元數據空間的數據結構。類加載器收到了類加載的請求時,不會自己先去嘗試加載這個類,而是把請求委託給父加載器去執行;如果父加載器還存在父類加載器,則依次向上委託,因此類加載請求最終都應該被傳遞到頂層的啟動類加載器中
  • Java類加載機制,你理解了嗎?
    我們知道,我們寫的java文件是不能直接運行的,我們可以在IDEA中右鍵文件名點擊運行,這中間其實摻雜了一系列的複雜處理過程。這篇文章,我們只討論我們的代碼在運行之前的一個環節,叫做類的加載。按照我寫文章的常規慣例,先給出這篇文章的大致結構;首先,認識類加載機制,然後,詳細介紹類加載的過程。最後,介紹了類加載器,還有雙親委派原則。
  • JVM中Java類的生命周期,一文搞定
    這裡的「加載階段」和我們常說的「類加載」是兩回事,「類加載」指的是虛線框中三部分加起來。加載(Loading)加載,是指查找字節流,並且據此創建類的過程。是類加載過程的一個階段。這些類加載器需要先由另一個類加載器,比如說啟動類加載器,加載至 Java 虛擬機中,方能執行類加載。標準擴展(Extension)類加載器它負責加載相對次要、但又通用的類,負責將 JAVA_HOME/jre/lib/ext 或者由系統變量 java.ext.dirs指定位置中的類庫加載到內存中。
  • 騷操作:不重啟 JVM,如何替換掉已經加載的類?
    「方法區中的數據是類加載時從class文件中提取出來的。」「class文件從哪來?」「從Java或者其他符合JVM規範的原始碼中編譯而來。」「原始碼從哪來?」「廢話,當然是手寫!」「倒著推,手寫沒問題,編譯沒問題,至於加載……有沒有辦法加載一個已經加載過的類呢?
  • 詳解JVM類加載
    雙親委派6.1 雙親委派機制每一個類都有一個對於它的類加載器。系統中的ClassLoader在協同工作時會默認使用雙親委派機制。在類加載的時候,首先判斷該類是否被加載,已經加載過的類無需加載會直接返回,否則會自己嘗試加載。加載的時候,首先會把該請求委派給父類加載器進行處理,因此所有的請求最終都會傳送到頂層的啟動類加載BootstrapClassLoader加載器中。當父類加載加載器無法處理時,才會自己進行處理。
  • 大廠面試系列(一)::JVM基礎
    類加載器的本質類加載器為什麼有三層結構怎麼自定義類加載器做容器隔離?講講類加載機制唄?都有哪些類加載器,這些類加載器都加載哪些文件?手寫一下類加載Demo Classloader作用講一講類加載器工作機制?你知道強引用、弱引用和軟引用嗎?為什麼要有這些東西?他們有什麼作用?
  • JVM的藝術—類加載器篇
    那麼就向下加載2加載:jvm加載類的時候是通過雙親委派的方式去加載委託,但是加載的時候是由上向下去加載的,當委託到最頂層啟動類加載器的時候,無法在向上委託,那麼啟動類加載器就開始嘗試去加載這個類如果文字描述你還不清楚什麼是雙親委託機制,那麼我畫了一幅圖可以更清楚類加載的過程。
  • JVM的雙親委派機制
    JVM的雙親委派機制JVM類加載器是什麼機制?為什麼使用這種機制(這種機制的好處是什麼)?說下類加載流程?用代碼驗證類加載機制。為什麼要破壞類的這種加載機制?JVM雙親委派機制,簡單來說:我爸是李剛,有事找我爸。用三個字來說:往上捅。不信?咱們一起看看我們已經知道了JVM類加載器的四種加載機制,那麼這四種加載機制是怎麼個加載過程呢?
  • jvm系列一:我們的java程序如何跑起來
    2、jvm如何加載class字節碼上文提到將class字節碼加載到jvm內存中,其實就是一個類加載過程。那麼我們的類加載過程是怎麼樣的呢?,類是什麼時候加載到內存的,它的入口是什麼?當然,當我們啟動一個jvm進程時,類就會加載到內存中,並且在類中找到main方法,這個就是入口,從而開始加載你的程序,以下面這個程序為例: