Java:類加載

Java 8

IDE Eclipse

---

 

目錄

一、概述

二、開始試驗

try1:獲取各種類加載器

try2:Class.forName加載類

try3:Application ClassLoader加載類

try4:自定義類加載器&加載類

try5:自定義類加載器&熱部署

參考文檔

 

一、概述

類加載:使用 類加載器ClassLoader 將字節碼加載到內存,創建Class對象。

 

ClassLoader一般是由系統提供的,在Java 8中,有以下3個類加載器:

  • 啓動類加載器(Bootstrap ClassLoader)

C++實現,加載Java基礎類,主要是<JRE_HOME>/lib/rt.jar中的。

  • 擴展類加載器(Extension ClassLoader)

static class sun.misc.Launcher$ExtClassLoader extends java.net.URLClassLoader,

加載一些擴展類,主要是<JRE_HOME>/lib/ext目錄中的jar包。

  • 應用程序類加載器(Application ClassLoader)

static class sun.misc.Launcher$AppClassLoader extends java.net.URLClassLoader,

加載自己寫的 和 引入的第三方類庫,即所有類路徑中指定的類。

程序運行時,會創建一個 Application ClassLoader,如無特別說明,一般都是用它加載類,也因此被稱爲 系統類加載器(System ClassLoader),可以使用 ClassLoader.getSystemClassLoader() 獲取。

---

三個類加載器存在一定的關係:父子委派關係。這個設計的目的是實現 雙親委派模型,即優先讓父ClassLoader去加載,這樣可以避免 Java類庫被覆蓋的問題。

說明,

1、在eclipse中,三個類加載器的源碼沒看到,或許要去官網下載源碼纔行;

2、上面的JRE_HOME,本來是 JAVA_HOME的,但在我電腦安裝的 jdk1.8.0_202 中沒有找到,但在 JRE_HOME 中存在;

3、擴展和應用程序類加載器都 繼承了 java.net.URLClassLoader,有源碼,其下也有很多子類;但它繼承了 SecureClassLoader,其上還有ClassLoader抽象類;

4、本文針對Java 8的類加載做介紹,對於Java 9+的類加載器系統,另一種 體系,尚未研究。

5、雙親委派模型 雖然是 一般模型,但也有一些其它例外:1)自定義加載順序、2)網狀加載順序、3)父加載器委派給子加載器加載。

除了系統提供的類加載器,還可以 創建自定義類加載器,通過繼承ClassLoader抽象類即可。

通過自定義類加載器,可以實現一些強大的功能,比如:

1、熱部署

2、應用的模塊化和相互隔離

3、從不同地方靈活加載

 

加載類的幾種方式:

1、Class.forName靜態方法

兩個靜態方法:

public static Class<?> forName(String className) throws ClassNotFoundException;

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException;

其中,前者是後者的簡單版本,底層都是調用 forName0 函數,而前者調用時,initialize設置爲 true——執行類初始化(包括執行 static代碼塊)。

2、使用程序的Application ClassLoader對象的 實例方法 loadClass

public Class<?> loadClass(String name) throws ClassNotFoundException;

3、使用自定義ClassLoader的 實例方法 loadClass

先創建自定義ClassLoader,再生成其對象,再調用loadClass方法。

---

 

更多知識點:Class類、Java運行時數據區域

 

二、開始試驗

試驗中使用了 lombok:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>

 

try1:獲取各種類加載器

public class LoadMain {

	private static Consumer<Object> cs = System.out::println;
	
	public static void main(String[] args) {
		ClassLoader scl = ClassLoader.getSystemClassLoader();
		cs.accept("scl=" + scl);
		
		ClassLoader cl = LoadMain.class.getClassLoader();
		cs.accept("cl =" + cl);
		
		// 返回true:是同一個對象
		cs.accept("scl == cl? = " + (scl == cl));
		
		// 擴展類加載器
		ClassLoader p1 = cl.getParent();
		cs.accept("p1=" + p1);
		
		// 啓動類加載器,值爲null——因爲使用C++實現
		ClassLoader p2 = p1.getParent();
		cs.accept("p2=" + p2);
    }
}

 

測試結果:

scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null

 

啓動時,可以使用 java命令的 -verbose:class 參數 來查看(監視)JVM加載了哪些類。

在上面的程序添加後,可以看到下面的內容:

執行結果(部分):可以看到,其中加載了 LoadMain類

...省略...
[Loaded fanshe.load.LoadMain$$Lambda$1/1418481495 from fanshe.load.LoadMain]
[Loaded java.lang.invoke.LambdaForm$MH/303563356 from java.lang.invoke.LambdaForm]
scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null
....程序結束...
[Loaded java.lang.Shutdown from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]

 

try2:Class.forName加載類

添加類LoadedOne 用於動態加載:

LoadedOne.java
package fanshe.load;

import lombok.Data;

@Data
public class LoadedOne {

	private String name = "1726";
	
	private final float pi = 3.14f;
	
	private static Integer count;
	
	private static final int MAX = 100;
	
	static {
		System.out.println("set count");
		count = 99999;
		System.out.println("count=" + count);
	}
	
}

 

繼續改造LoadMain:啓動後,休眠20秒,然後再調用 Class.forName價值 LoadedOne類。

public class LoadMain {

	private static Consumer<Object> cs = System.out::println;
    
	private static Class<?> loadedOneCls;
	private static String clsPath = "fanshe.load.LoadedOne";
	
	public static void main(String[] args) {
    
    ...
    	try {
			cs.accept("sleep...20秒後 加載 LoadedOne類...now=" + new Date());
			TimeUnit.SECONDS.sleep(20);
		} catch (Exception e) {
			e.printStackTrace();
		}
        
        try {
			// 方式1:會執行類初始化
			Class<?> loadedOneCls = Class.forName(clsPath);
			// 方式2:不會執行類初始化
//			loadedOneCls = Class.forName(clsPath, false, cl);
			cs.accept("loadedOneCls=" + loadedOneCls);
			cs.accept("loadedOneCls=" + loadedOneCls.getSimpleName());
			cs.accept("loadedOneCls=" + loadedOneCls.getName());
			cs.accept("loadedOneCls=" + loadedOneCls.getCanonicalName());
			cs.accept("loadedOneCls=" + loadedOneCls.getTypeName());
			
			try {
				Object nobj = loadedOneCls.newInstance();
				cs.accept("new obj=" + nobj);
			} catch (InstantiationException | IllegalAccessException e) {
				e.printStackTrace();
			}
		} catch (ClassNotFoundException e1) {
			e1.printStackTrace();
		}
		
		if (true) {
			cs.accept("....程序結束...now=" + new Date());
			return;
		}
	}
}

 

繼續使用 -verbose:class,可以監控 休眠後加載的過程。

執行結果(部分):

[Loaded fanshe.load.LoadMain$$Lambda$1/1418481495 from fanshe.load.LoadMain]
[Loaded java.lang.invoke.LambdaForm$MH/303563356 from java.lang.invoke.LambdaForm]
scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null
[Loaded java.util.Date from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
......
sleep...20秒後 加載 LoadedOne類...now=Sun Oct 24 17:37:19 CST 2021
......
set count
count=99999
loadedOneCls=class fanshe.load.LoadedOne
loadedOneCls=LoadedOne
loadedOneCls=fanshe.load.LoadedOne
loadedOneCls=fanshe.load.LoadedOne
loadedOneCls=fanshe.load.LoadedOne
......
[Loaded sun.misc.FDBigInteger from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
new obj=LoadedOne(name=1726, pi=3.14)
....程序結束...now=Sun Oct 24 17:37:39 CST 2021
[Loaded java.lang.Shutdown from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Program Files\Java\jdk1.8.0_202\jre\lib\rt.jar]

 

上面代碼有 2個 Class.forName函數, 試驗使用的是第一個——需要執行類初始化,在加載時就會執行靜態代碼塊。

選擇第2個時,則會再 創建對象 時纔會執行 靜態代碼塊。

 

注意,在Eclipse中,LoadedOne類的class文件 已經自動編譯到了classes 類路徑中了,否則,請使用javac編譯。

 

try3:Application ClassLoader加載類

loadClass方法:底層調用了另一個 protected的 loadClass方法,多一個resolve參數。

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

示例程序:

	private static String clsPath = "fanshe.load.LoadedOne";
    
    /**
	 * 使用系統類加載器加載 LoadedOne
	 * @author ben
	 * @date 2021-10-24 17:54:38 CST
	 */
	public static void loadBySystemCl() {
		ClassLoader scl = ClassLoader.getSystemClassLoader();
		try {
			Class<?> newcls = scl.loadClass(clsPath);
			cs.accept("newcls=" + newcls);
			cs.accept("newcls#1=" + newcls.getSimpleName());
			cs.accept("newcls#2=" + newcls.getName());
			cs.accept("newcls#3=" + newcls.getCanonicalName());
			cs.accept("newcls#4=" + newcls.getTypeName());
			
			try {
				Object obj = newcls.newInstance();
				cs.accept("obj=" + obj);
			} catch (InstantiationException | IllegalAccessException e) {
				e.printStackTrace();
			}
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

 

執行結果:注意,這種加載方式,沒有執行類初始化——static塊沒有在加載時執行,而是在創建類對象前執行的。

scl=sun.misc.Launcher$AppClassLoader@73d16e93
cl =sun.misc.Launcher$AppClassLoader@73d16e93
scl == cl? = true
p1=sun.misc.Launcher$ExtClassLoader@816f27d
p2=null
sleep...20秒後 加載 LoadedOne類...now=Sun Oct 24 18:01:16 CST 2021
newcls=class fanshe.load.LoadedOne
newcls#1=LoadedOne
newcls#2=fanshe.load.LoadedOne
newcls#3=fanshe.load.LoadedOne
newcls#4=fanshe.load.LoadedOne
set count
count=99999
obj=LoadedOne(name=1726, pi=3.14)
....程序結束...now=Sun Oct 24 18:01:21 CST 2021

 

try4:自定義類加載器&加載類

繼承ClassLoader類,實現findClass函數就可以了——實現從 不同來源 獲取class文件並執行加載。

不同來源包括:文件系統、數據庫系統、Web服務器等。

ClassLoader類的findClass函數:直接拋出了異常!

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

 

接下來實現 自定義類加載器,並D盤下的LoadedOne類(默認package)。來自博客園

LoadedOne.java
public class LoadedOne {

	private String name = "D:\\class";
	
	private final float pi = 3.14f;
	
	private static Integer count;
	
	private static final int MAX = 1000;
	
	static {
		System.out.println("set count");
		count = 99999;
		System.out.println("count=" + count);
	}
	
	public String toString() {
		return name + ", " + pi;
	}
}

 

MyLoader.java:可以加載 D盤下 任何 默認package下的類

package fanshe.load;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyLoader extends ClassLoader {

	// 加載D盤下的類文件LoadedOne.class
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
        
        String filename = "d:/" + name + ".class";
        
        File file = new File(filename);
        System.out.println("執行類加載:\nfile.length=" + file.length() + ", lastModified=" + file.lastModified());
        byte[] fb = new byte[(int) file.length()];
        
        try (FileInputStream fis = new FileInputStream(file);) {
        	fis.read(fb);
        	System.out.println("fb:[0-9]");
        	System.out.printf("0x%02x 0x%02x 0x%02x 0x%02x 0x%02x\n", fb[0], fb[1], fb[2], fb[3], fb[4]);
        	System.out.printf("0x%02x 0x%02x 0x%02x 0x%02x 0x%02x\n", fb[5], fb[6], fb[7], fb[8], fb[9]);
        	
        	return defineClass(name, fb, 0, fb.length);
        } catch (IOException e) {
        	throw new ClassNotFoundException(name);
        }
    }
	
}

 

使用MyLoader:

// LoadMain.java 中建立下面的方法
	/**
	 * 加載類測試
	 * @author ben
	 * @date 2021-10-24 19:58:50 CST
	 */
	private static void myLoader1() {
		MyLoader ml = new MyLoader();
		cs.accept("MyLoader ml=" + ml);
		cs.accept("MyLoader ml=" + ml.getParent());

		// 確保 D:\\LoadedOne.class 文件存在
		final String cls = "LoadedOne";
		try {
			Class<?> newcls = ml.loadClass(cls);
			cs.accept("newcls=" + newcls);
			cs.accept("newcls#1=" + newcls.getSimpleName());
			cs.accept("newcls#2=" + newcls.getName());
			cs.accept("newcls#3=" + newcls.getCanonicalName());
			cs.accept("newcls#4=" + newcls.getTypeName());
			
			Object obj;
			try {
				// 新建對象
				obj = newcls.newInstance();
				cs.accept("new obj=" + obj);
			} catch (InstantiationException | IllegalAccessException e) {
				cs.accept("newInstance()異常:" + e);
			}
		} catch (ClassNotFoundException e) {
			cs.accept("加載失敗:" + cls);
			e.printStackTrace();
			return;
		}
	}

 

執行結果:加載成功。但在加載時沒有執行類初始化。可以看到,自定義類加載器的父類是 系統類加載器。

MyLoader ml=fanshe.load.MyLoader@65ab7765
MyLoader ml=sun.misc.Launcher$AppClassLoader@73d16e93
file.length=1061, lastModified=1634743643344
fb:[0-4]
0xca 0xfe 0xba 0xbe 0x00
0x00 0x00 0x34 0x00 0x4b
newcls=class LoadedOne
newcls#1=LoadedOne
newcls#2=LoadedOne
newcls#3=LoadedOne
newcls#4=LoadedOne
set count
count=99999
new obj=D:\class, 3.14
....程序結束...now=Sun Oct 24 20:03:25 CST 2021

 

另外,輸出了class文件的前4個字節——cafebabe!

 

開啓 -verbose:class 檢查加載的類,顯示如下:和之前 使用Class.forName 加載的不同

[Loaded LoadedOne from __JVM_DefineClass__]
dynamicLoadClass2: loadedOneCls=class LoadedOne
set count
count=99999

 

實現加載類時執行初始化:

重寫兩個參數的 loadClass(String name, boolean resolve)失敗了,TODO

 

try5:自定義類加載器&熱部署

熱部署就是,在不重啓應用(JVM)的情況下,把 被加載類 改了,然後,程序檢測到更新,再次執行 類加載,使用新的類。

 

踩坑:同一個類加載器對象執行熱部署,失敗

同一個ClassLoader,類只會被加載一次,加載後,即使class文件已經變了,再次加載得到的還是原來的Class對象。來自博客園

示例程序:

	// LoadMain.java 文件中		// 自定義類加載器
	public static void main(String[] args) {
		MyLoader myl = new MyLoader();
		while (true) {
			try {
				cs.accept("1秒後執行類加載...now=" + new Date());
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			try {
				dynamicLoadClass2(myl);
				cs.accept("加載完畢,修改類文件,並編譯新的class文件...");
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
		}
		
	}

    private static void dynamicLoadClass2(MyLoader cl) throws ClassNotFoundException {
		// 入參 無法實現 熱部署——重新加載類文件
		// 同一個類加載器
		
		// 新建類加載器 纔可以 動態加載
		// 怎麼使用ClassLoader卸載 已加載的類呢?
//		cl = new MyLoader();
		
		loadedOneCls = cl.loadClass("LoadedOne");
		cs.accept("dynamicLoadClass2: loadedOneCls=" + loadedOneCls);
		try {
			Object nobj = loadedOneCls.newInstance();
			cs.accept("2 new obj=" + nobj);
		} catch (InstantiationException | IllegalAccessException e) {
			e.printStackTrace();
		}
	}

 

執行結果:使用同一個類加載器,不能實現熱部署

更改上面的示例程序,每次加載使用新的ClassLoader對象來自博客園

// 打開上面的這句註釋
cl = new MyLoader();

 

執行結果:動態加載成功。

 

上面實現了熱部署的功能,但是,存在下面的問題:

1、會創建很多ClassLoader對象;

2、每次創建ClassLoader對象去加載類,但是,類不一定變化了,需要判斷——最後修改時間等;

3、類加載器可以卸載已加載的類嗎?

 

使用jvisualvm.exe查看內存中加載的類和類加載器

除了 jvisualvm.exe,jconsole.exe 命令也可以看到一些信息:來自博客園

當然,還有 jmap命令,可以輸出 dump文件 進行更進一步分析。來自博客園

 

參考文檔

1、書《Java編程的邏輯》 by 馬昌俊

2、Java內存區域(運行時數據區域)和內存模型(JMM)

3、【JVM】查看JVM加載的類及類加載器的方法

4、一篇文章喫透:爲什麼加載數據庫驅動要用Class.forName()

5、

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章