您好,登錄后才能下訂單哦!
小編給大家分享一下Java虛擬機之類加載的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
Java是一門面向對象編程語言,可以編寫桌面應用程序、Web應用程序、分布式系統和嵌入式系統應用程序。
類加載的流程可以簡單分為三步:
加載
連接
初始化
而其中的連接又可以細分為三步:
驗證
準備
解析
下面會分別對各個流程進行介紹。
在了解類接在流程之前,先來看一下觸發類加載的條件。
JVM
不會無條件加載類,只有在一個類或接口在初次使用的時候,必須進行初始化。這里的使用是指主動使用,主動使用包括如下情況:
創建一個類的實例的時候:比如使用new
創建,或者使用反射、克隆、反序列化
調用類的靜態方法的時候:比如使用invokestatic
指令
使用類或接口的靜態字段:比如使用getstatic
/putstatic
指令
使用java.lang.reflect
中的反射類方法時
初始化子類時,要求先初始化父類
含有main()
方法的類
除了以上情況外,其他情況屬于被動使用,不會引起類的初始化。
比如下面的例子:
public class Main { public static void main(String[] args){ System.out.println(Child.v); } } class Parent{ static{ System.out.println("Parent init"); } public static int v = 100; } class Child extends Parent{ static { System.out.println("Child init"); } }
輸出如下:
Parent init
100
而加上類加載參數-XX:+TraceClassLoading
后,可以看到Child
確實被加載了:
[0.068s][info ][class,load] com.company.Main [0.069s][info ][class,load] com.company.Parent [0.069s][info ][class,load] com.company.Child Parent init 100
但是并沒有進行初始化。另外一個例子是關于final
的,代碼如下:
public class Main { public static void main(String[] args){ System.out.println(Test.STR); } } class Test{ static{ System.out.println("Test init"); } public static final String STR = "Hello"; }
輸出如下:
[0.066s][info ][class,load] com.company.Main
Hello
Test
類根本沒有被加載,因為final
被做了優化,編譯后的Main.class
中,并沒有引用Test
類:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String Hello 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
在字節碼偏移3的位置,通過ldc
將常量池第4項入棧,此時在字節碼文件中常量池第4項為:
#3 = Class #24 // com/company/Test #4 = String #25 // Hello #5 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
因此并沒有對Test
類進行加載,只是直接引用常量池中的常量,因此輸出沒有Test
的加載日志。
類加載的時候,JVM
必須完成以下操作:
通過類的全名獲取二進制數據流
解析類的二進制數據流為方法區內的數據結構
創建java.lang.Class
類的實例,表示該類型
第一步獲取二進制數據流,途徑有很多,包括:
字節碼文件
JAR
/ZIP
壓縮包
從網絡加載
等等,獲取到二進制數據流后,JVM
進行處理并轉化為一個java.lang.Class
實例。
驗證的操作是確保加載的字節碼是合法、合理并且規范的。步驟簡略如下:
格式檢查:判斷二進制數據是否符合格式要求和規范,比如是否以魔數開頭,主版本號和小版本號是否在當前JVM
支持范圍內等等
語義檢查:比如是否所有類都有父類存在,一些被定義為final
的方法或類是否被重載了或者繼承了,是否存在不兼容方法等等
字節碼驗證:會試圖通過對字節碼流的分析,判斷字節碼是否可以正確被執行,比如是否會跳轉到一條不存在的指令,函數調用是否傳遞了正確的參數等等,但是卻無法100%判斷一段字節碼是否可以被安全執行,只是盡可能檢查出可以預知的明顯問題。如果無法通過檢查,則不會加載這個類,如果通過了檢查,也不能說明這個類完全沒有問題
符號引用驗證:檢查類或方法是否確實存在,并且確定當前類有沒有權限訪問這些數據,比如無法找到一個類就拋出NoClassDefFoundError
,無法找到方法就拋出NoSuchMethodError
類通過驗證后,就會進入準備階段,在這個階段,JVM
為會類分配相應的內存空間,并設置初始值,比如:
int
初始化為0
long
初始化為0L
double
初始化為0f
引用初始化為null
如果存在常量字段,那么這個階段也會為常量賦值。
解析就是將類、接口、字段和方法的符號引用轉為直接引用。符號引用就是一些字面量引用,和JVM
的內存數據結構和內存布局無關,由于在字節碼文件中,通過常量池進行了大量的符號引用,這個階段就是將這些引用轉為直接引用,得到類、字段、方法在內存中的指針或直接偏移量。
另外,由于字符串有著很重要的作用,JVM
對String
進行了特別的處理,直接使用字符串常量時,就會在類中出現CONSTANT_String
,并且會引用一個CONSTANT_UTF8
常量項。JVM
運行時,內部的常量池中會維護一張字符串拘留表(intern
),會保存其中出現過的所有字符串常量,并且沒有重復項。使用String.intern()
可以獲得一個字符串在拘留表的引用,比如下面代碼:
public static void main(String[] args){ String a = 1 + String.valueOf(2) + 3; String b = "123"; System.out.println(a.equals(b)); System.out.println(a == b); System.out.println(a.intern() == b); }
輸出:
true
false
true
這里b
就是常量本身,因此a.intern()
返回在拘留表的引用后就是b
本身,比較結果為真。
初始化階段會執行類的初始化方法<clint>
,<clint>
是由編譯期生成的,由靜態成員的賦值語句以及static
語句共同產生。
另外,加載一個類的時候,JVM
總是會試圖加載該類的父類,因此父類的<clint>
方法總是在子類的<clint>
方法之前被調用。另一方面,需要注意的是<clint>
會確保在多線程環境下的安全性,也就是多個線程同時初始化同一個類時,只有一個線程可以進入<clint>
方法,換句話說,在多線程下可能會出現死鎖,比如下面代碼:
package com.company; import java.util.concurrent.TimeUnit; public class Main extends Thread{ private char flag; public Main(char flag){ this.flag = flag; } public static void main(String[] args){ Main a = new Main('A'); a.start(); Main b = new Main('B'); b.start(); } @Override public void run() { try{ Class.forName("com.company.Static"+flag); }catch (ClassNotFoundException e){ e.printStackTrace(); } } } class StaticA{ static { try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } try{ Class.forName("com.company.StaticB"); }catch (ClassNotFoundException e){ e.printStackTrace(); } System.out.println("StaticA init ok"); } } class StaticB{ static { try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } try{ Class.forName("com.company.StaticA"); }catch (ClassNotFoundException e){ e.printStackTrace(); } System.out.println("StaticB init ok"); } }
在加載StaticA
的時候嘗試加載StaticB
,但是由于StaticB
已經被加載中,因此加載StaticA
的線程會阻塞在Class.forName("com.company.StaticB")
處,同理加載StaticB
的線程會阻塞在Class.forName("com.company.StaticA")
處,這樣就出現死鎖了。
ClassLoader
是類加載的核心組件,所有的Class
都是由ClassLoader
加載的,ClassLoader
通過各種各樣的方式將Class
信息的二進制數據流讀入系統,然后交給JVM
進行連接、初始化等操作。因此ClassLoader
負責類的加載流程,無法通過ClassLoader
改變類的連接和初始化行為。
ClassLoader
是一個抽象類,提供了一些重要接口定義加載流程和加載方式,主要方法如下:
public Class<?> loadClass(String name) throws ClassNotFoundException
:給定一個類名,加載一個類,返回這個類的Class
實例,找不到拋出異常
protected final Class<?> defineClass(byte[] b, int off, int len)
:根據給定字節流定義一個類,off
和len
表示在字節數組中的偏移和長度,這是一個protected
方法,在自定義子類中才能使用
protected Class<?> findClass(String name) throws ClassNotFoundException
:查找一個類,會在loadClass
中被調用,用于自定義查找類的邏輯
protected Class<?> findLoadedClass(String name)
:尋找一個已經加載的類
在標準的Java
程序中,JVM
會創建3類加載器為整個應用程序服務,分別是:
啟動類加載器:Bootstrap ClassLoader
擴展類加載器:Extension ClassLoader
應用類加載器(也叫系統類加載器):App ClassLoader
另外,在程序中還可以定義自己的類加載器,從總體看,層次結構如下:
一般來說各個加載器負責的范圍如下:
啟動類加載器:負責加載系統的核心類,比如rt.jar
包中的類
擴展類加載器:負責加載lib/ext/*.jar
下的類
應用類加載器:負責加載用戶程序的類
自定義加載器:加載一些特殊途徑的類,一般是用戶程序的類
2.3 雙親委派
默認情況下,類加載使用雙親委派加載的模式,具體來說,就是類在加載的時候,會判斷當前類是否已經被加載,如果已經被加載,那么直接返回已加載的類,如果沒有,會先請求雙親加載,雙親也是按照一樣的流程先判斷是否已加載,如果沒有在此委托雙親加載,如果雙親加載失敗,則會自己加載。
在上圖中,應用類加載器的雙親為擴展類加載器,擴展類加載器的雙親為啟動類加載器,當系統需要加載一個類的時候,會先從底層類加載器開始進行判斷,當需要加載的時候會從頂層開始加載,依次向下嘗試直到加載成功。
在所有加載器中,啟動類加載器是最特別的,并不是使用Java
語言實現,在Java
中沒有對象與之相對應,系統核心類就是由啟動類加載器進行加載的。換句話說,如果嘗試在程序中獲取啟動類加載器,得到的值是null
:
System.out.println(String.class.getClassLoader() == null);
輸出結果為真。
以上是“Java虛擬機之類加載的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。