1、引言2、類的加載、連接、初始化一、加載1.一、加載的class來源二、類的連接2.一、驗證2.二、準備2.三、解析三、類的初始化3.一、< clinit>方法相關3.二、類初始化時機3.三、final定義的初始化3.四、ClassLoader只會對類進行加載,不會進行初始化3、類加載器一、JVM類加載器分類1.一、Bootstrap ClassLoader1.2 、Extension ClassLoader1.三、 System ClassLoader4、類加載機制1.一、JVM主要的類加載機制。1.二、類加載流程圖5、建立並使用自定義類加載器一、自定義類加載分析二、實現自定義類加載器6、總結java
當程序使用某個類時,若是該類還未被加載到內存中,則JVM會經過加載、連接、初始化三個步驟對該類進行類加載。bootstrap
類加載指的是將類的class文件讀入內存,併爲之建立一個java.lang.Class對象。類的加載過程是由類加載器來完成,類加載器由JVM提供。咱們開發人員也能夠經過繼承ClassLoader來實現本身的類加載器。數組
經過類的加載,內存中已經建立了一個Class對象。連接負責將二進制數據合併到 JRE中。連接須要經過驗證、準備、解析三個階段。緩存
驗證階段用於檢查被加載的類是否有正確的內部結構,並和其餘類協調一致。便是否知足java虛擬機的約束。安全
類準備階段負責爲類的類變量分配內存,並設置默認初始值。網絡
咱們知道,引用其實對應於內存地址。思考這樣一個問題,在編寫代碼時,使用引用,方法時,類知道這些引用方法的內存地址嗎?顯然是不知道的,由於類還未被加載到虛擬機中,你沒法得到這些地址。舉例來講,對於一個方法的調用,編譯器會生成一個包含目標方法所在的類、目標方法名、接收參數類型以及返回值類型的符號引用,來指代要調用的方法。多線程
解析階段的目的,就是將這些符號引用解析爲實際引用。若是符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那麼解析將觸發這個類的加載(但未必會觸發解析與初始化)。app
類的初始化階段,虛擬機主要對類變量進行初始化。虛擬機調用< clinit>方法,進行類變量的初始化。jvm
java類中對類變量進行初始化的兩種方式:ide
public class Test {
static int A = 10;
static {
A = 20;
}
}
class Test1 extends Test {
private static int B = A;
public static void main(String[] args) {
System.out.println(Test1.B);
}
}
//輸出結果
//20
複製代碼
從輸出中看出,父類的靜態初始化塊在子類靜態變量初始化以前初始化完畢,因此輸出結果是20,不是10。
若是類或者父類中都沒有靜態變量及方法,虛擬機不會爲其生成< clinit>方法。
接口與類不一樣的是,執行接口的<clinit>方法不須要先執行父接口的<clinit>方法。 只有當父接口中定義的變量使用時,父接口才會初始化。 另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>方法。
public interface InterfaceInitTest {
long A = CurrentTime.getTime();
}
interface InterfaceInitTest1 extends InterfaceInitTest {
int B = 100;
}
class InterfaceInitTestImpl implements InterfaceInitTest1 {
public static void main(String[] args) {
System.out.println(InterfaceInitTestImpl.B);
System.out.println("---------------------------");
System.out.println("當前時間:"+InterfaceInitTestImpl.A);
}
}
class CurrentTime {
static long getTime() {
System.out.println("加載了InterfaceInitTest接口");
return System.currentTimeMillis();
}
}
//輸出結果
//100
//---------------------------
//加載了InterfaceInitTest接口
//當前時間:1560158880660
複製代碼
從輸出驗證了:對於接口,只有真正使用父接口的類變量纔會真正的加載父接口。這跟普通類加載不同。
public class MultiThreadInitTest {
static int A = 10;
static {
System.out.println(Thread.currentThread()+"init MultiThreadInitTest");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread() + "start");
System.out.println(MultiThreadInitTest.A);
System.out.println(Thread.currentThread() + "run over");
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
//輸出結果
//Thread[main,5,main]init MultiThreadInitTest
//Thread[Thread-0,5,main]start
//10
//Thread[Thread-0,5,main]run over
//Thread[Thread-1,5,main]start
//10
//Thread[Thread-1,5,main]run over
複製代碼
從輸出中看出驗證了:只有第一個線程對MultiThreadInitTest進行了一次初始化,第二個線程一直阻塞等待等第一個線程初始化完畢。
注意:對於一個使用final定義的常量,若是在編譯時就已經肯定了值,在引用時不會觸發初始化,由於在編譯的時候就已經肯定下來,就是「宏變量」。若是在編譯時沒法肯定,在初次使用纔會致使初始化。
public class StaticInnerSingleton {
/**
* 使用靜態內部類實現單例:
* 1:線程安全
* 2:懶加載
* 3:非反序列化安全,即反序列化獲得的對象與序列化時的單例對象不是同一個,違反單例原則
*/
private static class LazyHolder {
private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();
}
private StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
return LazyHolder.INNER_SINGLETON;
}
}
複製代碼
看這個例子,單例模式靜態內部類實現方式。咱們能夠看到單例實例使用final定義,但在編譯時沒法肯定下來,因此在第一次使用StaticInnerSingleton.getInstance()方法時,纔會觸發靜態內部類的加載,也就是延遲加載。這裏想指出,若是final定義的變量在編譯時沒法肯定,則在使用時仍是會進行類的初始化。
public class Tester {
static {
System.out.println("Tester類的靜態初始化塊");
}
}
class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
//下面語句僅僅是加載Tester類
classLoader.loadClass("loader.Tester");
System.out.println("系統加載Tester類");
//下面語句纔會初始化Tester類
Class.forName("loader.Tester");
}
}
//輸出結果
//系統加載Tester類
//Tester類的靜態初始化塊
複製代碼
從輸出證實:ClassLoader只會對類進行加載,不會進行初始化;使用Class.forName()會強制致使類的初始化。
類加載器負責將.class文件(不論是jar,仍是本地磁盤,仍是網絡獲取等等)加載到內存中,併爲之生成對應的java.lang.Class對象。一個類被加載到JVM中,就不會第二次加載了。
那怎麼判斷是同一個類呢?
每一個類在JVM中使用全限定類名(包名+類名)與類加載器聯合爲惟一的ID,因此若是同一個類使用不一樣的類加載器,能夠被加載到虛擬機,但彼此不兼容。
Bootstrap ClassLoader爲根類加載器,負責加載java的核心類庫。根加載器不是ClassLoader的子類,是有C++實現的。
public class BootstrapTest {
public static void main(String[] args) {
//獲取根類加載器所加載的所有URL數組
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
Arrays.stream(urLs).forEach(System.out::println);
}
}
//輸出結果
//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
//file:/C:/SorftwareInstall/java/jdk/jre/classes
複製代碼
根類加載器負責加載%JAVA_HOME%/jre/lib下的jar包(以及由虛擬機參數 -Xbootclasspath 指定的類)。
咱們將rt.jar解壓,能夠看到咱們常用的類庫就在這個jar包中。
Extension ClassLoader爲擴展類加載器,負責加載%JAVA_HOME%/jre/ext或者java.ext.dirs系統熟悉指定的目錄的jar包。你們能夠將本身寫的工具包放到這個目錄下,能夠方便本身使用。
System ClassLoader爲系統(應用)類加載器,負責加載加載來自java命令的-classpath選項、java.class.path系統屬性,或者CLASSPATH環境變量所指定的JAR包和類路徑。程序能夠經過ClassLoader.getSystemClassLoader()來獲取系統類加載器。若是沒有特別指定,則用戶自定義的類加載器默認都以系統類加載器做爲父加載器。
注意:類加載器之間的父子關係並非類繼承上的父子關係,而是實例之間的父子關係。
public class ClassloaderPropTest {
public static void main(String[] args) throws IOException {
//獲取系統類加載器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系統類加載器:" + systemClassLoader);
/*
獲取系統類加載器的加載路徑——一般由CLASSPATH環境變量指定,若是操做系統沒有指定
CLASSPATH環境變量,則默認以當前路徑做爲系統類加載器的加載路徑
*/
Enumeration<URL> eml = systemClassLoader.getResources("");
while (eml.hasMoreElements()) {
System.out.println(eml.nextElement());
}
//獲取系統類加載器的父類加載器,獲得擴展類加載器
ClassLoader extensionLoader = systemClassLoader.getParent();
System.out.println("系統類的父加載器是擴展類加載器:" + extensionLoader);
System.out.println("擴展類加載器的加載路徑:" + System.getProperty("java.ext.dirs"));
System.out.println("擴展類加載器的parant:" + extensionLoader.getParent());
}
}
//輸出結果
//系統類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
//file:/C:/ProjectTest/FengKuang/out/production/FengKuang/
//系統類的父加載器是擴展類加載器:sun.misc.Launcher$ExtClassLoader@1540e19d
//擴展類加載器的加載路徑:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
//擴展類加載器的parant:null
複製代碼
從輸出中驗證了:系統類加載器的父加載器是擴展類加載器。但輸出中擴展類加載器的父加載器是null,這是由於父加載器不是java實現的,是C++實現的,因此獲取不到。但擴展類加載器的父加載器是根加載器。
圖中紅色部分,能夠是咱們自定義實現的類加載器來進行加載。
除了根類加載器,全部類加載器都是ClassLoader的子類。因此咱們能夠經過繼承ClassLoader來實現本身的類加載器。
ClassLoader類有兩個關鍵的方法:
因此,若是要實現自定義類,能夠重寫這兩個方法來實現。但推薦重寫findClass方法,而不是重寫loadClass方法,由於loadClass方法內部回調用findClass方法。
咱們來看一下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 {
//第二步,判斷父加載器是否爲null
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) {
//第三步,若是前面都沒有找到,就會調用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) {
resolveClass(c);
}
return c;
}
}
複製代碼
loadClass加載方法流程:
因此,爲了避免影響類的加載過程,咱們重寫findClass方法便可簡單方便的實現自定義類加載。
基於以上分析,咱們簡單重寫findClass方法進行自定義類加載。
public class Hello {
public void test(String str){
System.out.println(str);
}
}
public class MyClassloader extends ClassLoader {
/**
* 讀取文件內容
*
* @param fileName 文件名
* @return
*/
private byte[] getBytes(String fileName) throws IOException {
File file = new File(fileName);
long len = file.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fin = new FileInputStream(file)) {
//一次性讀取Class文件的所有二進制數據
int read = fin.read(raw);
if (read != len) {
throw new IOException("沒法讀取所有文件");
}
return raw;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//將包路徑的(.)替換爲斜線(/)
String fileStub = name.replace(".", "/");
String classFileName = fileStub + ".class";
File classFile = new File(classFileName);
//若是Class文件存在,系統負責將該文件轉換爲Class對象
if (classFile.exists()) {
try {
//將Class文件的二進制數據讀入數組
byte[] raw = getBytes(classFileName);
//調用ClassLoader的defineClass方法將二進制數據轉換爲Class對象
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException e) {
e.printStackTrace();
}
}
//若是clazz爲null,代表加載失敗,拋出異常
if (null == clazz) {
throw new ClassNotFoundException(name);
}
return clazz;
}
public static void main(String[] args) throws Exception {
String classPath = "loader.Hello";
MyClassloader myClassloader = new MyClassloader();
Class<?> aClass = myClassloader.loadClass(classPath);
Method main = aClass.getMethod("test", String.class);
System.out.println(main);
main.invoke(aClass.newInstance(), "Hello World");
}
}
//輸出結果
//Hello World
複製代碼
ClassLoader還有一個重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的做用是將class的二進制數組轉換爲Calss對象。
此例子很簡單,我寫了一個Hello測試類,而且編譯事後放在了當前路徑下(你們能夠在findClass中加入判斷,若是沒有此文件,能夠嘗試查找.java文件,並進行編譯獲得.class文件;或者判斷.java文件的最後更新時間大於.class文件最後更新時間,再進行從新編譯等邏輯)。
本篇從類加載的三大階段:加載、連接、初始化開始細說每一個階段的過程;詳細講解了JVM經常使用的類加載器的區別與聯繫,以及類加載機制流程,最後經過自定義的類加載器例子結束本篇。小弟能力有限,你們看出有問題請指出,讓博主學習改正。歡迎討論啊。
注意:本篇博客總結主要來源。若有轉載,請註明出處