寫本篇的動因只是一段看起來很詭異的程式碼,讓我感覺有必要認識一下ClassLoader
----[Counter.java]-------------------------
public class Counter {
private static Counter sCounter = new Counter();//<---- tag1
public static int count = 10;//<---- tag2
private Counter() {
count++;
}
public static Counter getInstance() {
return sCounter;
}
}
----[Client.java]-------------------------
public class Client {
public static void main(String[] args) {
Counter counter = Counter.getInstance();
System.out.println(counter.count);//10
}
}
|-- 當tag1和tag2換一下位置,得到的是11
複製程式碼
一、Java類載入流程
1.Java虛擬機器結構
上一篇講了Java虛擬機器,關於類載入器一筆帶過,本篇詳細講一下
java檔案通過javac可以編譯成.class檔案,類載入器就是將.calss載入到記憶體裡
2.類載入的流程
關於Class例項在堆中還是方法區中?這裡找了一篇文章,講得挺深
2.1:載入
將位元組碼(二進位制流)載入方法區
堆記憶體中生成java.lang.Class物件,作為方法區中該類各種資料的操作入口
|-- .class檔案主要來源--------------------
-– 磁碟中直接載入
-– 網路載入.class檔案
-– 從zip ,jar 等檔案中載入.class 檔案
-– 從專有資料庫中提取.class檔案
-– 將Java原始檔動態編譯為.class檔案
複製程式碼
2.2:連線 - 驗證
驗證載入進來的位元組流資訊是否符合虛擬機器規範
[1].檔案格式驗證: 位元組流是否符合class檔案格式規範
[2].後設資料驗證: 是否符合java的語言語法的規範
[3].位元組碼驗證:方法體進行校驗分析,保證執行時沒危害出現
[4].符號引用驗證 :常量池中的各種符號引用資訊進行匹配性校驗
複製程式碼
2.3:連線 - 準備
為類靜態變數分配記憶體並設定為[對應型別的初始值]
----[Counter.java]-------------------------
public class Counter {
private static Counter sCounter = new Counter();
public static int count = 1;
private Counter() {
count++;
}
public static Counter getInstance() {
return sCounter;
}
}
如上:在準備階段 count 的值為int的預設值 = 0
複製程式碼
2.4:連線 - 解析
常量池內的符號引用替換為直接引用的過程,也就是字面量轉化為指標。
主要解析:類,介面,欄位,類方法,介面方法,方法型別,方法控制程式碼和呼叫點限定符引用
2.5 : 初始化
按順序查詢
靜態變數
以及靜態程式碼塊
對使用者自定義類變數的賦值
,
//現在count=0,呼叫後new Counter()時count++,變為1
private static Counter sCounter = new Counter();
public static int count = 10;// 此時count賦值為10
複製程式碼
二、類被初始化的時機
1.類被初始化的時機程式碼測試
1.建立例項
2.訪問靜態變數或者對該靜態變數賦值
3.呼叫靜態方法
4.反射
5.初始化一個類的子類
6.JVM啟動時被標明為啟動類(main)
---->[Shape類]------------------
public class Shape {
public static String color = "白色";
static {
System.out.println("-----初始化於Shape-----");
}
public static void draw() {
}
}
---->[Shape子類:Rect]------------------
public class Rect extends Shape {
public static int radius = 20;
static {
System.out.println("-----初始化於Rect-----");
}
}
new Shape(); //1.建立例項
String color = Shape.color;//2.訪問靜態變數
Shape.color = "黑色";//2.對該靜態變數賦值
Shape.draw();//3.呼叫靜態方法
Class.forName("classloader.Shape");//4.反射
Rect.radius = 10;//5.初始化一個類的子類
複製程式碼
2.final對初始化的影響
|-- 訪問編譯期靜態常量[不會]觸發初始化
|-- 訪問執行期靜態常量[會]觸發初始化
public class Shape {
...
public static final int weight = 1;
public static final int height = new Random(10).nextInt();
...
}
int w = Shape.weight;//編譯期靜態常量不會觸發初始化
int h = Shape.height;//執行期靜態常量會觸發初始化
|-- 其中height在執行時才可以確定值,訪問會觸發初始化
複製程式碼
3.初始化的其他小點
|-- 類初始化時並不會初始化它的介面
|-- 子介面初始化不會初始化父介面
|-- 宣告類變數時不會初始化
|-- 子類再呼叫父類的靜態方法或屬性時,子類不會被初始化
Shape shape;//宣告類變數,不會初始化
String color = Rect.color;//只初始化Shape
Rect.draw();//只初始化Shape
複製程式碼
三、關於類載入器
1.系統類載入器(應用類載入器)
通過
ClassLoader.getSystemClassLoader()
可以獲取系統類類載入器
debug一下,可以看到系統類載入器:類名為AppClassLoader
,所以也稱應用類載入器
ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println(loader);
Shape shape = new Shape();
////sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader loader = shape.getClass().getClassLoader();
String name = "toly";
ClassLoader loaderSting = name.getClass().getClassLoader();
System.out.println(loaderSting);//null
//可見String的類載入器為null,先說一下,為null時由Bootstrap類載入器載入
|-- 還有一點想強調一下,類載入器載入類後,不會觸發類的初始化
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> shapeClazz = loader.loadClass("classloader.Shape");//此時不初始化
Shape shape = (Shape) shapeClazz.newInstance();//建立例項時才會初始化
複製程式碼
2.父委託機制(或雙親委託機制)
這裡的父並不是指繼承,而是ClassLoader類中有一個parent屬性是ClassLoader型別
所以是認乾爹,而不是親生的。就像Android中的ViewGroup和View的父子View關係
認了乾爹之後,有事先讓乾爹來擺平,乾爹擺不平,再自己來,都擺不平,就崩了唄。
---->[ClassLoader#成員變數]----------------
private final ClassLoader parent;
---->[ClassLoader#建構函式一參]----------------
|-- 可以在一參建構函式中傳入parent,認個乾爹,瞟了一下原始碼,貌似是parent初始化的唯一途徑
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
|--關於父委託機制loadClass方法完美詮釋:
---->[ClassLoader#loadClass]------------------
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
---->[ClassLoader#loadClass(String,boolean)]------------------------------
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) {//有乾爹,讓乾爹來載入
c = parent.loadClass(name, false);
} else {//沒有乾爹,讓大佬Bootstrap類載入器載入
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {//乾爹和大佬都載入不了
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;
}
}
複製程式碼
3.三個JVM中的類載入器
Bootstrap ClassLoader : 引導類載入器(啟動類載入器/根類載入器)
|-- C++語言實現, 負責載入jre/lib路徑下的核心類庫
System.out.println(System.getProperty("sun.boot.class.path"));
//D:\M\JDK1.8\jre\lib\resources.jar;
// D:\M\JDK1.8\jre\lib\rt.jar;
// D:\M\JDK1.8\jre\lib\sunrsasign.jar;
// D:\M\JDK1.8\jre\lib\jsse.jar;
// D:\M\JDK1.8\jre\lib\jce.jar;
// D:\M\JDK1.8\jre\lib\charsets.jar;
// D:\M\JDK1.8\jre\lib\jfr.jar;
// D:\M\JDK1.8\jre\classes
Launcher$ExtClassLoader : 擴充類載入器
|-- Java語言實現,負責載入jre/lib/ext
System.out.println(System.getProperty("java.ext.dirs"));
//D:\M\JDK1.8\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
Launcher$AppClassLoader : 系統類載入器
|-- Java語言實現,載入環境變數路徑classpath或java.class.path 指定路徑下的類庫
String property = System.getProperty("java.class.path");
//D:\M\JDK1.8\jre\lib\charsets.jar;
// D:\M\JDK1.8\jre\lib\deploy.jar;
...略若干jre的jar路徑...
// J:\FileUnit\file_java\base\out\production\classes; <--- 當前專案的輸出路徑
// C:\Program Files\JetBrains\IntelliJ IDEA 2018.1.3\lib\idea_rt.jar
複製程式碼
四、自定義類本地磁碟類載入器
1.自定義類載入器的乾爹
---->[ClassLoader#建構函式]------------------------------------------
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
這裡可以看出無參構造是預設乾爹是:getSystemClassLoader,也就是系統類載入器載入器
當然也可以使用一參構造認乾爹
|-- 上面分析:在ClassLoader#loadClass方法中,當三個JVM的類載入器都找不到時
|-- 會呼叫findClass方法來初始化c ,那我們來看一下findClass:
---->[在ClassLoader#findClass]------------------------
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
就問你一句:人家直接拋異常,你敢不覆寫嗎?
複製程式碼
2.自定義LocalClassLoader
/**
* 作者:張風捷特烈
* 時間:2019/3/7/007:14:05
* 郵箱:1981462002@qq.com
* 說明:本地磁碟類載入器
*/
public class LocalClassLoader extends ClassLoader {
private String path;
public LocalClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) {
byte[] data = getBinaryData(name);
if (data == null) {
return null;
}
return defineClass(name, data, 0, data.length);
}
/**
* 讀取位元組流
*
* @param name 全類名
* @return 位元組碼陣列
*/
private byte[] getBinaryData(String name) {
InputStream is = null;
byte[] result = null;
ByteArrayOutputStream baos = null;
try {
if (name.contains(".")) {
String[] split = name.split("\\.");
name = split[split.length - 1];
}
String path = this.path + "\\" + name + ".class";
File file = new File(path);
if (!file.exists()) {
return null;
}
is = new FileInputStream(file);
baos = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int len = 0;
while ((len = is.read(buff)) != -1) {
baos.write(buff, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (baos != null) {
result = baos.toByteArray();
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
複製程式碼
3.測試類的位元組碼檔案
新建一個類HelloWorld,有一個公共方法say,注意包名和資料夾名
package com.toly1994.classloader;
public class HelloWorld {
public void say() {
System.out.println("HelloWorld");
}
}
複製程式碼
4.使用LocalClassLoader
使用LocalClassLoader載入剛才的位元組碼檔案,通過反射呼叫say方法,執行無誤
這裡要提醒一下:使用javac編譯時的jdk版本,要和工程的jdk版本一致,不然會報錯
LocalClassLoader loader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
try {
Class<?> clazz = loader.loadClass("com.toly1994.classloader.HelloWorld");;
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
Method say = clazz.getMethod("say");
say.invoke(obj);//HelloWorld
} catch (NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
|-- 這裡可以測試一下obj的類載入器
System.out.println(obj.getClass().getClassLoader());
//classloader.LocalClassLoader@6b71769e
複製程式碼
這樣無論.java檔案移到磁碟的哪個位置,都可以的通過指定路徑載入
五、自定義類網路類載入器
將剛才的class檔案放到伺服器上:http://www.toly1994.com:8089/imgs/HelloWorld.class
然後訪問路徑來讀取位元組流,進行類的載入
1.自定義NetClassLoader
核心也就是獲取到流,然後findClass中通過defineClass生成Class物件
/**
* 作者:張風捷特烈
* 時間:2019/3/7/007:14:05
* 郵箱:1981462002@qq.com
* 說明:網路類載入器
*/
public class NetClassLoader extends ClassLoader {
private String urlPath;
public NetClassLoader(String urlPath) {
this.urlPath = urlPath;
}
@Override
protected Class<?> findClass(String name) {
byte[] data = getDataFromNet(urlPath);
if (data == null) {
return null;
}
return defineClass(name, data, 0, data.length);
}
private byte[] getDataFromNet(String urlPath) {
byte[] result = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
URL url = new URL(urlPath);
is = url.openStream();
baos = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int len = 0;
while ((len = is.read(buff)) != -1) {
baos.write(buff, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (baos != null) {
result = baos.toByteArray();
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
複製程式碼
2.使用
使用上基本一致
NetClassLoader loader = new NetClassLoader("http://www.toly1994.com:8089/imgs/HelloWorld.class");
try {
Class<?> clazz = loader.loadClass("com.toly1994.classloader.HelloWorld");
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
Method say = clazz.getMethod("say");
say.invoke(obj);//HelloWorld
} catch (NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
|-- 這裡可以測試一下obj的類載入器
System.out.println(obj.getClass().getClassLoader());
//classloader.NetClassLoader@66d2e7d9
複製程式碼
3.父委派機制測試
現在網路和本地都可以,我們讓本地的loader當做網路載入的父親
---->[NetClassLoader#新增構造]------------------------
public NetClassLoader(ClassLoader parent, String urlPath) {
super(parent);
this.urlPath = urlPath;
}
---->[測試類]-----------------------------
LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
//這裡講NetClassLoader的乾爹設定為localLoader
NetClassLoader netLoader = new NetClassLoader(localLoader, "http://www.toly1994.com:8089/imgs/HelloWorld.class");
try {
Class<?> clazz = netLoader.loadClass("com.toly1994.classloader.HelloWorld");
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
System.out.println(obj.getClass().getClassLoader());
//這裡列印classloader.LocalClassLoader@591f989e
Method say = clazz.getMethod("say");
say.invoke(obj);//HelloWorld
} catch (NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
|-- 可以看到,老爹LocalClassLoader能載入,作為孩子的NetClassLoader就沒載入
|--- 現在將本地的[刪了],老爹LocalClassLoader載入不了,NetClassLoader自己搞
System.out.println(obj.getClass().getClassLoader());
classloader.NetClassLoader@4de8b406
複製程式碼
現在應該很明白父委派機制是怎麼玩的了吧,如果NetClassLoader也載入不了,就崩了
六、class物件的解除安裝
1.一個類被class被能被GC回收(即:解除安裝)的條件
[1].該類所有的例項都已經被GC。
[2].載入該類的ClassLoader例項已經被GC。
[3].該類的java.lang.Class物件沒有在任何地方被引用。
複製程式碼
2.使用自定義載入器時JVM中的引用關係
LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
Class<?> clazz = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
System.out.println(obj.getClass().getClassLoader());
Method say = clazz.getMethod("say");
say.invoke(obj);//HelloWorld
|-- 使用上面的類載入器再載入一次com.toly1994.classloader.HelloWorld可見兩個class物件一致
System.out.println(clazz.hashCode());//1265210847
Class<?> clazz2 = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
System.out.println(clazz2.hashCode());//1265210847
複製程式碼
2.解除安裝
LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
Class<?> clazz = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();
Method say = clazz.getMethod("say");
say.invoke(obj);//HelloWorld
// 清除引用
obj = null; //清除該類的例項
localLoader = null; //清楚該類的ClassLoader引用
clazz = null; //清除該class物件的引用
複製程式碼
後記:捷文規範
參考文章:
深入理解Java類載入器(ClassLoader)
Java --ClassLoader建立、載入class、解除安裝class
關於Class例項在堆中還是方法區中?
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 附錄 |
---|---|---|
V0.1--無 | 2018-3-7 | 無 |
釋出名:
JVM之類載入器ClassLoader
捷文連結:https://juejin.im/post/5c7a9595f265da2db66df32c
2.更多關於我
筆名 | 微信 | |
---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 |
我的github:https://github.com/toly1994328
我的簡書:https://www.jianshu.com/u/e4e52c116681
我的簡書:https://www.jianshu.com/u/e4e52c116681
個人網站:http://www.toly1994.com
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援