Java面試相關(一)-- Java類加載全過程

轉(zhuǎn)載請說明出處:Java面試相關(一)-- Java類加載全過程

JVM判斷并裝載類的過程

概述:

我們知道,Java中我們寫類的代碼,是存在于一個個.java文件中的,而這個后綴名也是讓JVM識別編譯的基礎。可能有些Android開發(fā)者對幾個ClassLoader(如:AppClassLoader等)比較熟悉,那么,整個類的加載過程:從未進行編譯的.java文件,到類的初始化完畢并等待被實例化使用的過程,具體是怎么樣的。
??收集Java資料、看了幾篇本人覺得較好的博文后,總結(jié)以下關于Java類的加載過程,掌握此過程,能夠更加理解Java類的各個方法的執(zhí)行順序,以及JVM的工作和Java類生成的原理。讀者笑納~

類的加載過程分析


從.java文件到實際加載到內(nèi)存中,實際上是這樣的一個過程
??.java文件 -> 通過你的JDK環(huán)境相關指令編譯 -> .class文件 -> JVM初始化之后,如果有類的執(zhí)行、調(diào)用等相關操作,JVM就會將.class文件加載到內(nèi)存中,并開始下面的一系列處理:(鏈接->初始化)

一、關于ClassLoader


首先我們要搞清楚一點,ClassLoader是Java用于加載類的一個機制。等到程序運行時,JVM先初始化,在JVM初始化的過程中,JVM生成幾個ClassLoader,JVM調(diào)用指定的ClassLoader去加載.class文件等各類路徑、文件的類。

  1. 程序運行時類的加載實際過程
  • JDK執(zhí)行指令去尋找jre目錄,尋找jvm.dll,并初始化JVM;
  • 產(chǎn)生一個Bootstrap Loader(啟動類加載器);
  • BootstrapLoader自動加載ExtendedLoader(標準擴展類加載器),并將其父Loader設為Bootstrap Loader。
  • BootstrapLoader自動加載AppClassLoader(系統(tǒng)類加載器),并將其父Loader設為Extended Loader。
  • 最后由AppClassLoader加載HelloWorld類。
  1. 各種ClassLoader及其特點
  • BootstrapLoader(啟動類加載器):加載System.getProperty("sun.boot.class.path")所指定的路徑或jar
  • ExtendedLoader(標準擴展類加載器ExtClassLoader):加載System.getProperty("java.ext.dirs")所指定的路徑或jar。在使用Java運行程序時,也可以指定其搜索路徑,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld
  • AppClassLoader(系統(tǒng)類加載器AppClassLoader):加載System.getProperty("java.class.path")所指定的路徑或jar。在使用Java運行程序時,也可以加上-cp來覆蓋原有的Classpath設置,例如: java -cp ./lavasoft/classes HelloWorld
  • 特點
    • ExtClassLoader和AppClassLoader在JVM啟動后,會在JVM中保存一份,并且在程序運行中無法改變其搜索路徑。如果想在運行時從其他搜索路徑加載類,就要產(chǎn)生新的類加載器。
    • 運行一個程序時,總是由AppClassLoader(系統(tǒng)類加載器)開始加載指定的類
    • 在加載類時,每個類加載器會將加載任務上交給其父,如果其父找不到,再由自己去加載
    • BootstrapLoader(啟動類加載器)是最頂級的類加載器了,其父加載器為null
  1. 各類ClassLoader的關系圖解(幫助理解)


    各個ClassLoader的作用以及他們之間的關系.png

注意:圖解中可得,執(zhí)行代碼c.getClassLoader().getParent().getParent()null,由于get不到BootstrapLoader,因為BootstrapLoader是C層次實現(xiàn)的。

  1. 關于不同類加載器所處命名空間不同的問題理解
    Java當中,不同的類加載器加載的類在虛擬機中位于不同命名空間下,而不同命名空間下的類相互不可見。那么,有時我們會很不解,BootstrapLoader加載java.util.List類,而我們自己定義的類比如com.androidjp.MyClass 則由APPClassLoader來加載。從上面的圖中我們可以看到,APPClassLoader等ClassLoader之間是繼承與被繼承的關系,而APPClassLoader本身可以作為我們自定義ClassLoader的父類,當默認調(diào)用APPClassLoader去加載某個類時,它先在它的緩存區(qū)查看要加載的這個類是否存在,存在則直接加載,不存在則它會讓它的父類ExtendedLoader去加載,而ExtendedLoader又會調(diào)用進行同樣的步驟,直到他的父親BootstrapLoader,這種調(diào)用關系我們稱之為“雙親委托機制”。在這種機制下,原本在JVM的BootstrapLoader類型表【JVM為每一個類加載器維護一個表,表中存放所有以這個類加載器為初始類加載器的類】中的java.util.List,就能夠被這個MyClass所發(fā)現(xiàn),因為他們兩個類之間相互是‘融洽’的,換句話說,MyClass的初始類加載器的先輩所加載的類,也是我的親人,我們之間的交互是安全的。所以, 才有了‘MyClass中可以調(diào)用加載List’的過程。詳細可以點擊這篇文章來參考。

二、類的加載方式


  1. 方式一:命令行啟動應用時候由JVM初始化加載
  2. 方式二:通過Class.forName()方法動態(tài)加載(默認會執(zhí)行初始化塊,但如果指定ClassLoader,初始化時不執(zhí)行靜態(tài)塊 )
  3. 方式三:通過ClassLoader.loadClass()方法動態(tài)加載(不會執(zhí)行初始化塊 )

解析:
方式一其實就是通過以下幾種主動引用類的方式所觸發(fā)的JVM的類加載和初始化過程。然后,其實這三種類加載方式,在java 層面上都是JVM調(diào)用了ClassLoader去加載類的過程,只是:方式一相對與方式二和方式三而言,屬于靜態(tài)方式的加載;而方式二和方式三的區(qū)別,在于Class.ForName源碼中:

///Class.forname(String name)
public static Class<?> forName(String className) throws ClassNotFoundException {
  return forName(className, true, VMStack.getCallingClassLoader());
}
………………
///實際調(diào)用:
public static Class<?> forName(String className, boolean shouldInitialize,
      ClassLoader classLoader) throws ClassNotFoundException {
  if (classLoader == null) {
      classLoader = BootClassLoader.getInstance();
  }
    Class<?> result;
  try {
      result = classForName(className, shouldInitialize, classLoader);
  } catch (ClassNotFoundException e) {
      Throwable cause = e.getCause();
      if (cause instanceof LinkageError) {
          throw (LinkageError) cause;
      }
      throw e;
  }
  return result;
}

在源碼當中可以看到,參數(shù)boolean shouldInitialize,在默認情況下的Class.forName(String)此參數(shù)默認為true,則默認情況下會進行初始化,

那么,初始化到時是怎么個操作過程,此過程又是怎么樣去觸發(fā)的呢?下面我們通過分析類的加載流程以及整體圖解,來幫助說明。

三、詳細分析整個類的加載流程


下面分析一下類的幾種加載方式、ClassLoader對類加載的背后,是怎么個原理:

1. 類從編譯、被使用,到卸載的全過程:

<u>編譯 -> 加載 -> 鏈接(驗證+準備+解析)->初始化(使用前的準備)->使用-> 卸載</u>

2. 類的初始化之前

加載(除了自定義加載)和鏈接的過程是完全由jvm負責的,包括:加載 -> 驗證 -> 準備 -> 解析

這里的“自定義加載”可以理解為:自定義類加載器去實現(xiàn)自定義路徑中類的加載,可以參考這篇文章。由于默認各個路徑的類文件加載過程在JVM初始化的過程中就默認設定好了,也就是一般步驟下的加載過程,已經(jīng)在JVM初始化過程中規(guī)定的AppClassLoader等加載器中規(guī)定了步驟,所以,按一般的加載步驟,就是按JVM規(guī)定的順序,JVM肯定先負責了類的加載和鏈接處理,然后再進行類初始化。

  1. 首先是加載
  • 此過程由類加載器完成
  • 這一塊JVM要完成3件事:
    1. 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
    2. 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
    3. 在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。
  • 這一步很靈活,很多技術都是在這里切入,因為它并沒有限定二進制流從哪里來,那么我們可以<u>用系統(tǒng)的類加載器,也可以用自己的方式寫加載器來控制字節(jié)流的獲取</u>:
    1. 從class文件來->一般的文件加載
    2. 從zip包中來->加載jar中的類
    3. 從網(wǎng)絡中來->Applet
  • 獲取二進制流獲取完成后會按照jvm所需的方式保存在方法區(qū)中,同時會在java堆中實例化一個java.lang.Class對象與堆中的數(shù)據(jù)關聯(lián)起來。
  1. 然后是驗證(也稱為檢驗):
    一句話:檢查代碼的:完整性、正確性、安全性
  • 主要經(jīng)歷幾個步驟:文件格式驗證->元數(shù)據(jù)驗證->字節(jié)碼驗證->符號引用驗證
    • 文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范并 驗證其版本是否能被當前的jvm版本所處理。ok沒問題后,字節(jié)流就可以進入內(nèi)存的方法區(qū)進行保存了。后面的3個校驗都是在方法區(qū)進行的。
    • 元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義化分析,保證其描述的內(nèi)容符合java語言的語法規(guī)范。
    • 字節(jié)碼檢驗:最復雜,對方法體的內(nèi)容進行檢驗,保證其在運行時不會作出什么出格的事來。
    • 符號引用驗證:來驗證一些引用的真實性與可行性,比如代碼里面引了其他類,這里就要去檢測一下那些來究竟是否存在;或者說代碼中訪問了其他類的一些屬性,這里就對那些屬性的可以訪問行進行了檢驗。(這一步將為后面的解析工作打下基礎)
  • 目的:確保class文件的字節(jié)流信息符合jvm的口味,不會讓jvm感到不舒服。假如class文件是由純粹的java代碼編譯過來的,自然不會出現(xiàn)類似于數(shù)組越界、跳轉(zhuǎn)到不存在的代碼塊等不健康的問題,因為一旦出現(xiàn)這種現(xiàn)象,編譯器就會拒絕編譯了。但是,跟之前說的一樣,Class文件流不一定是從java源碼編譯過來的,也可能是從網(wǎng)絡或者其他地方過來的,甚至你可以自己用16進制寫,假如jvm不對這些數(shù)據(jù)進行校驗的話,可能一些有害的字節(jié)流會讓jvm完全崩潰。

驗證階段很重要,但也不是必要的,假如說一些代碼被反復使用并驗證過可靠性了,實施階段就可以嘗試用-Xverify:none參數(shù)來關閉大部分的類驗證措施,以簡短類加載時間。

  1. 隨后是準備
    一句話:為靜態(tài)域分配存儲空間
  • 這階段會為類變量(指那些靜態(tài)變量)分配內(nèi)存并設置類比那輛初始值的階段,這些內(nèi)存在方法區(qū)中進行分配。這里要說明一下,這一步只會給那些靜態(tài)變量設置一個初始的值,而那些實例變量是在實例化對象時進行分配的。
    例如:
    • public static int value=123;此時value的值為0,不是123。
    • private int i = 123; 此時,i 還未進行初始化,因為這句代碼還不能執(zhí)行。
  1. 最后是解析
    一句話:符號引用 -> 直接引用
  • 是對類的字段,方法等東西進行轉(zhuǎn)換,具體涉及到Class文件的格式內(nèi)容。

3. 類的初始化條件(主動對類進行引用)

說明:要對類進行初始化,代碼上可以理解為<u>‘為要初始化的類中的所有靜態(tài)成員都賦予初始值、對類中所有靜態(tài)塊都執(zhí)行一次,并且是按代碼編寫順序執(zhí)行’</u>。
如下代碼:輸出的是‘1’。如果①和②順序調(diào)換,則輸出的是‘123’。

public class Main {
 public static void main(String[] args){
      System.out.println(Super.i);
  }
}
class Super{
  //①
  static{
      i = 123;
  }
  //②
  protected static int i = 1;
}
  1. 遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,假如類還沒進行初始化,則馬上對其進行初始化工作。
    其實就是3種情況:
  • 用new實例化一個類時
  • 讀取或者設置類的靜態(tài)字段時(不包括被final修飾的靜態(tài)字段,因為他們已經(jīng)被塞進常量池了)
  • 執(zhí)行靜態(tài)方法的時候。
  1. 使用java.lang.reflect.*的方法對類進行反射調(diào)用的時候,如果類還沒有進行過初始化,馬上對其進行。
  2. 初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。
  3. 當jvm啟動時,用戶需要指定一個要執(zhí)行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。
  4. 用Class.forName(String className);來加載類的時候,也會執(zhí)行初始化動作。
    【注意:ClassLoader的loadClass(String className);方法只會加載并編譯某類,并不會對其執(zhí)行初始化】

說明:“主動對類進行引用”指的就是以上五種JVM規(guī)定的判定初始化與否的預處理條件。
那么,其他的方式,都可歸為‘類被動引用’的方式,這些方式是不會引起JVM去初始化相關類的:

  1. <u>子類調(diào)用父類</u>的靜態(tài)變量(子類不會進行初始化,父類會初始化)
  2. 通過<u>數(shù)組</u>引用類的情況(類Main不會被初始化)
    如:list = Main[10];
  3. 調(diào)用類中的<u>final靜態(tài)常量</u>(類不會被初始化)

四、原理分析圖解


類加載中每個部分詳細的原理說明,可以查看這篇文章。以下的圖解為本人總結(jié),算比較全地對每個步驟的原理過程一目了然:

Java類加載過程.png

說明: 圖解左下角說的<clinit>()方法,概念上是一個方法塊,這個<clinit>(){……}方法塊在初始化過程中執(zhí)行,可以用下面代碼理解:

class Parent{
 public static int A=1; 
 static{                            
 A=2; 
 }
}
 ---相當于---->
class Parent{
 <clinit>(){
   public static int A=1;
   static{
     A=2;
   }
 }
}

相當于把靜態(tài)變量的賦值和靜態(tài)代碼塊等操作順序串連成一個方法。
注意:

  • 對于類,會生成<clinit>(){……}方法體:去包含靜態(tài)變量的賦值和靜態(tài)塊代碼
  • 而對于接口,也會生成<clinit>(){……}方法體:去初始化接口中的成員變量
  • 接口和類初始化過程的區(qū)別:類的初始化執(zhí)行之前要求父類全部都初始化完成了,但接口的初始化貌似對父接口的初始化不怎么感冒,也就是說,子接口初始化的時候并不要求其父接口也完成初始化,只有在真正使用到父接口的時候它才會被初始化(比如引用接口上的常量的時候啦)

五、簡單代碼示例說明


這里,用一個java代碼示例,來根據(jù)輸出得到的各個方法和塊的執(zhí)行順序,去更加形象地理解整個類的加載和運行過程:

public class Main {
  public static void main(String[] args){
      System.out.println("我是main方法,我輸出Super的類變量i:"+Sub.i);
      Sub sub  = new Sub();
  }
}
class Super{
  {
      System.out.println("我是Super成員塊");
  }
  public Super(){
      System.out.println("我是Super構(gòu)造方法");
  }
  {
      int j = 123;
      System.out.println("我是Super成員塊中的變量j:"+j);
  }
  static{
      System.out.println("我是Super靜態(tài)塊");
      i = 123;
  }
  protected static int i = 1;
}
class Sub extends Super{
  static{
      System.out.println("我是Sub靜態(tài)塊");
  }
  public Sub(){
      System.out.println("我是Sub構(gòu)造方法");
  }
  {
      System.out.println("我是Sub成員塊");
  }
}

得到結(jié)果為:



說明:

  1. 對于同一個類:靜態(tài)代碼塊和靜態(tài)變量的賦值 是先于main方法的調(diào)用執(zhí)行的。
  2. 對于同一個類:靜態(tài)代碼塊和靜態(tài)變量的賦值是按順序執(zhí)行的。
  3. 子類調(diào)用父類的類變量成員,是不會觸發(fā)子類本身的初始化操作的【所以我們調(diào)用 Sub.i ,Sub.class并沒有被初始化和加載】。
  4. 使用new方式創(chuàng)建子類,對于類加載而言,是先加載父類、再加載子類(注意:此時由于父類已經(jīng)在前面初始化了一次,所以,這一步,就只有子類初始化,父類不會再進行初始化)
  5. 不論成員塊放在哪個位置,它都 先于 類構(gòu)造方法執(zhí)行。

參考文章


http://my.oschina.net/volador/blog/87194
http://lavasoft.blog.51cto.com/62575/184547/

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容