Java編譯器把 「.java」 代碼文件編譯成 「.class」 字節碼文件,而後類加載器又負責在須要的時候把字節碼文件的類加載到JVM中,加載過程當中經歷了什麼?類加載器又有哪幾種。不一樣的類加載器又如何確保不會重複加載相同的類。下面將一一解答這些問題java
在聊類加載機制以前,首先須要瞭解一下Java字節碼,由於它和類加載的過程相關。tomcat
Java在誕生的時候的口號:「Write Once, Run AnyWhere", 爲了這個目的,Sun公司發佈了許多能夠在不一樣平臺上運行的JVM——負責載入和執行Java編譯後的字節碼。安全
先來看一下,字節碼長什麼樣子:bash
cafe babe 0000 0034 014c 0a00 3e00 950a
0096 0097 0900 3d00 980b 0099 009a 0900
3d00 9b0a 009c 009d 0a00 9e00 9f0a 00a0
00a1 0b00 9900 a207 00a3 0a00 0a00 a409
003d 00a5 0a00 a600 a70a 00a8 00a9 0b00
9900 aa08 00ab 0b00 ac00 ad07 00ae 0a00
9c00 af08 00b0 0a00 b100 b20a 00b3 00b4
0a00 1200 b50a 0012 00b6 0a00 b100 b70a
00b8 00b9 0a00 3d00 ba0a 00b1 00a9 0a00
b100 bb09 003d 00bc 0a00 1200 bd0a 0012
複製代碼
這段字節碼中的 cafe babe
被稱爲「魔數」,是 JVM 識別 .class 文件的標誌。網絡
JVM 就是負責把這樣的文件載入,而且解釋成計算機能聽懂的語言。app
咱們首先要搞明白一個問題,通常在什麼狀況下會去加載一個類呢?this
其實,答案很是簡單就是代碼中用到這個類的時候。spa
那總得有個頭呀,也就是那一個是一開始就加載的類?這就要說到啓動類的.net
public static void main(String[] args){
Manager manager = new Manager();
}
複製代碼
就是一切的起源,也是Java的規定吧。線程
簡單歸納一下:首先你的代碼中包含「main()」方法的主類必定會在JVM進程啓動以後被加載到內存,開始執行你的「main()」方法中的代碼。
接着遇到你使用了別的類,好比「Manager」,此時就會從對應的「.class」字節碼文件加載對應的類到內存裏來。
一個類從加載到使用,通常會經歷下面的過程:
載入 -> 驗證 -> 準備 -> 解析 -> 初始化 -> 使用 -> 卸載
JVM 在該階段的主要目的是將字節碼從不一樣的數據源(多是 class 文件、也多是 jar 包,甚至網絡)轉化爲二進制字節流加載到內存中,並生成一個表明該類的 java.lang.Class
對象。
JVM 會在該階段對二進制字節流進行校驗,只有符合 JVM 字節碼規範的才能被 JVM 正確執行。該階段是保證 JVM 安全的重要屏障,下面是一些主要的檢查。
cafe bene
開頭)。JVM 會在該階段對類變量(也稱爲靜態變量,static
關鍵字修飾的)分配內存並初始化(對應數據類型的默認初始值,如 0、0L、null、false 等)。
舉個例子:
public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";
複製代碼
chenmo不會被分配內存,而 wanger 會;但 wanger 的初始值不是「王二」而是 null
。
須要注意的是,static final
修飾的變量被稱做爲常量,和類變量不一樣。常量一旦賦值就不會改變了,因此 cmower 在準備階段的值爲「沉默王二」而不是 null
。
該階段將常量池中的符號引用轉化爲直接引用。
首先須要解釋一下符合引用,在編譯時,Java 類並不知道所引用的類的實際地址,所以只能使用符號引用來代替。好比 com.Wanger
類引用了 com.Chenmo
類,編譯時 Wanger 類並不知道 Chenmo 類的實際內存地址,所以只能使用符號 com.Chenmo
。直接引用經過對符號引用進行解析,找到引用的實際內存地址。
更加詳細的解釋能夠看這篇文章
上面提到在準備階段僅僅是給類變量,開闢一個內存空間,而後給個初始值罷了,那麼賦值階段的執行就是在初始化階段,另外靜態代碼塊也會在這個初始化階段被執行。
public class ClassLoaderDemo {
public static int i = 5;
static {
test();
}
public static void test() {
System.out.println("正在執行初始化")
}
}
複製代碼
也就是說,上面的代碼中,i被賦值爲5,和靜態代碼塊將會在這個階段執行。
此外,這裏還有一個很是重要的規則,就是若是初始化一個類的時候,發現他的父類還沒初始化,那麼必須先初始化他的父類。
給定下面代碼:求counter1和counter2的值
public class Classloader {
private static Classloader classloader = new Classloader(); // 1
public static int counter1; // 2
public static int counter2 = 0; // 3
public Classloader() {
counter1++;
counter2++;
}
public static Classloader getClassloader() {
return classloader;
}
}
// 運行主類
public class TestClassloader {
public static void main(String[] args) {
Classloader classloader = Classloader.getClassloader();
System.out.println(classloader.counter1);
System.out.println(classloader.counter2);
}
}
複製代碼
答案是
counter1 == 1
counter2 == 0
複製代碼
下面來一步步分析呀,在準備階段的時候,給靜態變量賦予默認的初始值,也就是
classloader = null;
counter1 = 0;
counter2 = 0;
接下來的初始化階段,初始化靜態變量是從上往下依次執行,因此最早開始執行new CLassloader();而後執行構造方法,因此接下來的 counter1 = 1, counter2 = 1;
繼續執行初始化,counter1沒有在初始化階段進行賦值操做,跳過,counter2 被賦值 0,因此最終的結果是 counter1 = 1, counter2 = 0;
你也能夠試試,將語句1放到語句3後面,輸出結果是counter1 = 1, counter2 = 1;
Java的類加載器有下面這幾種:
主要負責加載Java目錄下的核心類,具體是負責加載<$JAVA_HOME>/ jre/lib/rt.jar 裏全部的class或Xbootclassoath選項指定的jar包。由**C++**實現,不是ClassLoader子類。
負責加載java平臺中擴展功能的一些jar包,包括<$JAVA_HOME>/jre/lib/ext/*.jar 或 -Djava.ext.dirs指定目錄下的jar包。
負責加載classpath中指定的jar包及 Djava.class.path 所指定目錄下的類和jar包。它是java應用程序默認的類加載器。
經過java.lang.ClassLoader的子類自定義加載class,屬於應用程序根據自身須要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader。
在虛擬機啓動的時候會初始化BootstrapClassLoader,而後在Launcher類中去加載ExtClassLoader、AppClassLoader,並將AppClassLoader的parent設置爲ExtClassLoader,並設置線程上下文類加載器。
Launcher是JRE中用於啓動程序入口main()的類,讓咱們看下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 {
//加載應用程序類加載器,並設置parent爲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);
//此處刪除無關代碼。。。
}
複製代碼
看上面的源代碼,不知道你發現沒有,ExtClassLoader沒有設置parent, 主要緣由是由於BootstrapClassLoader是由C++實現的,因此並不存在一個Java的類,因此若是你嘗試執行
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
}
複製代碼
會輸出這樣的結果
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@5a61f5df
null
複製代碼
一個類加載器首先將類加載請求傳送到父類加載器,只有當父類加載器沒法完成類加載請求時才嘗試加載。
首先,虛擬機只有在兩個類的類名相同且加載該類的加載器均相同的狀況下才斷定這是一個類。爲了解決開發者自定義的類名與官方類的可能的加載衝突問題。所以使用了具體優先級的層次加載關係。
例如類java.lang.Object,它存在在rt.jar中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader進行加載,所以Object類在程序的各類類加載器環境中都是同一個類。相反,若是沒有雙親委派模型而是由各個類加載器自行加載的話,若是用戶編寫了一個java.lang.Object的同名類並放在Class-path中,那系統中將會出現多個不一樣的Object類,程序將混亂。所以,若是開發者嘗試編寫一個與rt.jar類庫中重名的Java類,能夠正常編譯,可是永遠沒法被加載運行。
本身實現ClassLoader時只須要繼承ClassLoader類,而後覆蓋findClass(String name)方法便可完成一個帶有雙親委派模型的類加載器。
如下是抽象類 java.lang.ClassLoader 的代碼片斷,其中的 loadClass() 方法運行過程以下:先檢查類是否已經加載過,若是沒有則讓父類加載器去加載。當父類加載器加載失敗時拋出 ClassNotFoundException,此時嘗試本身去加載。
僞代碼:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
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) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
複製代碼
這時候,可能有人會有疑問?爲何不繼承ExtClassLoader或者AppClassLoader呢?
由於AppClassLoader和ExtClassLoader都是Launcher的靜態內部類,都是包訪問路徑權限的。
下面是實現了自定義ClassLoader的代碼
public class MyClassLoader extends ClassLoader {
/** * 重寫父類方法,返回一個Class對象 * ClassLoader中對於這個方法的註釋是: * This method should be overridden by class loader implementations */
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
String classFilename = name + ".class";
File classFile = new File(classFilename);
if (classFile.exists()) {
try (FileChannel fileChannel = new FileInputStream(classFile)
.getChannel();) {
MappedByteBuffer mappedByteBuffer = fileChannel
.map(MapMode.READ_ONLY, 0, fileChannel.size());
byte[] b = mappedByteBuffer.array();
clazz = defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
}
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader();
Class clazz = myClassLoader.loadClass(args[0]);
Method sayHello = clazz.getMethod("sayHello");
sayHello.invoke(null, null);
}
}
複製代碼
本文章參考了知乎用戶請叫我程序猿大人的好怕怕的類加載器文章