Java 8
IDE Eclipse
---
目錄
try3:Application ClassLoader加載類
類加載:使用 類加載器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>
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]
添加類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
繼承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
熱部署就是,在不重啓應用(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 馬昌俊
4、一篇文章喫透:爲什麼加載數據庫驅動要用Class.forName()
5、