雙親載入機制
以下兩張圖足夠說明(jdk1.8)雙親載入機制。
注意的是AppClassLoader和ExtClassLoader都是sun.misc.Launcher的內部類,同樣都繼承URLClassLoader,parent是ClassLoader中欄位,當初始化AppClassLoader時,它的parent就被設定ExtClassLoader,而ExtClassLoader的parent則被設定為null,使用者自定義的則被設定AppClassLoader。
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Check access to the parent class loader
// If the caller's class loader is same as this class loader,
// permission check is performed.
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
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;
}
}
第一次打破雙親載入機制
為何出現的原由?
雙親載入模型是在JDK1.2之後才被引入,自然為向前相容,沒有把loadClass方法設定為不可覆蓋的方法,而是新增加了findClass方法,讓使用者儘量重寫此方法。
換個方向理解,其實就是可以覆蓋loadClass方法,進行直接載入一些類。如下面例子,自定義載入器繼承ClassLoader,在loadClass方法中,刪除雙親載入邏輯,後面增加一個判斷,如果不是自己的包下檔案,讓父類去載入。而測試類,直接使用自定義載入器去loadClass時,返回的類的載入器是此自定義載入器。
一個例子:
public class FirstClassLoader extends ClassLoader{
private String classPath;
public FirstClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] getByte(String name) throws Exception{
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
int len = fileInputStream.available();
byte [] data = new byte[len];
fileInputStream.read(data);
fileInputStream.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try{
byte [] data = getByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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();
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 除了刪除上面雙親載入機制地方,要加這個判斷,不然會Object等一些類無法載入
if(!name.startsWith("com.java.study")) {
return this.getParent().loadClass(name);
}
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;
}
}
}
測試類:
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
// 傳入你要load的class路徑
FirstClassLoader firstClassLoader = new FirstClassLoader("xxxx");
Class clazz = firstClassLoader.loadClass("com.java.study.StudyApplication");
System.out.println(clazz.getClassLoader());
}
}
第二次打破雙親載入機制
第二次打破雙親載入機制,出現在JNDI服務中。瞭解前可以先熟悉SPI(Service Provider Interface)。(參考:https://www.zhihu.com/question/49667892)
對應JDBC例子就是,呼叫方是rt.jar包下DriverManager,而具體Driver實現方在三方jar包下(如mysql-connector-java.jar),Bootstrap Class Loader載入的類在載入過程中要使用Application Class Loader才能載入的類,在雙親載入模型是不能做到的。所以這裡出現打破雙親載入機制。具體分析如下:
先看獲取連線寫法:
Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.130.1:3306/test", "root", "password");
再分析DriverManager類,其中靜態程式碼塊在類載入過程中進行初始化呼叫loadInitialDrivers(),接下來呼叫ServiceLoader#load(), 此方法中引入Thread.currentThread().getContextClassLoader(),即Application Class Loader,是具體打破雙親載入機制的實現。後面載入Driver具體實現類,便可用此Loader
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
......
}
}
}
}
public final class ServiceLoader<S> implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) {
// 打破雙親載入機制具體實現
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}
在Thread類中可以看到,初始化時會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,這個類載入器預設就是Application Class Loader。
public class Thread implements Runnable {
private ClassLoader contextClassLoader;
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
Thread parent = currentThread();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}
解下來重要方法一,ServiceLoader#hasNextService() 中會去掃描jar包下META-INF/services/java.sql.Driver檔案,再通過parse(service, configs.nextElement())方法去獲取Driver具體實現類com.mysql.cj.jdbc.Driver等。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 載入jar包下META-INF/services/java.sql.Driver檔案
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 獲取具體實現類
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
重要方法二,ServiceLoader#nextService()先傳入全路徑名和Loader獲取com.mysql.cj.jdbc.Driver未初始化的例項物件,再在c.newInstance()中進行初始化
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 獲取com.mysql.cj.jdbc.Driver
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
其中com.mysql.cj.jdbc.Driver原始碼中可看到,類載入時會往DriverManager中註冊Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
// 往DriverManager中註冊Driver
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
最後到具體DriverManager#getConnection()方法。由於在載入com.mysql.cj.jdbc.Driver時已經設定classLoader為Application class Loader,此時callerCL不為null。直接走下面遍歷註冊的Drivers,獲取連線
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
Connection con = aDriver.driver.connect(url, info);
} catch (SQLException ex) {
......
}
}
}
}
以上第二次打破雙親載入機制就全部分析完,主要實現便是引入Thread.currentThread().getContextClassLoader()。
第三次打破雙親載入機制
第三次打破雙親載入機制由於使用者對程式動態性追求而導致的,如熱部署。主要可研究OSGi原理。
先把OSGi類搜尋順序摘錄(《深入理解java虛擬機器》):
- 將以java.*開頭的類委派給父類載入器載入
- 否則,將委派列表名單內的類,委派給父類載入器載入
- 否則,將Import列表中的類,委派給Export這個類的Bundle的類載入器載入
- 否則,查詢當前Bundle的ClassPath使用主機的類載入器載入
- 否則,查詢類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類載入器載入
- 否則,查詢Dynamic Import列表的Bundle,委派給對應Bundle的類載入器載入
- 否則,類查詢失敗
另補充JDK9後對應原始碼,ClassLoader中一些改變。
新增BuiltinClassLoader;ExtClassLoader替換為PlatformClassLoader。新增BootClassLoader。AppClassLoader、PlatformClassLoader、BootClassLoader三者都繼承BuiltinClassLoader。如下圖:
在AppClassLoader中先委派父類載入器進行載入,查詢要載入的包是否在module中:
不在,委派parent去載入;
在,並和當前ClassLoader相同,使用PlatformClassLoader去module中載入;
在,但和當前ClassLoader不同,使用其他載入器載入
private static class AppClassLoader extends BuiltinClassLoader {
@Override
protected Class<?> loadClass(String cn, boolean resolve) throws ClassNotFoundException{
return super.loadClass(cn, resolve);
}
}
public class BuiltinClassLoader extends SecureClassLoader {
@Override
protected Class<?> loadClass(String cn, boolean resolve) throws ClassNotFoundException
{
Class<?> c = loadClassOrNull(cn, resolve);
if (c == null)
throw new ClassNotFoundException(cn);
return c;
}
}
public class BuiltinClassLoader extends SecureClassLoader {
protected Class<?> loadClassOrNull(String cn, boolean resolve) {
synchronized (getClassLoadingLock(cn)) {
// check if already loaded
Class<?> c = findLoadedClass(cn);
if (c == null) {
// find the candidate module for this class
LoadedModule loadedModule = findLoadedModule(cn);
if (loadedModule != null) {
// package is in a module
BuiltinClassLoader loader = loadedModule.loader();
if (loader == this) {
if (VM.isModuleSystemInited()) {
c = findClassInModuleOrNull(loadedModule, cn);
}
} else {
// delegate to the other loader
c = loader.loadClassOrNull(cn);
}
} else {
// check parent
if (parent != null) {
c = parent.loadClassOrNull(cn);
}
// check class path
if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
c = findClassOnClassPathOrNull(cn);
}
}
}
if (resolve && c != null)
resolveClass(c);
return c;
}
}
}
參考:
《深入理解java虛擬機器》
https://www.bilibili.com/video/BV1RK4y1a7NF?p=6
https://www.jianshu.com/p/09f73af48a98
https://www.cnblogs.com/jay-wu/p/11590571.html
https://www.jianshu.com/p/78f5e2103048
https://www.cnblogs.com/lyc88/articles/11431383.html
https://www.cnblogs.com/huxuhong/p/11856786.html
https://www.zhihu.com/question/49667892
https://www.jianshu.com/p/5dc10732de6a
https://www.jianshu.com/p/a18aecaecc89