1. 程式人生 > >關於Class物件、類載入機制、虛擬機器執行時記憶體佈局的全面解析和推測

關於Class物件、類載入機制、虛擬機器執行時記憶體佈局的全面解析和推測

簡介:

本文是對Java的類載入機制,Class物件,反射原理等相關概念的理解、驗證和Java虛擬機器中記憶體佈局的一些推測。本文重點講述瞭如何理解Class物件以及Class物件的作用。

歡迎探討,如有錯誤敬請指正

如需轉載,請註明出處 http://www.cnblogs.com/nullzx/


1. 類載入機制

當我們編寫好一個“.java”檔案,通過javac編譯器編譯後會形成一個“.class”檔案。當我們執行該檔案時,Java虛擬機器就通過類載入器(類載入器本質就是一段程式)把“.class”檔案載入到記憶體,在方法區形成該類各方法的程式碼段和描述該類細節資訊的常量池,同時在堆區形成一個表示該類的Class物件(一個java.lang.Class類的例項)。Class物件中儲存了指向了該類所有屬性和方法的詳細資訊的指標(同時,還儲存了指向該類的父類的Class物件的指標)。我們能夠通過Class物件直接建立該類的例項,並呼叫該類的所有方法,這就是我們所說的反射。

類載入器不僅僅可以載入本地檔案系統中的“.class”檔案,還可以通過各種形式進行載入,比如通過網路上的獲取的資料流作為 “.class”。

類載入器本質上實現一個解析的工作,把表示該類的位元組資料變成方法區中的位元組碼和並在堆區產生表示該類的Class物件。

 

1.1 類載入器(ClassLoader)的層次結構

Java預設提供的三個ClassLoader(JAVA_HOME表示JDK的安裝目錄)

BootStrapClassLoader:稱為啟動類載入器,是Java類載入層次中最頂層的類載入器,負責載入JAVA_HOME\jre\lib目錄下JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等。該載入器不是ClassLoader的子類,由C/C++語言實現其功能。

ExtensionClassLoader:稱為擴充套件類載入器,負責載入Java的擴充套件類庫,預設載入JAVA_HOME\jre\lib\ext目下的所有jar。它是ClassLoader的子類,由Java語言實現。

AppClassLoader:稱為應用程式類載入器,負責載入當前應用程式目錄下的所有jar和class檔案以及環境變數CLASSPATH指定的jar(即JAVA_HOME/lib/dt.jar和JAVA_HOME/lib/tools.jar)和第三方jar。AppClassLoader是ClassLoader的子類,由Java語言實現。

注意JDK中有兩個lib目錄,一個是JAVA_HOME/lib,另一個是JAVA_HOME/jre/lib。

在java中,還存在兩個概念,分別是系統類載入器和執行緒上下文類載入器,其實都是指是AppClassLoader載入器。

 

1.2 類載入器雙親委派模型

ClassLoader使用的是雙親委託來搜尋類。每個ClassLoader例項都有一個父類載入器的引用(不是繼承的關係,是一個包含的關係)。

AppClassLoader的父載入器是ExtensionClassLoader,而Extension ClassLoader的父載入器是BootstrapClassLoader,而Bootstrap ClassLoader是虛擬機器內建的類載入器,本身沒有父載入器。

image

(圖片來自於http://blog.csdn.net/u011080472/article/details/51332866

當一個ClassLoader物件需要載入某個類時,在它試圖親自搜尋某個類之前,先把這個任務委託給它的父類載入器,父類載入器繼續向上委託,直到BootstrapClassLoader類載入器為止。即,首先由最頂層的類載入器BootstrapClassLoader在指定目錄試圖載入目標類,如果沒載入到,則把任務回退給ExtensionClassLoader,讓它在指定目錄進行載入,如果它也沒載入到,則繼續回退給AppClassLoader 進行載入,以此類推。如果所有的載入器都沒有找到該類,則丟擲ClassNotFoundException異常。否則將這個找到的“*.class”檔案進行解析,最後返回表示該類的Class物件。

java程式碼中我們只能使用ExtensionClassLoader和AppClassLoader的例項,這兩種類載入器分別有且只有一個例項。我們無法通過任何方法建立這兩個類的額外的例項,可以理解為設計模式中的單例模式。

 

1.3 為什麼要使用雙親委託這種模型?

1)這樣可以避免重複載入,當父親已經載入了該類的時候,子類就沒有必要,也不應該再載入一次。

2)核心類通過Java自帶的載入器載入,可以確保這些類的位元組碼沒有被篡改,保證程式碼的安全性。

JVM在判定兩個Class物件是否相同時,不僅要滿足兩個類名相同,而且要滿足由同一個類載入器載入。只有兩者同時滿足的情況下,JVM才認為這兩個Class物件是相同的。

 

1.4 自定義類載入器

除了Java預設提供的三個類載入器之外,使用者還可以根據需要定義自已的類載入器,自定義的類載入器都必須繼承自java.lang.ClassLoader類。

 

既然JVM已經提供了預設的類載入器,為什麼還要定義自已的類載入器呢?

1)因為Java中提供的預設ClassLoader,只加載指定目錄下的jar和class,如果我們想載入其它位置的class檔案或jar時就需要定義自己的ClassLoader。

2)對於那些已經加密過的Class檔案,自定義ClassLoader可以在解析Class檔案前,進行解密操作。這樣相互配合的方式保證了程式碼的安全性。

 

1.5 自定義類載入器的步驟

主要分為兩步

1)繼承java.lang.ClassLoader

2)重寫父類的findClass方法

下面是API文件中給出的自定義載入器的實現模型

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class NetworkClassLoader extends ClassLoader {      String host;      int port;        public Class findClass(String name) {          byte [] b = loadClassData(name);          return defineClass(name, b, 0 , b.length);      }        private byte [] loadClassData(String name) {          // load the class data from the connection           . . .      } }

 

下面的程式碼是一個類載入器的具體實現。MyClassLoader類載入器主要載入任意指定目錄下的“*.class”檔案,而這個指定的目錄又不在環境變數ClassPath所表示的目錄中。

 

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package demo;   import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;   public class MyClassLoader extends ClassLoader {      private String path;        @Override      public Class<?> findClass(String name){          byte [] data = null ;          try {              data = loadClassData(path);          } catch (IOException e) {              e.printStackTrace();          }          return defineClass(name, data, 0 , data.length);      }            private byte [] loadClassData(String path) throws IOException{          File f = new File(path);          FileInputStream fis = new FileInputStream(f);          byte [] data = new byte [( int ) f.length()];          fis.read(data);          fis.close();          return data;      }            /*       * 定義了帶兩個引數的loadClass方法,為了多傳遞一個path引數       * 內部一定要呼叫父類的loadClass方法,因為該方法內實現了雙親委派模型      */      public Class<?> loadClass(String path, String name) throws ClassNotFoundException{          this .path = path;          return super .loadClass(name);      }            public static void main(String[] args) throws ClassNotFoundException{          MyClassLoader mcl = new MyClassLoader();          /*列印當前類載入器的父載入器*/          System.out.println(mcl.getParent());          System.out.println( "==========" );                    Class<?> cls1 = mcl.loadClass( "D:/使用者目錄/我的文件/Eclipse/Person.class"                  , "javalearning.Person" );                    System.out.println(cls1.getClassLoader());          System.out.println( "==========" );                    Class<?> cls2 = mcl.loadClass( null , "java.lang.Thread" );          System.out.println(cls2.getClassLoader());          System.out.println( "==========" );                } }

 

通過程式碼實現可以看出,自定義類載入器的核心精髓是呼叫ClassLoader類中的defineClass方法。

 

下面是執行結果

?
1 2 3 4 5 6 sun.misc.Launcher$AppClassLoader @4e0e2f2a ========== demo.MyClassLoader @2a139a55 ========== null ==========

 

從執行結果看出,MyClassLoader的父載入器是AppClassLoader(這是在ClassLoader的建構函式中預設的實現方式)。Person.Class由MyClassLoader載入(父類載入器都沒有載入成功),而當MyClassLoader載入String.class時,委託到BootstrapClassLoader載入,發現BootstrapClassLoader已載入完畢,結果null表示String類的載入器是BootstrapClassLoader。

 

Person類

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package javalearning; public class Person{      public int age;      public String name;            public Person(){          name = "zx" ;          age = 18 ;      }            @Override      public String toString(){          return name + " " + age;      } }

 

2. 談談java.lang.Class和java.lang.Object之間的悖論

通過java的語法學習,我們知道以下三點

1)java.lang.Class類繼承java.lang.Object類

2)按照語法規則,建立一個java.lang.Class物件必須先建立它的父類(java.lang.Object)的一個物件(準確的說是開闢一片記憶體區域作為Class物件,並它其中的一部分割槽域作為Object物件)

3)按照語法規則,建立一個類的物件,必須先存在表示該類的java.lang.Class物件

但是這三點又是矛盾的。這兩個物件的建立沒有辦法順序實現。所以不是先建立好一個,再建立另一個,而是通過自舉實現的。也就是說是通過自舉程式將兩個物件建立好,然後才進入java的執行環境。而自舉程式本身不是由Java語言實現的,而是由C和C++實現的。

所有的java.lang.Class物件的建立不是通過建構函式建立的,而是通過載入器生成的。每個類都有對應的用於反射該類的Class物件,每個類有且只對應一個Class物件。

每一個類都從Object類中繼承了一個getClass的例項方法,返回表示該類的Class物件。

 

在java的堆區中,有一個特殊的Class物件,即Class.class。Class.class物件有兩層含義。

第一,可以把它看成一個普通的物件一個屬於Class類的例項

第二,它又表示是Class類本身用於反射的物件所以該物件的getClass方法返回它本身)或者說表示Class類本身的Class物件。

我們不能通過Class.class的newInstance方法產生Class類的例項,如果這麼做,會丟擲異常。另一個方面,假設能夠產生這樣的物件,我們怎麼知道這個物件應該對應哪一個類呢?

 

3. 談談java.lang.Class和類載入器之間的悖論

類載入器也是一個類,也有對應的Class物件,但是Class物件又必須通過載入器的例項產生,顯然這兩點又是矛盾的。

三個預設的類載入器中ExtensionClassLoader和AppClassLoader是由java程式碼實現的,而BootstrapClassLoader是由C/C++實現的。也就是說BootstrapClassLoader沒有,不需要有,也不可能有對應Class物件。ExtensionClassLoader類的例項和它對應ExtensionClassLoader.class物件都是由BootstrapClassLoader一併載入建立完成。建立完成後,再由ExtensionClassLoader物件載入AppClassLoader.class。

 

4 java.lang.Class物件和物件的記憶體佈局

4.1 Class物件中到底存了什麼?

從已有資料來看,Class物件在不同的虛擬機器在實現上儲存的內容都不一致,但是理論上來講, Class物件內部一定儲存了方法區中該類的所有方法簽名,屬性簽名,和每個方法對應的位元組碼的地址。

 

4.2 例項和例項方法之間的關係?

obj.setName(“zhang san”)

在實際執行過程中等價於

setName(obj, “zhang san”)

也就是物件時作為引數傳遞到例項方法裡面的,物件本身不含指向該類方法的指標(Class物件並不含有Class類的方法的指標,但含有表示該類的所有方法的指標,可能有點繞,自己要理解一下)。方法的具體實現都位於方法區中相應的程式碼段中。當虛擬機器呼叫該方法時,只要將虛擬機器執行引擎的PC(程式計數器)指向該方法的地址,然後將例項存入該方法的棧幀中即可。通過例項直接呼叫方法時,實際上沒有,也沒有必要通過Class物件。

下面的示例表示了,鎖住Person.Class物件不能阻止其它執行緒的程式碼建立Person類的例項,並呼叫例項方法。

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package javalearning;   public class ClassLockTest {            public static class T1 extends Thread{          private Class<?> cls;          private boolean done;          public T1(Class<?> cls){              this .cls = cls;          }                    @Override          public  void run() {              synchronized (cls){                  while (!done){                                        }              }          }                    public void done(){              done = true ;          }      }            public static void main(String[] args) throws InterruptedException{          /*我們先讓執行緒t1鎖住Person.class物件,然後在主執行緒中建立該物件的例項,並呼叫toString方法*/          Class<?> cls = Person. class ;          T1 t1 = new T1(cls);          t1.start();                    while (!t1.isAlive()){              System.out.println( "t1 is not alive" );              Thread.sleep( 500 );          }                    Person p = new Person();          System.out.println(p);          t1.done();          System.out.println( "over" );      } }

 

執行結果

?
1 2 zx 18 over

 

4.3 Class物件有哪些功能?

1)反射(關於反射的使用會在後續部落格中講解)

2)多型的實現

我們通過以下程式碼來講解Class物件在多型中的應用

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package demo;   public class ClassObjectDemo1 {            /*定義兩個具有繼承關係的類,兩個類內部有同一個方法的不同實現*/      public static class Person{          public void speak(){              System.out.println( "i am a person" );          }      }            public static class Coder extends Person{          public void speak(){              System.out.println( "i am a coder" );          }      }            /*定義了一個靜態方法,靜態方法會呼叫對應型別的speak方法*/      public static void speakByType(Person p){          p.speak();      }            public static void main(String[] args) {          Person p0 = new Cod