JVM對於java程序員來講既是高級也是基礎,剛入行的同窗沒必要知道jvm的內存劃分、不須要知道類的加載過程、GC的回收過程也能夠舒舒服服的寫代碼,可是這種知其然不知其因此然的態度確定會限制咱們的上升空間,今天這篇文章開始走進jvm,咱們從第一步開始,先搞清楚類是怎麼被加載的,這就是今天要分享的內容!java
進入正題以前要先說一下JVM,JVM的組成結構主要是由 類裝載子系統、運行時數據區、執行引擎、本地方法接口這4部分組成,而今天的文章主要圍繞類狀態子系統展開描述!c++
%{JAVA_HOME}\jdk1.8.0_261\jre\lib
目錄下的類;sun.misc.Launcher.ExtClassLoader
,主要負責加載%{JAVA_HOME}\jdk1.8.0_261\jre\lib\ext
目錄下的類;sun.misc.Launcher.AppClassLoader
,負責加載咱們配置的環境變量classpath
目錄下的類;ClassLoader
類能夠實現本身定製的類加載方式,這種方式能夠用來打破雙親委派模型(雙親委派模型下面會講到);咱們能夠經過一段代碼很清晰的看到每一個類加載器的樣子:程序員
public class TestClassLoader {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
System.out.println(TestClassLoader.class.getClassLoader());
}
}
複製代碼
輸出結果:bootstrap
null
sun.misc.Launcher$ExtClassLoader@77459877
sun.misc.Launcher$AppClassLoader@18b4aac2
複製代碼
Object類對應的加載器爲何是null嘞? 上面已經說過了,jvm內部會建立一個c++編寫的啓動類加載器負責去加載%{JAVA_HOME}\jdk1.8.0_261\jre\lib
目錄下的類,這個加載器在java裏面是獲取不到的,因此是null; 下面兩個,一個是ExtClassLoader,一個是AppClassLoader,是sun.misc.Launcher
類的靜態內部類;而這個Launcher類是由C++調用sun.misc.Launcher#getLauncher
方法得到的;api
能夠經過如下代碼看一下類加載器之間有什麼聯繫安全
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extClassloader = appClassLoader.getParent();
ClassLoader bootstrapLoader = extClassloader.getParent();
System.out.println("the bootstrapLoader : " + bootstrapLoader);
System.out.println("the extClassloader : " + extClassloader);
System.out.println("the appClassLoader : " + appClassLoader);
複製代碼
輸出結果:markdown
the bootstrapLoader : null
the extClassloader : sun.misc.Launcher$ExtClassLoader@77459877
the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
複製代碼
能夠看到,默認的類加載器是AppClassLoader,往上走是ExtClassLoader,最上層是BootStrapClassLoader; 先來看一下類加載器的類圖結構:app
類加載器都是繼承的ClassLoader,爲每一個子類都維護了一個parent屬性:jvm
下面咱們就重點看parent屬性作了什麼事情,在實例化Launcher的時候 ,構造器裏面對app和ext這兩個對象作了初始化:ide
那繼續點進去看看AppClassLoader是怎麼建立的,中間套娃的代碼我就跳過了,他是直接調用super(parent)這個構造器,直接看他就行了:
在這個地方維護了類加載器之間的父子關係,因此Ext也是App的父加載器,那麼這麼作他到底要幹什麼呢?這裏涉及到了一個概念:雙親委派(下面會細說);上面的代碼還反映了一個問題:默認的類加載器是AppClassLoader?在JVM內部會默認調用Launcher類的getClassLoader()方法來獲取一個默認類加載器進行加載,而這個classLoader恰好就是在實例化Launcher類的時候生成的AppClassLoader:
一個類在被類加載器加載的時候,該類的加載器不會當即去加載,而是經過parent屬性找到其父加載器進行加載,一直遞歸往上找,一直到頂層的BootStrap都沒有被加載,就會返回到本類的加載器進行加載:
ClassLoader裏面除了維護parent屬性外,還維護了一個公共的loadClass方法,這個方法就是雙親委派的實現。咱們來詳細分析下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 這裏會一直往上調,app的parent是ext,ext的parent是bootstrap
c = parent.loadClass(name, false);
} else {
// 這個方法最終會調用到一個native方法,加載不到類的時候會返回null
// return null if not found
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父加載器沒有加載到類,返回null
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.lang包
package java.lang;
// 覆蓋原有的Object類
public class Object {
public static void main(String[] args) {
System.out.println("object ....");
}
}
複製代碼
上面的代碼執行完以後,會是什麼結果呢?
錯誤: 在類 java.lang.Object 中找不到 main 方法, 請將 main 方法定義爲:
public static void main(String[] args) 不然 JavaFX 應用程序類必須擴展javafx.application.Application 複製代碼
因此,由於有雙親委派的存在,咱們這種惡意破壞原有api的行爲就行不通了;
咱們能夠經過自定義類加載器,來指定咱們本身要去加載的類;讀完以上源碼咱們不難發現,雙親委派的邏輯在loadClass方法裏,而加載類的邏輯是在findClass方法裏,我想要本身實現一個類加載器就應該去繼承ClassLoader,重寫findClass方法,在findClass方法裏面去加載咱們本身指定的類:
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadByte(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
//初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載器設置爲應用程序類加載器AppClassLoader
MyClassLoader classLoader = new MyClassLoader("D:/test");
// 在這個路徑下面放一個User.class文件,由咱們本身的類加載器去加載
Class clazz = classLoader.loadClass("com.maolin.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
複製代碼
運行,輸出結果:
com.maolin.MyClassLoader
複製代碼
User.class的字節碼是被MyClassLoader加載器加載的; 經過這種方式還能夠打破雙親委派的機制,在重寫findClass的基礎上,再重寫loadClass:
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
/* 咱們把ClassLoader類的loadClass方法裏的代碼複製出來, 把雙親委派的那段代碼去掉,讓當前的類加載器直接加載,不向上委託 */
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
複製代碼
打破雙親委派機制篇幅太長,後續會單獨寫一篇文章來描述,想知道結果的能夠參考這兩篇文章:
類加載器說完了,咱們再來瞅瞅一個類被加載的時候會經歷哪些過程:
感謝各位讀者朋友耐心看完個人文章,文章中如有錯誤之處,還請留言指正,或者文章中有哪一個細節描述的不夠清晰,也能夠在評論區留言,我看到後必定會回覆並改正;
不求作的最好,但求作的更好。