java類加載器
Java類加載器(英語:Java Classloader)是Java運行時環(huán)境(Java Runtime Environment)的一部分坐漏,負(fù)責(zé)動態(tài)加載Java類到Java虛擬機(jī)的內(nèi)存空間中。類通常是按需加載恶复,即第一次使用該類時才加載亿絮。由于有了類加載器,Java運行時系統(tǒng)不需要知道文件與文件系統(tǒng)颁股。
JVM中的默認(rèn)類加載器
JVM中有3個默認(rèn)的類加載器:
- 引導(dǎo)(Bootstrap)類加載器盹廷。由原生代碼(C++語言)編寫征绸,不繼承自
java.lang.ClassLoader
。負(fù)責(zé)加載JVM自身需要的類俄占,負(fù)責(zé)將<JAVA_HOME>/jre/lib
路徑下的核心類庫或-Xbootclasspath
參數(shù)指定的路徑下的jar包加載到內(nèi)存中管怠。 - 擴(kuò)展(Extensions)類加載器。用來在
<JAVA_HOME>/jre/lib/ext
,或java.ext.dirs
中指明的目錄中加載 Java的擴(kuò)展庫缸榄。Java 虛擬機(jī)的實現(xiàn)會提供一個擴(kuò)展庫目錄渤弛。該類加載器在此目錄里面查找并加載 Java 類。該類由sun.misc.Launcher$ExtClassLoader
實現(xiàn)甚带。
//ExtClassLoader類中獲取路徑的代碼
private static File[] getExtDirs() {
//加載<JAVA_HOME>/lib/ext目錄中的類庫
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for (int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
- Apps類加載器(也稱系統(tǒng)類加載器)暮芭。根據(jù) Java應(yīng)用程序的類路徑(
java.class.path
或CLASSPATH環(huán)境變量)來加載 Java 類。一般來說欲低,Java 應(yīng)用的類都是由它來完成加載的⌒笪可以通過 ClassLoader.getSystemClassLoader()來獲取它砾莱。該類由sun.misc.Launcher$AppClassLoader
實現(xiàn),是程序中默認(rèn)的類加載器凄鼻。
在Java的日常應(yīng)用程序開發(fā)中腊瑟,類的加載幾乎都是由上述3種類加載器相互配合執(zhí)行的,在必要時块蚌,我們還可以自定義類加載器闰非,需要注意的是,Java虛擬機(jī)對class文件采用的是按需加載的方式峭范,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象财松,而且加載某個類的class文件時,Java虛擬機(jī)采用的是雙親委派模式,即把請求交由父類處理辆毡,它是一種任務(wù)委派模式菜秦。
雙親委派模式
雙親委派模式的工作原理
雙親委派模式要求除了頂層的引導(dǎo)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器舶掖,類加載器之間的關(guān)系如下:
雙親委派模式的工作原理是球昨,如果一個類加載器收到了類加載的請求,它并不會自己先去加載眨攘,而是把這個請求委托給父類的加載器去執(zhí)行主慰,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托鲫售,依次遞歸共螺,請求最終到達(dá)頂層的啟動類加載器,如果父類加載器可以完成類加載任務(wù)龟虎,就成功返回璃谨,倘若父類加載器無法完成加載任務(wù),子加載器才嘗試去自己加載鲤妥,這就是雙親委派模式佳吞。
雙親委派模式的優(yōu)勢
采用雙親委派模式的好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層次關(guān)系可以避免類的重復(fù)加載棉安,當(dāng)父ClassLoader已經(jīng)加載了該類時底扳,就沒有必要子ClassLoader再加載一次。
其次是考慮到安全因素贡耽,Java核心api中定義的類型不會被隨意替換衷模,假設(shè)通過網(wǎng)絡(luò)傳遞一個名為java.lang.Integer
的類,通過雙親委派模式傳遞到引導(dǎo)類加載器蒲赂,而引導(dǎo)類加載器在核心Java API中發(fā)現(xiàn)這個名字的類阱冶,發(fā)現(xiàn)該類已經(jīng)被加載,并不會重新加載網(wǎng)絡(luò)傳遞過來的java.lang.Integer
滥嘴,而是直接返回已加載過的Integer.class
木蹬,這樣可以防止核心API庫被隨意篡改。
類與類加載器
在JVM中表示兩個class對象是否為同一個類對象的兩個必要條件
- 類的包名和類名必須一致
- 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同
也就是說若皱,在JVM中镊叁,即使這兩個類對象來源于同一個class文件,被同一個虛擬機(jī)所加載走触,但只要加載它們的ClassLoader實例對象不同晦譬,那么這兩個類對象也是不相等的,這是因為不同的ClassLoader實例對象都擁有不同的獨立的類名稱空間
互广,所以加載的class對象存在不同的類名稱空間中敛腌。
class文件的顯示加載和隱式加載
所謂class文件的顯示加載與隱式加載是指JVM加載class文件到內(nèi)存的方式。
顯示加載指在代碼中通過調(diào)用ClassLoader加載class對象,如直接使用Class.forName(name)
或 this.getClass().getClassLoader().loadClass()
加載class對象
隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對象迎瞧,而是通過虛擬機(jī)自動加載到內(nèi)存中夸溶,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象凶硅,此時額外引用的類將通過JVM自動加載到內(nèi)存中缝裁。
編寫自己的類加載器
自定義類加載器的用途
- 運行時裝載或卸載類。這常用于:
- 改變Java字節(jié)碼的裝入,例如氢妈,可用于Java類字節(jié)碼的加密裝入粹污。當(dāng)一個class文件是通過網(wǎng)絡(luò)傳輸并且可能會進(jìn)行相應(yīng)的加密操作時,需要先對class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中首量。
- 修改已裝入的字節(jié)碼
- 熱部署
- Tomcat容器壮吩,每個WebApp有自己的ClassLoader,加載每個WebApp的ClassPath路徑上的類,一旦遇到Tomcat自帶的Jar包就委托給CommonClassLoader加載加缘。
- 隔離鸭叙,比如早些年比較火的Java模塊化框架OSGI,把每個Jar包以Bundle的形式運行,每個Bundle有自己的類加載器(不同Bundle可以有相同的類名)拣宏,Bundle與Bundle之間起到隔離的效果沈贝,同時如果一個Bundle依賴了另一個Bundle的某個類,那這個類的加載就委托給導(dǎo)出該類的BundleClassLoader進(jìn)行加載勋乾。
- Android熱修復(fù)宋下,組件化
實現(xiàn)自定義類加載器需要繼承ClassLoader或者URLClassLoader,繼承ClassLoader則需要自己重寫findClass()方法辑莫,并編寫加載邏輯学歧,繼承URLClassLoader則可以省去編寫findClass()方法及class文件加載轉(zhuǎn)換成字節(jié)碼流的代碼。
自定義File類加載器
繼承ClassLoader
public class FileClassLoader extends ClassLoader {
private String rootDir;
public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 獲取類的class文件字節(jié)數(shù)組
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
// 直接生成class對象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 獲取class文件各吨,并轉(zhuǎn)換成字節(jié)碼流
*
* @param name
* @return
*/
private byte[] getClassData(String name) {
String path = getClassPath(name);
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int read = 0;
while ((read = is.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String getClassPath(String name) {
return rootDir + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
}
}
DemoObj.java
import com.oyty.classloader;
public class DemoObj {
@Override
public String toString() {
return "I am demo obj";
}
}
運行代碼枝笨,輸出I am demo obj
,說明DemoObj類被成功加載绅你。需要注意的是如果DemoObj有包路徑的話,如本例中com.oyty.classloader
昭躺,則編譯后的class文件也需要放在包路徑的文件夾下忌锯。本例中最后class文件的完整路徑是/Users/oyty/Documents/newworkspace/idea/classloader/com/oyty/classloader/DemoObj.class
一般情況下,自己開發(fā)的類加載只需要覆寫findClass(string name)方法即可领炫。java.lang.ClassLoader類的方法loadClass()封裝前面提到的委派模式偶垮。該方法首先會調(diào)用findLoadedClass()方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話,會調(diào)用父類加載器的loadClass()方法來嘗試加載該類似舵;如果父類加載器無法加載該類的話脚猾,就調(diào)用findClass()方法來查找該類。因此為了保證類加載器都正確實現(xiàn)委派模式砚哗,在開發(fā)自己的類加載器時龙助,最好不要覆寫loadClass()方法,而是覆寫findClass()方法蛛芥。
loadClass()
方法源碼如下:
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
繼承URLClassLoader
public class FileUrlClassLoader extends URLClassLoader {
public FileUrlClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public FileUrlClassLoader(URL[] urls) {
super(urls);
}
public FileUrlClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
public static void main(String[] args) throws MalformedURLException {
String rootDir = "/Users/oyty/Documents/newworkspace/idea/classloader";
File file = new File(rootDir);
URI uri = file.toURI();
URL[] urls = {uri.toURL()};
FileUrlClassLoader loader = new FileUrlClassLoader(urls);
try {
Class<?> obj = loader.loadClass("com.oyty.classloader.DemoObj");
System.out.println(obj.newInstance().toString());
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
可以知道提鸟,當(dāng)自定義類加載器繼承自URLClassLoader,將會非常簡潔仅淑,無需額外編寫findClass()方法和class文件的字節(jié)流轉(zhuǎn)換邏輯称勋。
自定義網(wǎng)絡(luò)類加載器
講一個網(wǎng)絡(luò)類加載器的實際用途:通過網(wǎng)絡(luò)類加載器實現(xiàn)組件的動態(tài)更新⊙木梗基本場景是:Java的字節(jié)碼(.class)文件存放在服務(wù)器上赡鲜,客戶端通過網(wǎng)絡(luò)的方式獲取字節(jié)代碼并執(zhí)行。當(dāng)有版本更新的時候庐船,只需要替換掉服務(wù)器上保存的文本即可银酬。
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootUrl + "/"
+ className.replace('.', '/') + ".class";
}
}
類NetworkClassLoader負(fù)責(zé)通過網(wǎng)絡(luò)下載Java類字節(jié)代碼并定義出Java類。在通過NetworkClassLoader加載了某個版本的類之后醉鳖,一般有兩種做法來使用它捡硅。第一種做法是使用Java反射API;另一種做法是使用接口盗棵。需要注意的是壮韭,并不能直接在客戶端代碼中引用從服務(wù)器上下載的類,因為客戶端代碼的類加載器找不到這些類纹因。使用Java反射API可以直接調(diào)用Java類的方法喷屋,而使用接口的做法則是把接口的類放在客戶端中,從服務(wù)器上加載實現(xiàn)此接口的不同版本的類瞭恰,在客戶端通過相同的接口來使用這些實現(xiàn)類屯曹。
雙親委派模型的破壞者--線程上下文類加載器
待完善......
類加載器與Web容器
對于運行在 Java EE?容器中的 Web 應(yīng)用來說,類加載器的實現(xiàn)方式與一般的 Java 應(yīng)用有所不同惊畏。不同的 Web 容器的實現(xiàn)方式也會有所不同恶耽。以 Apache Tomcat 來說,每個 Web 應(yīng)用都有一個對應(yīng)的類加載器實例颜启。該類加載器也使用代理模式偷俭,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器缰盏。這與一般類加載器的順序是相反的涌萤。這是 Java Servlet 規(guī)范中的推薦做法淹遵,其目的是使得 Web 應(yīng)用自己的類的優(yōu)先級高于 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的负溪。這也是為了保證 Java 核心庫的類型安全透揣。
絕大多數(shù)情況下,Web 應(yīng)用的開發(fā)人員不需要考慮與類加載器相關(guān)的細(xì)節(jié)川抡。下面給出幾條簡單的原則:
- 每個 Web 應(yīng)用自己的 Java 類文件和使用的庫的 jar 包辐真,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面。
- 多個應(yīng)用共享的 Java 類文件和 jar 包猖腕,分別放在 Web 容器指定的由所有 Web 應(yīng)用共享的目錄下面拆祈。
- 當(dāng)出現(xiàn)找不到類的錯誤時,檢查當(dāng)前類的類加載器和當(dāng)前線程的上下文類加載器是否正確倘感。
參考:
https://zh.wikipedia.org/wiki/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8
https://blog.csdn.net/javazejian/article/details/73413292
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html