Class Load
load
我們編譯後的.class文件是如何被JVM加載進入內存的呢,中間經過了哪些個步驟呢?如下圖:
主要是經過這麼幾個步驟:
- loading加載:從磁盤讀取xxx.class文件進入內存
- linking
- verifiy驗證階段:驗證class文件是否已“cafababe”開頭
- prepare準備階段:對類的成員變量開闢空間,將class中的所有成員變量(靜態和非靜態)賦值爲默認值,不執行任何初始化操作
- resolute解析階段:爲類、接口、方法、成員變量的符號引用定位直接引用,也即與外部其他class建立引用關係 - init 初始化:將靜態成員變量的默認值替換成初始值
如現有A.class文件,內部定義了一個靜態成員變量B=1,其成員變量B加載過程如下:在prepare階段,先給B賦默認值0,在init階段,再給B賦初始值1。
那麼,當JVM將class文件讀取進內存之後,內存中有哪些東西呢?
- 將Class元數據信息保存在matespace上(1.8之後,1.8之前是保存在老年代)
類型 | 保存位置 |
---|---|
靜態成員變量 | 方法區的靜態部分 |
靜態方法 | 方法區的靜態部分 |
非靜態方法 | 方法區的非靜態部分 |
靜態代碼塊 | 方法區的靜態部分 |
構造代碼塊 | 方法區的靜態部分 |
- 創建當前class文件的Class對象,無論我們後面在代碼中new出了多少個實例,JVM中對應的Class對象,始終只有一個。
ClassLoader
Java中有4種ClassLoader,自上而下依次爲Bootstrap CLassloder、ExtClassLoader、AppClassLoader、CustomClassLoader(自定義類加載器)
那這4中加載器,它們之間的關係是什麼,各自的作用域又是什麼呢?要搞清楚這個問題,我們得分析下源碼:
Launcher.class部分源碼如下:
private static String bootClassPath = System.getProperty("sun.boot.class.path");
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
...
}
}
static class ExtClassLoader extends URLClassLoader {
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
...
}
}
通過源碼,我們可以看到有三處定義了uri,這實際便是幾個加載器的作用域,即
- Bootstrap CLassloder, 負責加載sun.boot.class.path路徑下的類,如lib/rt.jar,charset.jar等核心類庫,此部分功能由c++實現,我們代碼中這部分調用getClassLoader()方法,其返回的是個null,是個跟加載器
- ExtClassLoader ,負責加載java.ext.dirs路徑下的類,負責加載擴展包
- AppClassLoader,負責加載java.class.path路徑下的類
- CustomClassLoader,可自定義加載某個路徑下的類以及加載方式。
雙親委派
JVM沒有規定一個Class文件具體啥時候需要被加載,具體時間由各自的虛擬機決定。常用的Hotspot採用懶加載方式,即當需要用到這個Class的時候纔將該類加載進內存,加載流程是什麼樣的呢?這裏面就會涉及類加載的雙親委派機制了,我們以最全流程爲例講解:
現有一個Class需要加載進內存,使用CustomClassLoader。
- CustomClassLoader查看自身是否有加載過這個Class,如果有則不再加載,直接返回,如果沒有加載,轉2
- CustomClassLoader詢問父加載器AppClassLoader是否有加載過這個class,如果父加載器有加載過,直接返回,如果父加載器也沒有加載過,轉3
- AppClassLoader向父加載器ExtClassLoader詢問是否有加載過,同理,如果沒有加載過,或者加載不成功,轉4
- ExtClassLoader詢問父加載器Bootstrap,結果還是一樣沒有加載過,且自身不負責加載該類,Bootstrap通知子加載器ExtClassLoader,要求ExtClassLoader自己嘗試加載
- ExtClassLoader嘗試加載該類,發現該類作用域不在自己的管轄內,不能加載,於是再通知自己的子加載器AppClassLoader,要求AppClassLoader自己嘗試加載
- AppClassLoader加載失敗,通知CustomClassLoader自己加載
- CustomClassLoader負責加載該class進內存
通過上面的步驟可以得知,所謂雙親委派,就是在需要加載一個新的Class的時候,需要向父加載器一層層的問詢是否已經加載過,如果都沒有加載過,則由父加載器一層層通知子加載器,由子加載器嘗試加載class。
Java爲什麼要這麼設計呢?主要是爲了安全,設想這樣一種場景,我們自定義了一個加載器,用來加載String類,如果直接走我們自定義的加載器,我們在加載器內部收集所有string數據上傳,如果這些數據正好是用戶的用戶名和密碼,那我們就可以輕鬆拿到每個人的用戶名和密碼了。
還有一點,爲了避免重複加載,如果一個類已經被父加載器加載過了,內存中已經存在了一個Class對象,這時候直接用就可以了,不需要再次加載。
Java是如何實現雙親委派的呢?源碼如下:
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;
}
}
其實很簡單,就是通過遞歸的方式,先在 parent.loadClass中問詢是否有加載過該類,如過parent沒有加載過,且嘗試加載失敗,則當前的子加載器自己嘗試加載該class。