大綱
- 前言
- 類加載機制
- 類加載器
- 雙親委派機制
- 為什么要使用雙親委派機制?
- 分析ClassLoader
- loadClass()
- findClass()
- defineClass(String name, byte[] b, int off, int len)
- resolveClass(Class<?> c)
- 自定義類加載器
- 通過繼承URLClassLoader來實作自定義類加載器
- URLClassLoader
- findClass()
- Launcher類
- getExtClassLoader()
- createExtClassLoader()
前言
在我上一篇 類檔案結構(java虛擬機系列:一文明解 .class 檔案)博客中詳細介紹了類檔案(.class)如何為java語言的跨平臺性發揮作用和它的內部結構,而類檔案需要加載到虛擬機中,才能被運行,這篇文章將會詳細解說虛擬機的類加載機制
類加載機制
所謂的類加載,就是把.class的二進制檔案(不一定是檔案,也可以通過網路二進制位元組流)加載到記憶體中,形成一個可以直接使用的java型別(Class物件),虛擬機的類加載程序通常包括以下七個階段

-
加載:在加載階段,JVM通過一個類的全限定名稱來獲取二進制位元組流,最后在記憶體里生成一個代表該類的Class物件,加載階段是程式員最能掌控的一個階段,因為并沒有限定要通過何種途徑來獲取二進制流,所以我們可以通過自定義的類加載器,通過網路位元組流傳輸等多種途徑去獲取二進制流,
-
驗證: 加載與連接是交叉進行的,比如某些驗證位元組碼檔案格式的操作,驗證階段主要是確保Class檔案里面的資訊符合規范,不會影響到虛擬機自身的安全
-
準備: 這個階段主要為類變數(靜態變數)分配記憶體并初始化賦值,注意,這些類變數使用的記憶體在方法取,而方法區只是一個邏輯上的說法,在jdk7的方法區表現為永久代,而在jdk8使用了元空間的概念,所以類變數是隨著Class物件放在堆里面,
public static int a = 199; //在準備階段,賦給a的初始值是0而不是199 //只有當存放在<clinit>()的putstatic指令被執行后才會被賦值為199,而<clinit>類構造方法被執行是在初始化階段才被執行 public static final int b = 199; //而類變數b是由final修飾的,所以它的值199被存放于ConstantValue屬性(被其對應的欄位表參考)中,而ConstantValue會指向它對應的常量池之中的字面量,所以在準備階段就直接對b賦值 -
決議:決議階段就是把符號參考轉換為直接參考,符號就是用任意的字面量來描述參考的目標,而直接參考是可以直接指向目標的指標、相對偏移量或者是能間接定位到目標的句柄,直接參考直接對應著虛擬機的真實記憶體布局,《java虛擬機規范》中只說明在執行 checkcast、getfield、getstatic、instanceof、invokedynamic方法等17個用于運算子號參考的位元組碼之前,先對符號參考進行決議,所以虛擬機既可以在類加載階段就對常量池里面的符號參考進行決議,也可以等到一個符號參考將要被使用之前就對其決議,
-
**初始化:**初始化階段就是執行類構造器()的程序,而()是javac編譯是自動生成的,它搜集了類中對類變數賦值的動作和static{}靜態代碼塊的陳述句,如果一個普通的類沒有靜態變數賦值動作也沒有靜態代碼塊,()是不存在的,java虛擬機會保證在子類的()方法執行前先執行父類的()方法,而無需顯式呼叫,
類加載器
? 類加載器的任務是把二進制的位元組流轉換為記憶體中的Class物件,每個Class物件都包含著對它的類加載器的參考,對于任意一個類,都必須由它的類和加載它的類加載器共同確立在java虛擬機中的唯一性,
? 在Java虛擬機的角度來說,類加載器分為兩種,一種是虛擬機自身的啟動類加載器,使用C++撰寫的,而另一種稱為其它類加載器,使用java語言撰寫,都繼承了ClassLoader類,
- Bootstrap Class Loader(引導類加載器),是虛擬機自身的加載器,這個加載器負責加載存放在<JAVA_HOME>\lib目錄或者被-Xbootclasspath引數所指定的路徑存放jar,而且必須是符合的類別庫(如 rt.jar、tools.jar)
- Extension Class Loader(擴展類加載器),由java語言撰寫,它負責加載<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類別庫
- Application Class Loader(應用程式類加載器),它負責加載用戶路(ClassPath)上所有的類別庫
雙親委派機制
? 雙親委派機制的其實就是當子類加載器收到了對加載類的請求的時候,它本身先不去加載,而是把加載的請求傳遞給它的父類加載器,因此所有的加載請求都會傳遞到最頂層的啟動類加載器,然后父類加載器會嘗試去加載該類,如果加載不了則再由子類去加載,

為什么要使用雙親委派機制?
有一個顯而易見的好處就是java中的類和它的類加載器一起形成了具備優先級層次的關系,比如遵循雙親委派機制,可以確保在任意的加載器的環境中Object都是同一個類,因為它最終是由引導類加載器去加載的,而如果不遵循的話,用戶可以自定義一個 java.lang.Object類在classpath下加載,會造成Object類不唯一,造成應用程式的混亂,
分析ClassLoader
loadClass()
我們可以通過以下這種方式去使一個類進行加載,并獲取到它的Class物件
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> aClass = systemClassLoader.loadClass("com.test.JVM.JVMTest");
}
然后我們再來看看loadClass()方法,因為系統類加載器(也就是應用程式類加載器)繼承了ClassLoader,所以也繼承了ClassLoader的loadClass()方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//下面這段簡短的代碼遵循了雙親委派機制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//首先確保這個類是否被加載過,一個類只會被加載一次
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//委托給父類的loadClass()方法
c = parent.loadClass(name, false);
} else {
//如果parent為null,則證明它的父類加載器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//如果c還是null,則證明父類沒有去加載,所以當前的類加載器就去加載
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) {
resolveClass(c);
}
return c;
}
}
findClass()
我們在loadClass()方法看到,如果父類加載不了,那么當前的類加載器就呼叫該findClass()方法去加載,官方建議自定義的類加載器都去重寫這一方法而不是loadClass(),因為loadClass()方法是用來實作雙親委派機制的,而如果重寫它可能會破壞雙親委派機制,
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass(String name, byte[] b, int off, int len)
這個方法通常用在findClass()方法里,用于傳入對應類的全限定名稱加上一個位元組陣列和它的起始位置和長度,就能得到一個Class物件,如果直接呼叫此方法,得到的Class物件是未經過決議的,因為它沒有呼叫resolveClass(),要具體等到一個符號被參考之前才去決議它,
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
resolveClass(Class<?> c)
類加載器可能(也只是可能,因為上面有說到決議階段只要保證符號參考被使用前執行就行,可能在類加載階段就決議,也可能到用的時候才決議)會使用此方法來連接特定的類(決議階段,把符號參考變成虛擬機記憶體中的直接參考),
自定義類加載器
當我們想獲取D盤或者網路中的.class檔案并把它們加載到記憶體中或者需要Class的解密加密器時,可以自定義類加載器來完成,通常,我們需要繼承ClassLoader并重寫findClass()方法即可,如下圖
public class MyClassLoader extends ClassLoader{
private File classPathFile;
public MyClassLoader(){
//為了方便,這里就直接用了classpath的路徑,也可以換成其它盤的路徑
String path = MyClassLoader.class.getResource("").getPath();
this.classPathFile = new File(path);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String className = MyClassLoader.class.getPackage().getName()+"."+ name;
if (classPathFile != null){
File classFile = new File(classPathFile, name.replaceAll("\\.", "/") + ".class");
if (classFile.exists()){
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(classFile);
out = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int len;
while ((len = in.read(buff)) != -1){
out.write(buff,0,len);
}
//通過defineClass()加載出了Class物件
return defineClass(className,out.toByteArray(),0,out.size());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
return null;
}
}
? 自定義的類加載器的父加載器是AppClassLoader(如下圖),也就是應用程式(系統類)加載器,所以我們自定義的類加載器如果像上面一樣沒有重寫load()方法的話也是會遵循雙親委派機制的,
System.out.println(new MyClassLoader().getParent());
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Lv0sukXR-1635834649825)(java虛擬機系列:深入理解Java類加載機制.assets/image-20211102095820220.png)]
通過繼承URLClassLoader來實作自定義類加載器
如果通過去繼承ClassLoader來創造自定義的類加載器的話,需要重寫findClass()方法中去定位資源的代碼或者獲取位元組流的代碼,較為煩雜,所以如果不是有什么復雜的需求,可以通過繼承URLClassLoader來降低編碼量,
下面自定義了一個SelfClassLoader然后繼承了URLClassLoader
public class SelfClassLoader extends URLClassLoader {
public SelfClassLoader(URL[] urls) {
super(urls);
}
}
通過下面的方法加載D盤下的類
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//指定了在哪個檔案下
File file = new File("D://");
SelfClassLoader selfClassLoader = new SelfClassLoader(new URL[]{file.toURI().toURL()});
//這里需要指定類的全限定名稱
Class<?> aClass = selfClassLoader.loadClass("com.test.Test");
Object o = aClass.getConstructor().newInstance();
System.out.println(o);
}

URLClassLoader
先看看它的繼承關系

首先我們看看自定義的類加載器繼承了URLClassLoader后要怎么用
//像這樣,我們繼承了URLClassLoader并且寫了三個構造方法,并且分別執行了父類的三個構造方法
public class SelfClassLoader extends URLClassLoader {
public SelfClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public SelfClassLoader(URL[] urls) {
super(urls);
}
public SelfClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
}
然后我們羅列以下URLClassLoader的三個構造方法
public URLClassLoader(URL[] urls, ClassLoader parent)
public URLClassLoader(URL[] urls)
public URLClassLoader(URL[] urls, ClassLoader parent,URLStreamHandlerFactory factory)
- urls:指明了要加載的class檔案的路徑,因為是一個陣列,所以可以包含多個路徑
- parent:指定了該類加載器的父類加載器,AppClassLoader也是通過這個去指定父類加載器的,稍后會說
- factory:用來指定URLClassPath,這個URLClassPath是用來定位資源的
findClass()
因為URLClassLoader繼承了ClassLoader,而它沒有重寫ClassLoader的loadClass()方法,所以還是遵循雙親委派機制,所以我們重點看一下findClass()方法,因為它是用來加載出Class物件的方法,如下圖
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//把全限定名稱轉為路徑
String path = name.replace('.', '/').concat(".class");
//通過URLClassPath定位到資源
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//通過多載的defineClass方法來獲得Class實體
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
至于多載的defineClass原始碼就不去分析了,如果大家有興趣可以去看看
Launcher類
Launcher屬于oracle的閉源代碼,所以只能通過idea反編譯出來,存在了奇怪的變數名,Launcher里封裝了擴展類加載器和應用程式加載器,所以Launcher實作對以上兩個加載器的加載,下面我們來簡單了解以下它,首先來看看Launcher的構造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//獲取了擴展類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//獲取了應用程式加載器,注意這里把擴展類加載器的參考傳入了
//可以推測這里是把AppClassLoader的父類加載器設定為ExtClassLoader
//所以子父類加載器不是通過繼承來決定的,而是通過聚合的方式
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//設定執行緒背景關系加載器為AppClassLoader
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
我們再來看看被封裝的AppClassLoader
static class AppClassLoader extends URLClassLoader {
......
}
我們發現它繼承了URLClassLoader,其實ExtClassLoader也繼承了URLClassLoader,如下繼承圖

所以就很好理解了,AppClassLoader是通過URLClassLoader的構造方法傳入parent引數來確定他父類構造器是擴展類加載器的,我們再來看看擴展類或者應用程式類加載器是如何向URLClassLoader注冊它們要加載的包的路徑的,
getExtClassLoader()
我們先從擴展類加載器是如何獲取的開始,如下代碼
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
//使用了單例模式,確保擴展類加載器只能有一個實體
//這里具體使用了雙重檢查加synchronized,確保了只能有一個執行緒對擴展類加載器實體化
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
......省略其它代碼
}
createExtClassLoader()
然后我們再看看真正實體化的方法,如下
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
//得到檔案陣列
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
//在這里呼叫了ExtClassLoader構造方法
//這里傳入的var1引數是后來傳給它父類(URLClassLoader)構造方法的urls引數(檔案路徑)
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
我們再點進getExtDirs()看看它是如何獲取ExtClassLoader要加載類的檔案目錄
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
...省略代碼
}
然后我們發現它是通過System.getProperty()方法獲取的,我們測驗一下該方法
System.out.println(System.getProperty("java.ext.dirs"));
得到結果

這個路徑正是擴展類加載器允許加載的類的所在路徑,然后回到createExtClassLoader(),點進Launcher.ExtClassLoader(var1)方法
public ExtClassLoader(File[] var1) throws IOException {
//呼叫了擴展類加載器父類構造器(URLClassLoader)方法,指定了要加載的Class的路徑
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/345618.html
標籤:其他
上一篇:CentOS安裝python3
下一篇:VMware 與戴爾正式“分手”
