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語言本身支持運行時綁定,所以解析階段是用可能放在初始化之後進行的,稱為動態綁定或者晚期綁定。
加載:通過類的全局限定名找到.class文件,並利用.class文件創建一個java.lang.Class對象。
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
驗證:主要是確保.class文件的正確性,由有效的編譯器生成,不會對影響JVM的正常運行。通常包含如下四種驗證:
準備:為目標類的靜態欄位分配內存並設置默認初始值(當欄位被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
解析:將符號引用轉化為直接引用的過程。
前面的準備階段時,JVM為目標類的靜態變量分配內存並設置默認初始值(final修飾的靜態變量除外),但到了初始化階段會根據用戶編寫的代碼重新賦值。換句話說:初始化階段就是JVM執行類構造器方法<clinit>()的過程。
<init>()和<clinit>()從名字上來看,非常的類似,或許某些童鞋會給雙方畫上等號。然則,對於JVM來說,雖然兩者皆被稱為構造器方法,但此構造器非彼構造器。
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的類加載機制並非想像中那麼複雜,若靜下心來,仔細琢磨一二,亦感其中妙趣。
以上為個人解讀與理解,如有不明之處,望各位大佬不吝賜教。