-
1 # java架構設計
-
2 # JavaKeeper
從幾道面試題詳細回答下這個問題
直擊面試看你簡歷寫得熟悉JVM,那你說說類的載入過程吧?
我們可以自定義一個String類來使用嗎?
什麼是類載入器,類載入器有哪些?這些類載入器都載入哪些檔案?
多執行緒的情況下,類的載入為什麼不會出現重複載入的情況?
什麼是雙親委派機制?它有啥優勢?可以打破這種機制嗎?
類載入子系統類載入機制概念Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的載入機制。Class檔案由類裝載器裝載後,在JVM中將形成一份描述Class結構的元資訊物件,透過該元資訊物件可以獲知Class的結構資訊:如建構函式,屬性和方法等,Java允許使用者藉由這個Class相關的元資訊物件間接呼叫Class物件的功能,這裡就是我們經常能見到的Class類。
類載入子系統作用類載入子系統負責從檔案系統或者網路中載入class檔案,class檔案在檔案開頭有特定的檔案標識(0xCAFEBABE)
ClassLoader只負責class檔案的載入。至於它是否可以執行,則由Execution Engine決定
載入的類資訊存放於一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還存放執行時常量池資訊,可能還包括字串字面量和數字常量(這部分常量資訊是class檔案中常量池部分的記憶體對映)
Class物件是存放在堆區的
類載入器ClassLoader角色class file存在於本地硬碟上,可以理解為設計師畫在紙上的模板,而最終這個模板在執行的時候是要載入到JVM當中來根據這個檔案例項化出n個一模一樣的例項
class file載入到JVM中,被稱為DNA元資料模板,放在方法區
在.calss檔案 -> JVM -> 最終成為元資料模板,此過程就要一個運輸工具(類裝載器),扮演一個快遞員的角色
類載入過程類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝七個階段。(驗證、準備和解析又統稱為連線,為了支援Java語言的執行時繫結,所以解析階段也可以是在初始化之後進行的。以上順序都只是說開始的順序,實際過程中是交叉的混合式進行的,載入過程中可能就已經開始驗證了)
1. 載入(Loading):
透過一個類的全限定名獲取定義此類的二進位制位元組流
將這個位元組流所代表的的靜態儲存結構轉化為方法區的執行時資料結構
在記憶體中生成一個代表這個類的 物件,作為方法區這個類的各種資料的訪問入口
載入 檔案的方式
從本地系統中直接載入
透過網路獲取,典型場景:Web Applet
從zip壓縮檔案中讀取,成為日後jar、war格式的基礎
執行時計算生成,使用最多的是:動態代理技術
由其他檔案生成,比如 JSP 應用
從專有資料庫提取.class 檔案,比較少見
從加密檔案中獲取,典型的防 Class 檔案被反編譯的保護措施
2. 連線(Linking)驗證(Verify)
目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,保證被載入類的正確性,不會危害虛擬機器自身安全
主要包括四種驗證,檔案格式驗證,元資料驗證,位元組碼驗證,符號引用驗證
準備(Prepare)
為類變數分配記憶體並且設定該類變數的預設初始值,即零值
資料型別零值int0long0Lshort(short)0char"\u0000"byte(byte)0booleanfalsefloat0.0fdouble0.0dreferencenull這裡不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯示初始化
這裡不會為例項變數分配初始化,類變數會分配在方法區中,而例項變數是會隨著物件一起分配到Java堆中
private static int i = 1; //變數i在準備階只會被賦值為0,初始化時才會被賦值為1private final static int j = 2; //這裡被final修飾的變數j,直接成為常量,編譯時就會被分配為2解析(Resolve)
將常量池內的符號引用轉換為直接引用的過程
事實上,解析操作往往會伴隨著JVM在執行完初始化之後再執行
符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《Java虛擬機器規範》的Class檔案格式中。直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制代碼
解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別等。對應常量池中的、、等
3. 初始化(Initialization)
初始化階段就是執行類構造器方法<clinit>()的過程
此方法不需要定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來
構造器方法中指令按語句在原始檔中出現的順序執行
<clinit>()不同於類的構造器(構造器是虛擬機器視角下的<init>())
若該類具有父類,JVM會保證子類的<clinit>()執行前,父類的<clinit>()已經執行完畢
虛擬機器必須保證一個類的<clinit>()方法在多執行緒下被同步加鎖
public class ClassInitTest{private static int num1 = 30;
static{
num1 = 10;
num2 = 10; //num2寫在定義變數之前,為什麼不會報錯呢??
System.out.println(num2); //這裡直接列印可以嗎? 報錯,非法的前向引用,可以賦值,但不可呼叫
}
private static int num2 = 20; //num2在準備階段就被設定了預設初始值0,初始化階段又將10改為20
public static void main(String[] args){
System.out.println(num1); //10
System.out.println(num2); //20
}
}
類的主動使用和被動使用Java程式對類的使用方式分為:主動使用和被動使用。虛擬機器規範規定有且只有5種情況必須立即對類進行“初始化”,即類的主動使用。
建立類的例項、訪問某個類或介面的靜態變數,或者對該靜態變數賦值、呼叫類的靜態方法(即遇到new、getstatic、putstatic、invokestatic這四條位元組碼指令時)
反射
初始化一個類的子類
Java虛擬機器啟動時被標明為啟動類的類
JDK7 開始提供的動態語言支援:例項的解析結果,、、控制代碼對應的類沒有初始化,則初始化
除以上五種情況,其他使用Java類的方式被看作是對類的被動使用,都不會導致類的初始化。
eg:
public class NotInitialization {public static void main(String[] args) {
//只輸出SupperClass int 123,不會輸出SubClass init
//對於靜態欄位,只有直接定義這個欄位的類才會被初始化
System.out.println(SubClass.value);
}
}
class SuperClass {
static {
System.out.println("SupperClass init");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}
類載入器JVM支援兩種型別的類載入器,分別為引導類載入器(Bootstrap ClassLoader)和自定義類載入器(User-Defined ClassLoader)
從概念上來講,自定義類載入器一般指的是程式中由開發人員自定義的一類類載入器,但是Java虛擬機器規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader的類載入器都劃分為自定義類載入器
啟動類載入器(引導類載入器,Bootstrap ClassLoader)這個類載入使用C/C++ 語言實現,巢狀在JVM 內部
它用來載入Java的核心庫(、或路徑下的內容),用於提供JVM自身需要的類
並不繼承自 ,沒有父載入器
載入擴充套件類和應用程式類載入器,並指定為他們的父類載入器
出於安全考慮,Boostrap 啟動類載入器只加載名為java、Javax、sun等開頭的類
擴充套件類載入器(Extension ClassLoader)
java語言編寫,由實現
派生於 ClassLoader
父類載入器為啟動類載入器
從系統屬性所指定的目錄中載入類庫,或從JDK的安裝目錄的 子目錄(擴充套件目錄)下載入類庫。如果使用者建立的JAR 放在此目錄下,也會自動由擴充套件類載入器載入
應用程式類載入器(也叫系統類載入器,AppClassLoader)java語言編寫,由 實現
派生於 ClassLoader
父類載入器為擴充套件類載入器
它負責載入環境變數或系統屬性 指定路徑下的類庫
該類載入是程式中預設的類載入器,一般來說,Java應用的類都是由它來完成載入的
透過 方法可以獲取到該類載入器
public class ClassLoaderTest {public static void main(String[] args) {
//獲取系統類載入器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@135fbaa4
//獲取其上層:擴充套件類載入器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@2503dbd3
//再獲取其上層:獲取不到引導類載入器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); //null
//對於使用者自定義類來說,預設使用系統類載入器進行載入,輸出和systemClassLoader一樣
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@135fbaa4
//String 類使用引導類載入器進行載入。Java的核心類庫都使用引導類載入器進行載入,所以也獲取不到
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); //null
//獲取BootstrapClassLoader可以載入的api的路徑
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
}
}
使用者自定義類載入器在Java的日常應用程式開發中,類的載入幾乎是由3種類載入器相互配合執行的,在必要時,我們還可以自定義類載入器,來定製類的載入方式
為什麼要自定義類載入器?
隔離載入類
修改類載入的方式
防止原始碼洩露(Java程式碼容易被反編譯,如果加密後,自定義載入器載入類的時候就可以先解密,再載入)
使用者自定義載入器實現步驟
開發人員可以透過繼承抽象類 類的方式,實現自己的類載入器,以滿足一些特殊的需求
在JDK1.2之前,在自定義類載入器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類載入類,但是JDK1.2之後已經不建議使用者去覆蓋loadClass()方式,而是建議把自定義的類載入邏輯寫在findClass()方法中
編寫自定義類載入器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取位元組碼流的方式,使自定義類載入器編寫更加簡潔
ClassLoader常用方法
ClassLoader類,是一個抽象類,其後所有的類載入器都繼承自ClassLoader(不包括啟動類載入器)
方法描述getParent()返回該類載入器的超類載入器loadClass(String name)載入名稱為name的類,返回java.lang.Class類的例項findClass(String name)查詢名稱為name的類,返回java.lang.Class類的例項findLoadedClass(String name)查詢名稱為name的已經被載入過的類,返回java.lang.Class類的例項defineClass(String name, byte[] b, int off, int len)把位元組陣列b中內容轉換為一個Java類,返回java.lang.Class類的例項resolveClass(Class<?> c)連線指定的一個Java類對類載入器的引用
JVM必須知道一個型別是由啟動載入器載入的還是由使用者類載入器載入的。如果一個型別是由使用者類載入器載入的,那麼JVM會將這個類載入器的一個引用作為型別資訊的一部分儲存在方法區中。當解析一個型別到另一個型別的引用的時候,JVM需要保證這兩個型別的類載入器是相同的。
雙親委派機制Java虛擬機器對class檔案採用的是按需載入的方式,也就是說當需要使用該類的時候才會將它的class檔案載入到記憶體生成class物件。而且載入某個類的class檔案時,Java虛擬機器採用的是雙親委派模式,即把請求交給父類處理,它是一種任務委派模式。
工作過程
如果一個類載入器收到了類載入請求,它並不會自己先去載入,而是把這個請求委託給父類的載入器去執行;
如果父類載入器還存在其父類載入器,則進一步向上委託,依次遞迴,請求最終將到達頂層的啟動類載入器;
如果父類載入器可以完成類載入任務,就成功返回,倘若父類載入器無法完成此載入任務,子載入器才會嘗試自己去載入,這就是雙親委派模式
優勢
避免類的重複載入,JVM中區分不同類,不僅僅是根據類名,相同的class檔案被不同的ClassLoader載入就屬於兩個不同的類(比如,Java中的Object類,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,如果不採用雙親委派模型,由各個類載入器自己去載入的話,系統中會存在多種不同的Object類)
保護程式安全,防止核心API被隨意篡改,避免使用者自己編寫的類動態替換 Java的一些核心類,比如我們自定義類:java.lang.String
在JVM中表示兩個class物件是否為同一個類存在兩個必要條件:
類的完成類名必須一致,包括包名
載入這個類的ClassLoader(指ClassLoader例項物件)必須相同
沙箱安全機制如果我們自定義String類,但是在載入自定義String類的時候會率先使用引導類載入器載入,而引導類載入器在載入的過程中會先載入jdk自帶的檔案(rt.jar包中java\lang\String.class),報錯資訊說沒有main方法就是因為載入的是包中的String類。這樣就可以保證對java核心原始碼的保護,這就是簡單的沙箱安全機制。
破壞雙親委派模型雙親委派模型並不是一個強制性的約束模型,而是Java設計者推薦給開發者的類載入器實現方式,可以“被破壞”,只要我們自定義類載入器,重寫loadClass()方法,指定新的載入邏輯就破壞了,重寫findClass()方法不會破壞雙親委派。
雙親委派模型有一個問題:頂層ClassLoader,無法載入底層ClassLoader的類。典型例子JNDI、JDBC,所以加入了執行緒上下文類載入器(Thread Context ClassLoader),可以透過設定該類載入器,然後頂層ClassLoader再使用獲得底層的ClassLoader進行載入。
Tomcat中使用了自定ClassLoader,並且也破壞了雙親委託機制。每個應用使用WebAppClassloader進行單獨載入,他首先使用WebAppClassloader進行類載入,如果載入不了再委託父載入器去載入,這樣可以保證每個應用中的類不衝突。每個tomcat中可以部署多個專案,每個專案中存在很多相同的class檔案(很多相同的jar包),他們載入到jvm中可以做到互不干擾。
利用破壞雙親委派來實現程式碼熱替換(每次修改類檔案,不需要重啟服務)。因為一個Class只能被一個ClassLoader載入一次,否則會報。當我們想要實現程式碼熱部署時,可以每次都new一個自定義的ClassLoader來載入新的Class檔案。JSP的實現動態修改就是使用此特性實現。
回覆列表
我們平時都知道透過javac命令將
.java
檔案編譯成.class
檔案,之後這個class檔案就可以“被執行”了,但是我們需要搞清楚的是這個class檔案在虛擬機器中究竟是怎麼玩的。要想具體搞清楚java類是如何載入的Java虛擬機器中,我們需要搞清楚以下幾個問題:
一個Java類什麼時候開始被載入?
載入Java類的過程是怎樣的?
一個Java類從被載入到JVM記憶體中到這個類被解除安裝,主要包含以下七個步驟的生命週期。
一個Java類什麼時候開始被載入?Java虛擬機器並沒有規定在什麼時候需要載入Java類,但是對於Java類的初始化卻有明確的規定,有且只有以下5中情況時候便會立即觸發類的“初始化”動作:
遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有初始化,則需要先觸發其初始化,程式碼示例如下:
使用
java.lang.reflect
包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化,程式碼示例如下:當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。這個很好理解,就是當一個子類遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果父類還沒有初始化,則先初始化父類。
當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。
當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。這塊屬於動態載入的範疇,本質上還是需要需要new、getstatic、putstatic或invokestatic這4條位元組碼指令。
載入Java類的過程是怎樣的?知道了類被載入的條件後,我們需要知道一個Java類透過怎樣的過程被載入到Java虛擬機器中去了。
這個過程其實就是把一個
.class
檔案中的java類相關資訊載入到記憶體中,透過驗證、準備、解析等階段,最終生成一個存在於Java虛擬機器記憶體中的java.lang.Class
物件。這裡面涉及到的知識點比較多:
如何驗證一個
.class
檔案是符合Java虛擬機器規範的?準備階段都做哪些事情?
類載入機制是怎樣的?
什麼是雙親委派模型?
載入完的類儲存在Java虛擬機器的什麼記憶體區域?
載入的類GC可以回收嗎?如果可以回收需要滿足什麼樣的條件才可以回收一個類?
推薦大家一定要閱讀《深入理解Java虛擬機器》這本Java程式設計師必讀書籍!並且不只是讀一遍,要不停的讀,不停的品!
以上就是我個人對“JAVA虛擬機器中是如何載入JAVA類的?”這個問題的一些解答,這裡只是給大家開個一個頭,需要大家再繼續深耕下去。