目錄
<span id="jump1">一、什么是類的加載(類初始化)</span>
類加載器的任務是,根據類的全限定名來讀取類的二進制字節流,加載進JVM。并轉換成對應的java.lang.Class對象實例。
<span id="jump1_1">1、BootstrapClassLoader</span>
啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,負責加載存放在 JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被 -Xbootclasspath參數指定的路徑中的,并且能被虛擬機識別的類庫(如rt.jar,所有的java.開頭的類均被 BootstrapClassLoader加載)。啟動類加載器是無法被Java程序直接引用的。總結一句話:<mark>啟動類加載器加載java運行過程中的核心類庫JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的類,也就是JDK提供的類等常見的比如:Object、Stirng、List…</mark>
<span id="jump1_2">2、ExtensionClassLoader</span>
該加載器由 sun.misc.Launcher$ExtClassLoader實現,它負責加載 JDK\jre\lib\ext目錄中,或者由 java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴展類加載器
<span id="jump1_3">3、ApplicationClassLoader</span>
ApplicationClassLoader,該類加載器由 sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。總結一句話:<mark>應用程序類加載器加載CLASSPATH變量指定路徑下的類 即指你自已在項目工程中編寫的類</mark>
<span id="jump1_4">4、CustomClassLoader</span>
<span id="jump1_5">5、線程上下文類加載器</span>
<font color=red>Thread.currentThread().getContextClassLoader()</font>獲取線程上下文類加載器,線程上下文加載器其實很重要,它違背(破壞)雙親委派模型,很好地打破了雙親委派模型的局限性,盡管我們在開發中很少用到,但是框架組件開發絕對要頻繁使用到線程上下文類加載器,如Tomcat等等…
<span id="jump2">二、java虛擬機入口應用:sun.misc.Launcher</span>
<span id="jump2_1">
1、 Launcher類就是類加載器的入口工具類,其中維護了四種加載器。
1、bootClassPath ,這個是c實現的,我們在java層面看不到
2、extcl = ExtClassLoader 擴展類加載器,用于加載java.ext.dirs目錄下的類。
3、loader = AppClassLoader(dirs,extcl) 應用類加載器,將擴展類加載設置給AppClassLoader,當做<font color=red>邏輯父類parent</font>,加載用戶java.class.path下的類。
4、Thread.currentThread().setContextClassLoader(loader); 將當前類加載器設置給線程上下文類加載器
<font color=red>邏輯父類</font>是我的個人理解:
<mark>1、因為本身ExtClassLoader、AppClassLoader都是集成的URLClassLoader。并不沒有真正意義上的繼承關系。
2、這里設置parent,只是在URLClassLoader里存儲了一個parent的對象。然后開放了一個getParent()的方法。實現了形式上的父類關系。
3、為什么要這樣做呢?主要是為了實現雙親委派模型,讓他們有了邏輯上的“父子”關系。
import sun.misc.SharedSecrets;
import sun.misc.URLClassPath;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.util.StringTokenizer;
import java.util.Vector;
public class Launcher {
private static Launcher launcher = new Launcher();
//這個就是BootstrapClassLoader的位置
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
//應用層類加載器
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
//擴展類加載器
ClassLoader extcl;
try {
//初始化內部類擴展類加載器對象。
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
//通過內部類,獲取應用層類加載器對象
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
//設置AppClassLoader為線程上下文類加載器,這里注意設置給線程的是應用層加載器loader。
//等Thread.currentThread().getContextClassLoader()時獲取的就是應用層加載器對象
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
* 擴展類加載器:
* 負責加載存放在JRE的lib/ext/目錄下的jar包中的Class。
*
* 當然我們可以指定-D java.ext.dirs參數來添加和改變ExtClassLoader的加載路徑
*/
static class ExtClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException {
//獲取java.ext.dirs路徑下所有文件
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
//這里具體ACC(AccessController)的原理不再細看了,只看結果就好。
//1、這里的意思就是run方法里或返回一個 ExtClassLoader對象,但是默認走父類URLClassLoader的構造方法。
// 將dirs目錄設置給URLClassLoader()。
//2、這個意思其實就是相當于ExtClassLoader沒有做特殊的處理,僅僅就是用父類的URLClassLoader得方法。
// 而ExtClassLoader只是初始化了一下東西。和AppClassLoader很相似
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[I]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), (ClassLoader)null, sun.misc.Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
private static URL[] getExtURLs(File[] var0) throws IOException {
Vector var1 = new Vector();
for(int var2 = 0; var2 < var0.length; ++var2) {
String[] var3 = var0[var2].list();
if (var3 != null) {
for(int var4 = 0; var4 < var3.length; ++var4) {
if (!var3[var4].equals("meta-index")) {
File var5 = new File(var0[var2], var3[var4]);
var1.add(sun.misc.Launcher.getFileURL(var5));
}
}
}
}
URL[] var6 = new URL[var1.size()];
var1.copyInto(var6);
return var6;
}
}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*
* 應用類加載器:
* 和ExtClassloader一樣,只是將類的目錄設置給URLClassLoader了。
*
* 同時需要說明的是,
* 參數1:類的加載目錄,java.class.path。就是我們的項目目錄
* 參數2:設置父類ExtClassLoader,實際上并不是真正的繼承關系,
* 因為AppClassLoader和ExtClassLoader都extends URLClassLoader。
* 只是邏輯意義上的parent,目的是實現雙親委派模型。
*/
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException {
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
//將java.class.path的項目目錄的類,同時設置邏輯意義的parent extcl,就是上邊初始化的ExtClassLoader對象
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
AppClassLoader(URL[] urls, ClassLoader parent) {
//第二個參數是parent,也就是實現邏輯父子關系的方式。將parent存儲下來,然后暴露getParent()返回parent
super(urls, parent, sun.misc.Launcher.factory);
this.ucp.initLookupCache(this);
}
}
}
<span id="jump2_2">2、 ExtClassLoader 源碼
源碼已經在Launcher里粘過了,這里總結一下。
1、ExtClassLoader 是Launcher的內部類
2、extends URLClassLoader的子類
3、負責加載java.ext.dirs目錄下的類
4、可以指定-D java.ext.dirs參數來添加和改變ExtClassLoader的加載路徑
<span id="jump2_3">3、AppClassLoader</span>
1、AppClassLoader 是Launcher的內部類
2、extends URLClassLoader的子類。
3、負責加載java.class.path目錄下的類,即用戶代碼里的類
4、和ExtClassLoader是<font color=red>邏輯父子關系</font>,4.1因為他們都是直接集成URLClassLoader,并沒有真實的繼承關系。
4.2但是初始化AppClassLoader時,實際使用的是URLClassLoader(dirs,parent)構造函數,第二個參數是存儲一個parent對象。然后暴露一個getParent()方法實現邏輯意義上的父子關系
<span id="jump2_4">4、URLClassLoader</span>
正如上邊所說的,
- 1、AppClassLoader、ExtClassLoader實際上并沒有繼承關系,只是在AppClassLoader里存了一個叫做parent的ExtClassLoader對象而已,也就是邏輯父類。
- 2、那么按這樣理解,是不是ExtClassLoader會存一個BootStrapClassLoader的對象呢?其實并沒有,而是在ClassLoader中單獨有一個native的c方法<font color=red> findBootstrapClass()</font> ,底層是c實現的。作用就是通過BootStrapClassLoader實現對類的加載。也就是說BootStrapClassLoader是底層c寫的,并沒有一個實際的java類。
簡單看一下繼承關系:
看一下關鍵代碼:
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
import java.io.Closeable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.SecureClassLoader;
public class URLClassLoader extends SecureClassLoader implements Closeable {
...
}
public class SecureClassLoader extends ClassLoader {
...
}
public abstract class ClassLoader {
...
//這個存儲的就是邏輯父類的對象。
//1、AppClassLoader存儲的是ExtClassLoader的對象。
//2、ExtClassLoader存儲的是null。
//
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final java.lang.ClassLoader parent;
@CallerSensitive
public final java.lang.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;
}
/**
* Returns a class loaded by the bootstrap class loader;
* or return null if not found.
返回一個通過bootstrapClassLoader加載的class對象。如果沒找到就返回null
*/
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
// return null if not found
private native Class<?> findBootstrapClass(String name);
}
<span id="jump4">三、雙親委派模型</span>
<font id="jump4_1">(1)工作流程:
- 1、類加載器收到加載請求,不會直接加載,而是直接拋給父加載器,類似<font color=red>冒泡式委托</font>。CustomerClassLoader -> AppClassLoader -> ExtClassLoader -> BootStrapClassLoader。
-
2、當加載時,父加載器加載類失敗再依次調用子類加載器,進行加載。如果最終customerClassLoader也加載不到,就拋異常。類似<font color=red>遞進式加載</font>
image.png
<font id="jump4_2">(2)設計意義
- 保證jdk系統類的唯一性,安全性。
雙親委派模式的存在可以保證,java.lang.String等jdk類,不被自定義的類加載器隨意替換,防止篡改。
<span id="jump5">四、ClassLoader源碼分析
<span id="jump5_1">1、ClassLoader#loadClass()源碼分析
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1、判斷是否加載過該類
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//2、如果有父加載器,則調用邏輯父加載器。加載成功就會存起來。在findClass里查找
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//3、使用BootstrapClassLoader加載器加載類
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();
//4、通過子類自定義的方法加載類。
//這個方法是我們自定義類加載器的主要重寫方法。默認是拋出異常。
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;
}
}
}
<span id="jump5_2">2、<font color=red>URLClassLoader</font>#findClass()源碼分析
<mark>這個方法的作用就是
<mark>1、通過path路徑找到.class文件,
2、將.class加載進內存byte[]二進制數組,調用defineClass方法將byte[]轉換成java可以使用的Class<?>字節碼對象
默認ClassLoader的findClass方法是空方法。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
我們這里以URLClassLoader的findClass為例看源碼:
/**
* Finds and loads the class with the specified name from the URL search
* path. Any URLs referring to JAR files are loaded and opened as needed until the class is found.
*
* 從URL路徑中查找并加載具有特定名稱的類。
* 所有JAR包下的class使用之前都需要先加載進內存。
*
* @param name the name of the class
* @return the resulting class
* @exception ClassNotFoundException if the class could not be found,
* or if the loader is closed.
* @exception NullPointerException if {@code name} is {@code null}.
*/
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//name就是定義的類路徑。
// 例如:AppClassLoader默認設置的是java.class.path.上邊Launcher源碼里可以看到
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//通過路徑來找到對應的.class文件,并將二進制數據轉換生成Class<?>對象
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
<span id="jump5_3">3、ClassLoader#defineClass(byte[] b, int off, int len)源碼分析
我們這里只看最內層重載函數。
這個方法主要作用就是通過native的c語言的方法,<mark>將二進制數組轉成java 可以使用的Class<?>字節碼對象。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
//調用native方法,將二進制數組轉成class對象
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
<span id="jump5_4">4、小結
- <mark>loadClass() 作用就是實現雙親委派機制的邏輯
- <mark>findClass() 作用就是通過path,找到.class文件,并加載進內存(放到byte[]二進制數組中)。
- <mark>defineClass() 作用就是將二進制數組byte[]轉成java能使用的Class<?>字節碼對象
<span id="jump6">五、自定義類加載器</span>
<font id="jump6_1">1、作用
<font color=red>核心是擴展java虛擬機的動態加載類的機制</font>,用戶可以定制類的加載邏輯,比如:熱更新。就是通過自定類加載器,來加載網絡請求的類。將.class文件通過網絡下載到本地,然后通過自定義的類加載器加載。
<font id="jump6_2">2、實現邏輯
通過上邊對ClassLoader的源碼分析。已經很清楚了,我們可以直接繼承ClassLoader
- 1、<font color=red>重寫loadClass()</font>方法。但是jdk1.2之后官方已經<font color=red>不建議</font>直接重寫loadClass()了,因為那有可能不符合雙親委派機制了,畢竟是在這個方法實現的雙親委派邏輯。
- 2、<font color=red>重寫findClass()</font>方法,通常來說,我們重寫這個方法足夠了。這個方法是找到.class文件并加載進內存,存到byte[]二進制數組中。
<font id="jump6_3">3、示例
(1)創建一個Demo.class文件
- 1、創建package目錄com.test的Demo.java文件。
注意package包名必須有,這是java文件規范。默認就是文件目錄。
- 2、使用javac Demo.java 命令直接生成Demo.class文件
- 3、放到/Users/wangzheng/workspace_java/com/test 目錄下,準備使用自定義加載器加載
package com.test;
public class Demo {
public Demo(){
System.out.println("================================");
System.out.println("我是MyClassLoader加載器加載進內存的。");
System.out.println("================================");
}
}
(2)自定義MyClassLoader類加載器
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
public class MyClassLoaderDemo extends ClassLoader {
//自定義根目錄
String customRootPath = "";
public MyClassLoaderDemo(String rootPath) {
this.customRootPath = rootPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
final Class<?> result;
//1、拼接成自定義的.class存儲目錄。也就是Demo.class的目錄
String filePath = customRootPath + '/' + name.replace('.', '/').concat(".class");
//2、通過Demo.class目錄,文件流方式加載進byte[]二進制數組。
byte[] bytes = loadPathToByteArray(filePath);
//3、調用native的方法,將byte[]二進制數組,轉成Class<?>字節碼對象
result = defineClass(name, bytes, 0, bytes.length);
return result;
}
//將路徑轉成byte[]
private byte[] loadPathToByteArray(String filePath) {
try {
//通過路徑,文件流讀取到byte[]二進制數組中
FileInputStream fis = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = fis.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
try {
//設置Demo.class文件的根目錄。
MyClassLoaderDemo myClassLoaderDemo = new MyClassLoaderDemo("/Users/wangzheng/workspace_java");
//加載Demo.class
Class<?> aClass = myClassLoaderDemo.loadClass("com.test.Demo");
//實例化Demo對象
Object o = aClass.newInstance();
System.out.println(o.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印結果:
================================
我是MyClassLoader加載器加載進內存的。
================================
com.crm.web.test.MyClassLoaderDemo@64a294a6
<font id="jump7">六、加載類的三種方式
<font id="jump7_1">1、分類
1、靜態加載,通過<font color=red>new</font>關鍵字來實例化對象
2、動態加載,通過<font color=red>Class.forName()</font>方式動態加載對象(反射加載),然后調用類的<font color=red>newInstance()</font>實例化
3、動態加載,通過類加載器的<font color=red>loadClass()</font>方法來加載類,然后調用類的<font color=red>newInstance()</font>方法實例化對象
<font id="jump7_2">2、三種方式的區別
(1)第一種和第二種,使用的是默認類加載器(this.getClass.getClassLoader)。第三種可以使用用戶自定義的類加載器
(2)如果需要在當前路徑外加載.class文件,那么只能用第三種方式。
(3) <mark>Class.forName()和ClassLoader.loadClass()區別:
1、Class.forName(className)一個參數的方法,默認是會加載進內存的同時給static分配內存(初始化)。
2、Class.forName(className,initialize,loader)中間initialize可以控制是否進行(初始化)
3、ClassLoader.loadClass()方式是不會給static分配內存(<font color=red>初始化:參考上一篇文章。)。
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
//第二個參數,就是是否進行初始化階段。
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
/**
* 這種三個參數的,initialize就是決定是否進行初始化的。
**/
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}