面試官:JVM類加載器是否可以加載自定義的String?
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進!你不來,我和你的競爭對手一起精進!
編輯:業(yè)余草
推薦:https://www.xttblog.com/?p=5277
前言
今年面試過很多程序員,當(dāng)我問到類加載機制時,相信大多數(shù)小伙伴都可以答上來雙親委派機制,也都知道 JVM 出于安全性的考慮,全限定類名相同的 String 是不能被加載的。但是如果加載了,會出現(xiàn)什么樣的結(jié)果呢?異常?那是什么樣的異常。如果包名不相同呢?自定義類加載器是否可以加載呢?相信面試官從各種不同的角度出擊,很快就會答出漏洞,畢竟大多數(shù)人整天只是和 CRUD 打交道,并沒有深入研究過虛擬機 ...
接下來筆者就針對上述問題進行一一驗證。該篇文章抱著求證答案的方向出發(fā),并無太多理論方面的詳解。如有理解上的偏差,還望大家不吝賜教。
JVM 都有哪些類加載器
首先我們放上一張節(jié)選自網(wǎng)絡(luò)的 JVM 類加載機制示意圖

JVM 中內(nèi)置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現(xiàn)且全部繼承自 java.lang.ClassLoader:
「BootstrapClassLoader(啟動類加載器)」 :最頂層的加載類,由 C++ 實現(xiàn),負(fù)責(zé)加載 %JAVA_HOME%/lib 目錄下的 jar 包和類或者或被 -Xbootclasspath 參數(shù)指定的路徑中的所有類。 「ExtensionClassLoader(擴展類加載器)」 :主要負(fù)責(zé)加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統(tǒng)變量所指定的路徑下的 jar 包。 「AppClassLoader(應(yīng)用程序類加載器)」 :面向我們用戶的加載器,負(fù)責(zé)加載當(dāng)前應(yīng)用 classpath 下的所有 jar 包和類。
JVM 類加載方式
類加載有三種方式:
命令行啟動應(yīng)用時候由 JVM 初始化加載 通過 Class.forName() 方法動態(tài)加載 通過 ClassLoader.loadClass() 方法動態(tài)加載
「Class.forName()和ClassLoader.loadClass()區(qū)別」
Class.forName():將類的 .class 文件加載到 jvm 中之外,還會對類進行解釋,執(zhí)行類中的 static 塊;ClassLoader.loadClass():只干一件事情,就是將 .class 文件加載到j(luò)vm中,不會執(zhí)行 static 中的內(nèi)容,只有在 newInstance 才會去執(zhí)行 static 塊。Class.forName(name,initialize,loader)帶參函數(shù)也可控制是否加載 static 塊。并且只有調(diào)用了 newInstance() 方法采用調(diào)用構(gòu)造函數(shù),創(chuàng)建類的對象 。
JVM 類加載機制
「全盤負(fù)責(zé)」,當(dāng)一個類加載器負(fù)責(zé)加載某個 Class 時,該 Class 所依賴的和引用的其他 Class 也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個類加載器來載入。 「父類委托」,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。 「緩存機制」,緩存機制將會保證所有加載過的 Class 都會被緩存,當(dāng)程序中需要使用某個 Class 時,類加載器先從緩存區(qū)尋找該 Class,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應(yīng)的二進制數(shù)據(jù),并將其轉(zhuǎn)換成 Class 對象,存入緩存區(qū)。這就是為什么修改了 Class 后,必須重啟 JVM,程序的修改才會生效。

JVM 類加載機制源碼
雙親委派模型實現(xiàn)源碼分析:
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,檢查請求的類是否已經(jīng)被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加載器不為空,調(diào)用父加載器loadClass()方法處理
c = parent.loadClass(name, false);
} else {//父加載器為空,使用啟動類加載器 BootstrapClassLoader 加載
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;
}
}
「雙親委派模型的好處」
雙親委派模型保證了 Java 程序的穩(wěn)定運行,可以避免類的重復(fù)加載(JVM 區(qū)分不同類的方式不僅僅根據(jù)類名,相同的類文件被不同的類加載器加載產(chǎn)生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現(xiàn)一些問題,比如我們編寫一個稱為 java.lang.Object 類的話,那么程序運行的時候,系統(tǒng)就會出現(xiàn)多個不同的 Object 類。
「如果我們不想用雙親委派模型怎么辦?」
為了避免雙親委托機制,我們可以自己定義一個類加載器,然后重寫 loadClass() 即可。
系統(tǒng)類加載器加載自定義 String
「1. 首先我們看下普通的類加載過程」
package com.xttblog.demojava.loadclass;
public class ClassLoaderDemo{
public static void main(String[] args) {
System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}
}
結(jié)果輸出:
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@75bd9247
The GrandParent of ClassLodarDemo's ClassLoader is null
AppClassLoader的父類加載器為ExtClassLoaderExtClassLoader的父類加載器為 null,「null 并不代表ExtClassLoader沒有父類加載器,而是 BootstrapClassLoader」 。
「2. 我們自己定義一個 String 類,看下會發(fā)生什么」
package com.xttblog.demojava.loadclass;
public class String {
public static void main(String[] args) {
System.out.println("我是自定義的String");
}
}
結(jié)果輸出:
? demo-java javac src/main/java/com/xttblog/demojava/loadclass/String.java
? demo-java java src.main.java.com.xttblog.demojava.loadclass.String
錯誤: 找不到或無法加載主類 src.main.java.com.xttblog.demojava.loadclass.String
這里分明有 main 方法,全限定類名又和 jdk 的 String 不在同一個 package (不會造成沖突),為什么會輸出找不到或無法加載主類呢?
細(xì)心的小伙伴一定會發(fā)現(xiàn)該類沒有導(dǎo)入系統(tǒng)的 String 類,會不會因為 JVM 的類加載機制,AppClassLoader 加載類的時候,由于自定義的 String 被加載,攔截了上層的 String 類呢?String 對象是自定義的,不符合 main() 方法的定義方式,故系統(tǒng)拋找不到main() 方法。
我們反過來驗證下剛才的推測,再次運行剛才的 ClassLoaderDemo 會發(fā)生什么呢?what?IDE 中的 main() 方法去哪里了?還是手動編譯運行下吧。
? demo-java javac src/main/java/com/xttblog/demojava/loadclass/ClassLoaderDemo.java
? demo-java java src.main.java.com.xttblog.demojava.loadclass.ClassLoaderDemo
錯誤: 找不到或無法加載主類 src.main.java.com.xttblog.demojava.loadclass.ClassLoaderDemo
結(jié)果顯示: 之前正常運行的 java 類也找不到主類了。
我們導(dǎo)入正確的 String 類再來驗證下。
package com.xttblog.demojava.loadclass;
public class String {
public static void main(java.lang.String[] args) {
System.out.println("我是自定義的String");
}
}
結(jié)果輸出
我是自定義的 String
「3. 能否覆寫 lang 包下的 String 類?」
上邊的案例修改包路徑即可
package java.lang;
public class String {
public static void main(java.lang.String[] args) {
System.out.println("我是自定義的String");
}
}
輸出報錯。
Connected to the target VM, address: '127.0.0.1:63569', transport: 'socket'
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
public static void main(String[] args)
否則 JavaFX 應(yīng)用程序類必須擴展javafx.application.Application
**分析:**首先由于全限定類名 java.lang.String 等于 jdk 中的 String 類,根據(jù)上邊類加載源碼可知,當(dāng) AppClassLoader 加載該 String 時,判斷 java.lang.String 已經(jīng)加載,便不會再次加載。所以執(zhí)行的依舊是 jdk 中的 String,但是系統(tǒng)的 java.lang.String 中沒有 main() 方法,所以會報錯。這是一種安全機制。
然后驗證下默認(rèn)的類加載器能否加載自定義的 java.lang.String。==,默認(rèn)的 AppClassLoader 能加載 Everything?
public class LoadStringDemo {
public static void main(String[] args) {
URLClassLoader systemClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
URL[] urLs = systemClassLoader.getURLs();
for (URL url: urLs) {
System.out.println(url);
}
}
}
輸出日志如下:
...
file:/Users/xttblog/work/demo-java/target/classes/
...
日志太多,但是絕對沒有其他的包路徑(當(dāng)前包下的 java.lang.String 默認(rèn)只能時 jdk 中的)。
自定義類加載器
「為什么會存在自定義類加載器呢」
自定義類加載器的核心在于對字節(jié)碼文件的獲取,如果是加密的字節(jié)碼則需要在該類中對文件進行解密。
因為實際項目中,會有多種加載 .class 文件的方式,
從本地系統(tǒng)中直接加載 通過網(wǎng)絡(luò)下載 .class 文件 從 zip,jar 等歸檔文件中加載 .class 文件 從專有數(shù)據(jù)庫中提取 .class 文件 將 Java 源文件動態(tài)編譯為 .class 文件
「如何自定義類加載器」
package com.xttblog.demojava.loadclass;
import com.xttblog.ClassLoaderDemo;
import java.io.*;
import java.lang.reflect.Method;
public class MyClassLoader extends ClassLoader {
private String root;
/**
* @param name 全限定類名
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("/Users/xttblog/Desktop/demo");
Class<?> clz = Class.forName("LoadDemo", true, classLoader);
Object instance = clz.newInstance();
Method test = clz.getDeclaredMethod("test");
test.setAccessible(true);
test.invoke(instance);
System.out.println(instance.getClass().getClassLoader());
}
}
結(jié)果輸出
test
com.xttblog.demojava.loadclass.MyClassLoader@75bd9247
由此可知,自定義類加載器已可以正常工作。這里我們不能把 LoadDemo 放在類路徑下,由于雙親委托機制的存在,會直接導(dǎo)致該類由 AppClassLoader 加載,而不會通過我們自定義類加載器來加載。
「自定義類加載器加載手寫java.lang.String」
改寫自定義類加載器的 main() 方法。
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("/Users/xttblog/Desktop/demo");
Class<?> clz = classLoader.findClass("java.lang.String");
Object instance = clz.newInstance();
System.out.println(instance.getClass().getClassLoader());
}
JVM 由于安全機制拋出了 SecurityException。
/Users/xttblog/Desktop/demo/java/lang/String.class
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.example.demojava.loadclass.MyClassLoader.findClass(MyClassLoader.java:25)
at com.example.demojava.loadclass.MyClassLoader.main(MyClassLoader.java:71)
以上內(nèi)容,希望能夠?qū)Υ蠹颐嬖囉兴鶐椭O矚g的朋友,幫忙點個贊??!
