關於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文檔中給出的自定義加載器的實現模型

     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所表示的目錄中。

 

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方法。

 

下面是運行結果

sun.misc.Launcher$AppClassLoader@4e0e2f2a
==========
demo.MyClassLoader@2a139a55
==========
null
==========

 

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

 

Person類

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類的實例,並調用實例方法。

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");
	}
}

 

運行結果

zx 18
over

 

4.3 Class對象有哪些功能?

1)反射(關於反射的使用會在后續博客中講解)

2)多態的實現

我們通過以下代碼來講解Class對象在多態中的應用

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 Coder();
		speakByType(coder);
		
		Person p1 = new Person();
		speakByType(person);
	}
}

 

運行結果

i am a coder
i am a person

 

我們定義的靜態方法speakByType,顯然編譯器在編譯這個方法的時候不能確定到底調用哪一個speak方法,需要依據對象的具體類型才能確定符號引用。

現在我們通過字節碼工具重點查看一下speakByType的字節碼

    public static void speakByType(demo.ClassObjectDemo1$Person p) {
        /* L20 */
        0 aload_0;                /* p */
        1 invokevirtual 16;       /* void speak() */
        /* L21 */
        4 return;
    }

 

我們發現里面出現了一條字節碼調用語句 invokevirtual方法。而invokevirtual指令在執行時,首先會找到當前對象的類的Class對象,然后通過該Class對象查找sepak方法,如果通過該Class對象查找到了簽名一致的的sepak方法就會調用它。每個Class對象都會持有表示父類的Class對象的引用(通過Class的getSuperClass方法獲取),Object.class除外,自己想想為啥?當在子類的Class對象中沒有找到簽名一致的speak方法時,就從其父類的Class對象中繼續查找簽名一致的speak方法。顯然如果還沒有找到,則會沿着有繼承關系的Class的路徑繼續向下查找,如果直到Object.class對象中還未找到就會拋出異常。

 

3)instace of和向上轉型

當我們判斷某個對象是否屬於某個類時,比如 a instance of A,顯然只要判斷

a.getClass() == A.class && a.getClass().classLoader() == A.classLoader()即可,如果不滿足就沿着getSuperClass的路徑繼續向下找,如果直到Object.class還不滿足條件就返回false。同理在運行時,我們還能依據Class對象判斷向上轉型是否正確。

 

4.4 Class.class對象存在的意義是什么?

我們通常不會通過Class.class對象來間接訪問forName方法和其它相應方法,而是直接使用該類的方法。所以一種可能的情況就是利用Class對象進行類型判斷,即判斷一個對象是不是Class對象還是普通對象(判斷obj.getClass() == Class.class是否成立)。另一種可能就是保持概念的完整性,每一個類都有一個Class對象與之對應。

 

4.5 假設B類繼承了A類,那么B類的實例在內存中應該是什么樣子的?

image

 

java語言的設計者考慮到對象向上轉型等問題,每一個類的數據成員顯然要按照繼承關系的先后順序排列,同時考慮執行效率,還存在數據對齊等問題。

 

4.6 Java在運行時的內存布局

用一個例子來看看javaVM運行時,類、對象、Class對象、ClassLoader的關系

package demo;

import java.lang.reflect.InvocationTargetException;


public class ClassObjectDemo0 {
	
	/*定義兩個具有繼承關系的類,兩個類內部都為空*/
	public static class Person{
		
	}
	
	public static class Coder extends Person{
		
	}
	
	/*哈哈,main函數拋出的異常似乎有點多*/
	public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InterruptedException{
		
		/*通過對象找到該類的Class對象*/
		Coder coder = new Coder();
		System.out.println(coder.getClass());
		
		System.out.println("========================");
		
		/*通過Class對象可以看出繼承關系*/
		System.out.println(Coder.class);
		System.out.println(Coder.class.getSuperclass());
		System.out.println(Coder.class.getSuperclass().getSuperclass());
		
		System.out.println("========================");
		
		/*查看每個Class對象的類加載器*/
		System.out.println(Coder.class.getClassLoader());
		System.out.println(Person.class.getClassLoader());
		System.out.println(Object.class.getClassLoader());
		
		System.out.println("========================");
		
		/*每個Class對象都是Class類的實例*/
		System.out.println(Coder.class.getClass());
		System.out.println(Person.class.getClass());
		System.out.println(Object.class.getClass());
		
		System.out.println("========================");
		
		/*Class.class的getClass方法會返回它本身*/
		System.out.println(Class.class.getClass());

		/*產看Class對象的的父類*/
		System.out.println(Class.class.getSuperclass());
	}
}

 

運行結果

/*通過對象找到該類的Class對象*/
class demo.ClassObjectDemo$Coder
========================
/*通過Class對象可以看出繼承關系*/
class demo.ClassObjectDemo$Coder
class demo.ClassObjectDemo$Person
class java.lang.Object
========================
/*查看每個Class對象的類加載器*/
sun.misc.Launcher$AppClassLoader@4e0e2f2a
sun.misc.Launcher$AppClassLoader@4e0e2f2a
null /*說明Object類的加載器是BootstrapClassLoader*/
========================
/*每個Class對象都是Class類的實例*/
class java.lang.Class
class java.lang.Class
class java.lang.Class
========================
/*Class.class的getClass方法會返回它本身*/
class java.lang.Class
========================
/*查看Class.class對象的父類*/
class java.lang.Object

 

通過上面的結果,我們可以推測這些對象,方法,加載器等在堆和棧中布局的一種可能。

Class類的對象在內存的分布

在堆區中,我們一般的對象我們用淺黃色表示,Class對象用淺藍色表示(原諒我的辨色能力,什么顏色請自行體會)。

在堆區中,綠色箭頭表示getClass方法返回的對象。顯然,非Class對象的getClass方法返回這個類對應的Class對象。Class類繼承Object類,Object類定義了getClass方法,所有的Class對象也有getClass方法。如果把Class對象看成普通對象,那么它的getClass方法就會返回表示整個Class類的Class對象,即Class.class。而Class.class對象的getClass方法返回它本身。

堆區中,每個Class對象的黑色虛線都指向了方法區中表示該類的全部信息,所以我們能夠通過Class對象進行反射操作。

堆區中的黑色實線表示Class對象的getSupperClass方法返回的對象,由於所有的類都繼承於Object類,所以有的Class對象最終都指向於Object.clss,而Object.class沒有父類。

我們想要實現反射,一般使用Class.forName方法進行類加載,forName方法本質上就是調用ClassLoadr實例的loadClass方法。推測,為了方便每個加載器查找某個類是否已加載器過,每個類加載器可能都有一張表,記錄每個已加載的類和對應Class對象的地址。

 

4.7 數組與Class對象

不同數據類型,不同維度的數組都對應不同的Class對象

所有具有相同元素類型和維數的數組都共享該Class對象。

package javalearning;

public class ArrayTest {
	public static void main(String[] args){
		int[] a1 = new int[10];
		int[][] a2 = new int[5][3];
		Class<?> c1 = a1.getClass();
		Class<?> c2 = a2.getClass();
		System.out.println(c1 == c2);/*結果false*/
		System.out.println(c2.getComponentType() == c1);/*結果true*/
	}
}

 

由於數組沒有構造函數,我們也就沒有辦法通過它的Class對象直接創建數組對象。為了實現這個功能,JDK中就提供了Array類(java.lang.reflect.Array)來彌補這個缺陷。有關Array類的功能和使用,請參考Java的API文檔,注意區分java.util.Arrays類。

 

5 參考內容

[1]. classpath、path、JAVA_HOME的作用及JAVA環境變量配置

[2]. 深入分析Java ClassLoader原理

[3]. Java魔法堂:類加載器入了個門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM