類加載器負責在運行時將Java類動態加載到JVM(Java虛擬機)。此外,它們是JRE(Java運行時環境)的一部分。因此,由於類加載器,JVM不需要知道底層文件或文件系統以運行Java程序。
此外,這些Java類不會同時加載到內存中,而是在應用程序需要時。這就是類加載器的用武之地,他們負責將類加載到內存中。
1. 類加載器的層級結構
讓我們首先學習如何使用各種類加載器使用,一個簡單示例加載不同的類:
public class TestMain {
public static void main(String[] args) {
System.out.println("1: " + People.class.getClassLoader());
System.out.println("2: " + Logging.class.getClassLoader());
System.out.println("3: "+new AppletEvent("w",1,"3").getClass().getClassLoader());
}
}
輸出:
1: sun.misc.Launcher$AppClassLoader@135fbaa4
2:sun.misc.Launcher$ExtClassLoader@cc34f4d
3: null
我們可以看到,這裏有三種不同的類加載器:AppClassLoader,ExtClassLoader,還有一個顯示爲空的 BootstrapClassloader。
對於AppletEvent,它在輸出中顯示爲null。**這是因爲引導類加載器是用C++而不是Java編寫的,因此它不會顯示爲Java類。**由於這個原因,引導類加載器的行爲在不同的JVM實現之間會有所不同。
- BootstrapClassloader:它主要負責加載JDK內部類,通常是rt.jar和位於*$ JAVA_HOME / jre / lib* 目錄中的其他核心庫。此外,Bootstrap類加載器充當所有其他ClassLoader實例的父級。
- ExtClassLoader:擴展類加載器從JDK擴展目錄加載,通常是*$ JAVA_HOME / lib / ext目錄或java.ext.dirs*系統屬性中提到的任何其他目錄。
- AppClassLoader:系統或應用程序類加載器負責將所有應用程序級別類加載到JVM中。它加載在類路徑環境變量-classpath或-cp命令行選項中找到的文件。此外,它是Extensions類加載器的下屬級別。
2. 類加載器如何工作?
類加載器是Java運行時環境的一部分。當JVM請求類時,類加載器會嘗試使用 完全限定的類名 來定位類並將類定義加載到運行時。java.lang.ClassLoader.loadClass() 方法是負責將類加載爲運行時的方法,它嘗試基於完全限定名稱加載類。
如果當前類加載器尚未加載該類,它會將請求委託給父類加載器。此過程以遞歸方式發生。
最終,如果父類加載器沒有找到該類,則子類將調用 java.net.URLClassLoader.findClass() 方法來查找文件系統本身中的類。如果最後一個子類加載器也無法加載該類,則會拋出 java.lang.NoClassDefFoundError 或java.lang.ClassNotFoundException。
在ClassLoader類中,來看一下loadClass()方法的實現:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 第一步,檢查這個類是否已經被加載
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果沒有被加載
long t0 = System.nanoTime();
try {
//如果該類的上級加載器存在,那麼用該加載器來加載這個類
//這裏是一個循環的過程,知道上級類加載器不存在爲止
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果這個類沒有上級類加載器,那麼則使用BootstrapClassLoader來加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果仍然爲空,則調用 findClass方法來尋找這個類
//從內存中查找是否有這個類的緩存
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 解析class文件,就是將符號引用替換爲直接引用的過程
resolveClass(c);
}
return c;
}
}
總結上面的加載步驟:
- 首先會檢查當前傳入的類全路徑名是否在緩存中存在,即已經被加載;
- 如果沒有被加載,檢查當前類加載器的上級類加載器是否存在,如果存在,則使用上級類加載器繼續調用
loadClass()
方法繼續該過程;如果不存在,那麼則使用BootstrapClassLoader來加載; - 經過上面的類加載器過程,如果還沒有能夠加載該類,下面會再查詢一下緩存中是否有該類。
- 如果resolve參數爲true,則 尋找是否存在符號引用,如果有就解析class文件,將符號引用替換爲直接引用。
類加載原理:
類加載有3個原則:
-
委託模型
Java虛擬機和Java ClassLoader使用稱爲 委派層次結構算法的算法 將類加載到Java文件中。
ClassLoader基於委託模型給出的一組操作來工作。他們是:
- ClassLoader始終遵循委託層次結構原則。
- 每當JVM遇到一個類時,它會檢查該類是否已經加載;
- 如果已經在方法區域中加載了Class,則JVM繼續執行;
- 如果該方法區域中不存在該類,則JVM要求Java ClassLoader子系統加載該特定類,然後ClassLoader子系統將控件移交給Application ClassLoader;
- Application ClassLoader將請求委託給Extension ClassLoader,Extension ClassLoader依次將請求委託給Bootstrap ClassLoader;
- Bootstrap ClassLoader將在Bootstrap類路徑(JDK / JRE / LIB)中進行搜索。如果該類可用則加載它,否則請求被委託給Extension ClassLoader;
- Extension ClassLoader在Extension Classpath(JDK / JRE / LIB / EXT)中搜索類。如果該類可用則加載它,否則請求被委託給Application ClassLoader;
- Application ClassLoader在Application Classpath中搜索類。如果該類可用則會加載它,否則會生成ClassNotFoundException異常。
委託模型被我們翻譯爲雙親委派模型,這是正好 AppClassLoader 上面有兩個類加載器,如果有三個或者四個,這種叫法就不合適。個人認爲翻譯爲雙親不是太合適,因爲這兩個類加載器並不是平級的關係。
-
可見性原則
可見性原則聲明由父類ClassLoader加載的類對子類ClassLoader可見,但子類ClassLoader加載的類對子類ClassLoader不可見。假設擴展ClassLoader已加載類Person.class,那麼該類僅對Extension ClassLoader和Application ClassLoader可見,但對Bootstrap ClassLoader不可見。如果再次嘗試使用Bootstrap ClassLoader加載該類,則會給出異常 java.lang.ClassNotFoundException。
-
唯一性屬性
Uniquesness屬性確保類是唯一的,並且不會重複類。這也確保子類加載器不加載由父類加載器加載的類。如果父類加載器無法找到該類,則只有當前實例本身才會嘗試這樣做。
在JVM請求類之後,要遵循幾個步驟來加載類。根據委託模型加載類,但有一些重要的方法或函數在加載類時起着至關重要的作用。
- loadClass(String name,boolean resolve):此方法用於加載JVM引用的類,它將類的名稱作爲參數;
- defineClass():該方法是一個final方法,不能被覆蓋。此方法用於將字節數組定義爲類的實例。如果該類無效,則拋出ClassFormatError;
- findClass(String name):此方法用於查找指定的類,此方法僅查找但不加載類;
- findLoadedClass(String name):此方法用於驗證JVM引用的Class是否先前已加載;
- Class.forName(String name,boolean initialize,ClassLoader loader):此方法用於加載類以及初始化類。此方法還提供了選擇任何一個ClassLoader的選項。如果ClassLoader參數爲NULL,則使用Bootstrap ClassLoader。
3. ClassLoader 如何加載出類加載器的層級結構
Java類由java.lang.ClassLoader的實例加載。在Classloader中並沒有看到關於類加載器層級結構的相關代碼,那麼這一部分是怎麼實現的呢?
我們接着看ClassLoader的一個構造方法:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
主要看getSystemClassLoader()
方法:
在Classloader初始化的時候,會初始化系統類加載器。通過sclSet
變量來控制只初始化一次,並且如果在進入initSystemClassLoader
方法時發現sclSet
如果爲true則拋出異常。接着調用了Launcher
類:
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
點擊進入Launcher的構造方法,因爲sun.misc
包中的源碼在src.zip
中默認是沒有的,我是下載了openJDK源碼查看到的。
先看一下Launcher是如何初始化的:
通過getLauncher()
方法獲取 launcher
對象,launcher通過構造方法初始化,並且launcher對象是static類型的,意味着Launcher類是單例的。下面來看Launcher的構造方法:
從上面的初始化過程可以看出:
-
首先會創造擴展類加載器。擴展類加載器會去加載哪些類呢?在
getExtClassLoader() ---> getExtDirs()
方法中告訴了我們:String s = System.getProperty("java.ext.dirs");
獲取的是
java.ext.dirs
目錄下的所有類文件。 -
第二步是創建用於啓動應用程序的類加載器。同樣方式與第一步大同小異,加載的路徑如下:
final String s = System.getProperty("java.class.path");
-
第三步爲當前線程設置上下文類加載器。
-
最後,根據請求安裝安全管理器。
在ExtClassLoader初始化的過程中,將 父類加載器設置成了null,因爲BootstrapClassLoader是C++編寫,對於Java本身來說它是不存在的。所以在此處將ExtClassLoader的父加載器設置爲空,表示他的父類加載器就是啓動類加載器。
而下面的AppClassLoader在初始化設置父類加載器的時候,將 上面獲取到的 ExtClassLoader作爲他的父類加載器,最終的加載器層級狀態就如我們開頭所描述的這樣,構成了一個層級結構。
而到這裏我們也可以解釋從 ClassLoader 到 Launcher, 類的加載器從初始化層級結構到使用的過程。最終從 Launcher中返回來的是AppClassLoader,即應用層級的類加載器。
4. 自定義ClassLoader
正常情況下我們都會按照預設的目錄使用JDK自帶 的類加載器進行加載,比如Spring搭建的工程中會掃描指定包下所有的類,那麼什麼時候會使用到自定義類加載器呢?
- 當我們的類文件不是按照指定目錄存放的時候,即該目錄未配置到可以被AppClassLoader掃描的路徑中;
- 當你從網絡中讀取字節流轉成類文件的時候,如何處理這個類的邏輯也應該是自己實現;
- 實現簡易的熱部署模式,自定義類加載器,定時加載新的類文件。
以上這些場景下都可以用到自定義的類加載器。實現自定義類加載器需要繼承ClassLoader類,重寫其中的關鍵方法。
關鍵方法
findClass(String name)
:實際執行加載二進制流的具體行爲方法,這個方法顧名思義負責查找一個類並返回它。對我們自定義而言,這是我們最需要關注的。loadClass(String name)
:這個方法中主要負責協調加載類,通常它的邏輯比較固定,我們可以不去重寫。這是類加載器執行加載類邏輯的方法,包括檢查是否已經加載,調用父類加載,失敗則自己嘗試使用 findClass方法加載。defineClass(String name, byte[] b, int off, int len)
:負責定義類,這個方法我們主要調用就好了。
所以我們需要實現的步驟就是:
- 實現findClass方法,從你指定的位置加載類文件;
- 然後調用defineClass將類文件字節流轉爲對應的類對象即可。
實現代碼如下:
package com.rickiyang.learn;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* @author rickiyang
* @date 2019-09-05
* @Desc 加載自定義類文件
* 讀取編譯好的.class文件,使用自定義的classLoader進行加載
*/
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
super(CustomClassLoader.class.getClassLoader());
this.classPath = classPath;
}
@Override
public Class<?> findClass(String name) {
//檢查路徑是否可用
if (classPath == null) {
throw new IllegalArgumentException("Please set class path.");
}
//加載class文件數據
byte[] classData = loadClassData(classPath);
if (classData == null) {
throw new NullPointerException(" Please check class file path.");
}
// 將class的字節數組解碼爲Class實例
return defineClass(name, classData, 0, classData.length);
}
/**
* 讀取Class文件
*/
private byte[] loadClassData(String path) {
byte[] bytes = new byte[1024];
int length = 0;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
File classFile = new File(path);
FileInputStream fis = null;
try {
fis = new FileInputStream(classFile);
while ((length = fis.read(bytes)) != -1) {
byteStream.write(bytes, 0, length);
byteStream.flush();
}
return byteStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fis == null) {
throw new NullPointerException("null");
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public void setClassPath(String classPath) {
this.classPath = classPath;
}
}
/**
* 具體使用方式
*/
class MainClass {
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader("");
try {
classLoader.setClassPath("D:\\workspace\\springboot-learn\\single-database\\target\\classes\\com\\rickiyang\\learn\\TestClass.class");
Class clazz = classLoader.loadClass("com.rickiyang.learn.TestClass");
//反射來調用方法
Method method = clazz.getDeclaredMethod("doSomething");
System.out.println(clazz.getSimpleName());
System.out.println("result = " + method.invoke(clazz.newInstance()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
class TestClass {
public int doSomething() {
return 1;
}
}
上面這個案例的正確使用方式是你需要提前將TestClass.java 編譯成.class文件,然後才能使用。
熱部署實現
既然可以手動的通過使用自定義類加載器的方式來加載類對象,那麼我們是否可以想到如何實現一個簡單的熱加載呢?原理其實很簡單:通過上面的類加載器方法定時的掃描指定文件夾的所有.class文件即可。
定義類加載器:
package com.rickiyang.learn.utils;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Set;
/**
* @author rickiyang
* @date 2019-09-06
* @Desc 自建一個類加載器, 可以用來加載自己的類
*/
public class SelfClassLoder extends ClassLoader {
//用於讀取.Class文件的路徑
private String swapPath;
//用於標記這些name的類是先由自身加載的
private Set<String> useMyClassLoaderLoad;
public SelfClassLoder(String swapPath, Set<String> useMyClassLoaderLoad) {
this.swapPath = swapPath;
this.useMyClassLoaderLoad = useMyClassLoaderLoad;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null && useMyClassLoaderLoad.contains(name)) {
//特殊的類讓我自己加載
c = findClass(name);
if (c != null) {
return c;
}
}
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) {
//根據文件系統路徑加載class文件,並返回byte數組
byte[] classBytes = getClassByte(name);
//調用ClassLoader提供的方法,將二進制數組轉換成Class類的實例
return defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] getClassByte(String name) {
String className = name.substring(name.lastIndexOf('.') + 1) + ".class";
try {
FileInputStream fileInputStream = new FileInputStream(swapPath + className);
byte[] buffer = new byte[1024];
int length = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((length = fileInputStream.read(buffer)) > 0) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[]{};
}
}
創建一個定時器程序,2s同步使用自定義類加載器加載指定類:
package com.rickiyang.learn;
import com.google.common.collect.Sets;
import com.rickiyang.learn.utils.SelfClassLoder;
import java.util.Timer;
import java.util.TimerTask;
/**
* @author rickiyang
* @date 2019-09-06
* @Desc 測試自己的類加載器
* 注意這裏的SelfClassLoder必須要和Test類在同一包下,不然找不到Test類的路徑
* <p>
* 測試方式:
* 啓動定時任務首先會打印:當前版本是2哦
* 然後你手動將 2 改爲1 編譯一下Test類,或者提前編譯好,替換掉Test.class,
* 就可以看到自動加載了這個新的類文件了
* 這種方式可以用於我們熱更新某些類文件
*/
public class SelfClassLoderTest {
public static void main(String[] args) {
//創建一個2s執行一次的定時任務
new Timer().schedule(new TimerTask() {
@Override
public void run() {
String swapPath = SelfClassLoder.class.getResource("").getPath();
String className = "com.rickiyang.learn.Test";
//每次都實例化一個ClassLoader,這裏傳入swap路徑,和需要特殊加載的類名
SelfClassLoder myClassLoader = new SelfClassLoder(swapPath, Sets.newHashSet(className));
try {
//使用自定義的ClassLoader加載類,並調用printVersion方法。
Object o = myClassLoader.loadClass(className).newInstance();
o.getClass().getMethod("printVersion").invoke(o);
} catch (Exception e) {
e.printStackTrace();
}
}
}, 0, 2000);
}
}
5. 類加載的階段
類從被加載到內存中開始,直到被從內存中卸載爲止,它的整個生命週期包括:驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱爲連接(Linking),這7個階段的發生順序如下圖所示:
加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。
實際上,什麼時候開始執行類加載的第一個階段:加載,這點Java虛擬機並沒與強制約束,但是對於初始化階段,虛擬機規範則是嚴格規定了 有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始)。這裏的5種情況,通常被稱爲類的主動引用。除此之外,所有類引用的方式都不會觸發類的初始化,稱爲被動引用。
主動引用的5種類型
- 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。
- 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
下面詳細的看一下類加載的幾個過程:
1. 加載
這裏的加載只是類加載中的一個過程,更好理解的說法應該叫做裝載。如果要加載一個類型,虛擬機必須要完成3件事情。
- 通過一個類的全限定名獲取定義此類的二進制字節流。
- 解析這個二進制流爲方法區的運行時數據結構。
- 創建一個該類的
java.lang.Class
對象,作爲方法區這個類的各種數據的訪問入口。
由於虛擬機並沒有指明二進制字節流要從一個Class文件中獲取,準確的說沒有規定從哪裏獲取怎樣獲取,以下都是可能獲取的方式之一:
- 從本地文件系統加載一個Java class 文件。
- 通過網絡下載一個Java class文件。
- 從ZIP、JAR、EAR、WAR格式的壓縮文件中獲取。
- 運行時生成,JDK提供的動態代理技術,在
java.lang.reflect.Proxy
中,就是用了ProxyGenerator.generateProxyClass
來爲特定接口生成形式爲*$Proxy
的代理類的二進制字節流。 - 由數據庫中讀取或者其它文件生成,如JSP等等。
有了這些二進制的字節流文件後,就可以使用類加載器進行加載了。在加載階段我們既可以使用系統提供的引導類加載器來完成,也可以由用戶自定義的類加載器去完成,可以通過定義自己的類加載器去控制字節流的獲取方式(即重寫一個類加載器的loadClass()方法)。Java有關類加載器的相關知識點在本文中不做過多敘述,後續另起一篇博文再做介紹。
加載階段完成之後,JVM就會把二進制字節流就按照自己所需的格式存儲在方法區之中。然後在內存中實例化一個java.lang.Class類的對象(並沒有明確規定是在Java堆中,對於HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,但是存放在方法區裏面)。
加載階段與連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬於連接階段的內容,這兩個階段的開始時間仍然保持着固定的先後順序。如Java類加載器在加載一個二進制文件時會執行一個checkName()的方法,用於校驗文件是否爲空或者是否是有效的二進制名稱。
2. 驗證
驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。一般驗證階段會完成4個階段的驗證工作:
- 文件格式驗證;
- 元數據驗證;
- 字節碼驗證;
- 符號引用驗證。
文件格式驗證主要是驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。如是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型等等。該驗證階段的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內,格式上符合描述一個Java類型信息的要求。這階段的驗證是基於二進制字節流進行的,只有通過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,所以後面的3個驗證階段全部是基於方法區的存儲結構進行的,不會再直接操作字節流。
元數據驗證是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。如這個類是否有父類、是否繼承了不允許被繼承的類,類中的字段、方法是否與父類產生矛盾等等。該階段主要目的是對類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息。
字節碼驗證是整個驗證過程中最複雜的一個階段,主要目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型做完校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件。如保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,例如不會出現類似這樣的情況:在操作棧放置了一個int類型的數據,使用時卻按long類型來加載入本地變量表中。
符號引用驗證主要是在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。符號引用驗證的目的是確保解析動作能正常執行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
3. 準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在堆中。其次,這裏所說的初始值“通常情況”下是數據類型的零值,假設一個類變量的定義爲:
public static int v = 1;
那變量v在準備階段過後的初始值爲0而不是1。因爲這時候尚未開始執行任何java方法,而把v賦值爲1的putstatic指令是程序被編譯後,存放於類構造器()方法之中,所以把v賦值爲1的動作將在初始化階段纔會執行。
至於“特殊情況”是指:
public static final int v=1;
即當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,所以標註爲final之後,value的值在準備階段初始化爲1而非0。
4. 解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
- 符號引用(Symbol References): 符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須一致,因爲符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中。
- 直接引用(Direct References): 直接引用可以是直接目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局有關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在內存中存在。
我們通過一個例子來實際看一下在解析的過程中各個部分的代碼塊是採用什麼樣的先後順序來加載的,
我使用的開發工具是IDEA,在工具欄:run-edit configutations-vm options 菜單中配置如下參數:
-XX:+TraceClassLoading
,這樣在代碼調用時會跟蹤類的加載情況。
案例:
一個父類,一個子類,一個並行的類。測試子類和父類的加載順序,靜態代碼塊和非靜態代碼塊的加載時機。
package com.rickiyang.learn;
/**
* @author rickiyang
* @date 2019-09-10
* @Desc TODO
*/
public class TestClassLoadSequence extends BaseClass {
{
System.out.println("這裏是子類的普通代碼塊");
}
public TestClassLoadSequence() {
System.out.println("這裏是子類的構造方法");
}
@Override
public void print() {
System.out.println("這裏是子類的普通方法");
}
public static void print1() {
System.out.println("這裏是子類的靜態方法");
}
static {
System.out.println("這裏是子類的靜態代碼塊");
}
public static void main(String[] args) {
BaseClass bcb = new TestClassLoadSequence();
bcb.print();
}
Test2 o = new Test2();
}
class BaseClass {
public BaseClass() {
System.out.println("這裏是父類的構造方法");
}
public void print() {
System.out.println("這裏是父類的普通方法");
}
public static void prin1t() {
System.out.println("這裏是父類的靜態方法");
}
static {
System.out.println("這裏是父類的靜態代碼塊");
}
Other2 o2 = new Other2();
{
System.out.println("這裏是父類的普通代碼塊");
}
}
class Test2 {
Test2() {
System.out.println("初始化子類的屬性值");
}
}
class Other2 {
Other2() {
System.out.println("初始化父類的屬性值");
}
}
這個例子比較簡單,在運行代碼之前分析一下:帶有static關鍵字的代碼塊應該是最先執行,其次是非static關鍵字的代碼塊以及類的屬性(Fields),最後是構造方法。帶上父子類的關係後,上面的運行結果爲:
這裏是父類的靜態代碼塊
這裏是子類的靜態代碼塊
初始化父類的屬性值
這裏是父類的普通代碼塊
這裏是父類的構造方法
這裏是子類的普通代碼塊
初始化子類的屬性值
這裏是子類的構造方法
這裏是子類的普通方法
Process finished with exit code 0
注意的是類的屬性與非靜態代碼塊的執行級別是一樣的,誰先執行取決於書寫的先後順序。
結論:父類的靜態代碼塊->子類的靜態代碼塊->初始化父類的屬性值/父類的普通代碼塊(自上而下的順序排列)->父類的構造方法->初始化子類的屬性值/子類的普通代碼塊(自上而下的順序排列)->子類的構造方法。
注:構造函數最後執行。