1. 程式人生 > >JVM執行與類載入

JVM執行與類載入

類載入機制

JVM把class檔案載入到記憶體,並對資料進行校驗、解析和初始化,最終形成JVM可以直接使用的Java型別的過程。

類載入過程分為三個步驟:裝載(Load),連結(Link)和初始化(Initialize)連結,如下圖所示:

載入:將class檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區的執行時資料結構,然後在堆中生成一個代表這個
類的java.lang.Class物件,作為方法區中類資料的訪問入口(即引用地址)。
所有需要訪問和使用類資料只能通過這個堆中的Class物件。這個載入的過程需要類載入器參與。

連結:將Java類的二進位制程式碼合併到JVM的執行狀態之中的過程。
  1)驗證:確保載入的類資訊符合JVM規範,沒有安全方面的問題
  2)準備:正式為類變數(static)分配記憶體並設定類變數預設初始值的階段,這些記憶體都將在方法區中進行分配
  3)解析:虛擬機器常量池內的符號引用(常量名)替換為直接引用(地址)的過程
初始化:
  1)執行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯期自動收集類中所有類變數的賦值動作和靜態程式碼塊中
的語句合併產生的。(類構造器是構造類資訊的,不是構造該類物件的構造器)
  2)當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化
  3)虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確加鎖和同步
public class TestClassLoading {
	public static void main(String[] args) {
		System.out.println(A.m);
	}
}
class A{
	static{
		m = 300;
	}
	static int m = 100;
}
//第二步:連結結束後m=0
//第三步:初始化後,m的值由<clinit>()方法執行決定
         這個A的類構造器<clinit>()方法由類變數的賦值和靜態程式碼塊中的語句按照順序合併產生,類似於
         <clinit>(){
             m = 300;
             m = 100;
         }
public class TestClassLoading {
	public static void main(String[] args) {
		System.out.println(A.m);
	}
	
	static{
		System.out.println("main在的類");//最先載入
	}

}
class Father{
	static{
		System.out.println("父類被載入");
	}
}
class A extends Father{
	static{
		System.out.println("子類被載入");
		m = 300;
	}
	static int m = 100;
}

從哪裡載入.class位元組碼

class位元組碼

1)從本地系統直接讀取.class檔案

2)通過網路下載.class檔案或資料

3)從zip,jar等歸檔檔案中載入.class檔案

4)從專有資料庫中提取.class資料

5)將Java原始檔資料上傳到伺服器中動態編譯為.class資料

什麼時候會發生類初始化?

類的主動引用(一定會發生類的初始化)
  當虛擬機器啟動,先初始化main方法所在的類
  new一個類的物件
  呼叫類的靜態成員(除了final常量)和靜態方法
  使用java.lang.reflect包的方法對類進行反射呼叫
  當初始化一個類,如果其父類沒有被初始化,則先會初始化他的父類
類的被動引用(不會發送類的初始化)
  當訪問一個靜態域時,只有真正宣告這個域的類才會被初始化
  當通過子類引用父類的靜態變數,不會導致子類初始化
  通過陣列定義類引用,不會觸發此類的初始化
  引用常量不會觸發此類的初始化(常量在連結階段就存入呼叫類的常量池中了)
package com.reflection.classloader;

public class TestClassLoading {
	public static void main(String[] args) throws Exception{
		//主動引用:一定會導致A和Father的初始化
//		A a = new A();
//		System.out.println(A.m);
//		Class.forName("com.reflection.classloader.A");
		
		//被動引用
//		A[] array = new A[5];//不會導致A和Father的初始化
//		System.out.println(A.b);//只會初始化Father
//		System.out.println(A.M);//不會導致A和Father的初始化		
	}
	
	static{
		System.out.println("main在的類");
	}

}
class Father{
	static int b = 2;
	static{
		System.out.println("父類被載入");
	}
}
class A extends Father{
	static{
		System.out.println("子類被載入");
		m = 300;
	}
	static int m = 100;
	static final int M = 1;
}

類載入器

類載入器的作用:

類載入的作用:將class檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區的執行時資料結構,然後在堆中生成一個
代表這個類的java.lang.Class物件,作為方法區中類資料的訪問入口

類快取:標準的JavaSE類載入器可以按要求查詢類,但一旦某個類被載入到類載入器中,它將維持載入(快取)一段時間。
不過JVM垃圾回收機制可以回收這些Class物件。

類載入器的層次結構(樹狀結構)

1.引導類載入器(bootstrap class loader):
   它用來載入Java的核心庫(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路徑下的內容),是用原生程式碼(C/C++)來實現的,並不繼承自java.lang.ClassLoder。
   載入擴充套件類和應用程式類載入器。並指定為他們的父類載入器。
2.擴充套件類載入器(extensions class loader)
   用來載入Java的擴充套件庫(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路徑下的內容)。Java虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入Java類
   由sun.misc.Launcher$ExtClassLoader實現
3.應用程式類載入器(application class loader)
   它根據Java應用的類路徑(classpath,java.class.path)的類
   一般來說,Java應用的類都是由它來完成載入。
   由sun.misc.Launcher$AppClassLoader實現
4.自定義類載入器
   開發人員可以通過繼承java.lang.ClassLoader類的方式實現自己的類載入器,以滿足一些特殊的需求

依次往下是父子關係,但這個父子關係不是通過繼承實現的,而是通過組合實現的。(好比親生與養父)
public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields must be added *after* it.
//代表父類載入器,其他欄位必須新增在它的後面
private final ClassLoader parent;//組合的方式說明其父類載入器

//....
}
	public static void main(String[] args) {
		ClassLoader c = ClassLoader.getSystemClassLoader();
		System.out.println(c);//[email protected]
		
		ClassLoader cf = c.getParent();
		System.out.println(cf);//[email protected]
		
		ClassLoader cff = cf.getParent();
		System.out.println(cff);//null  引導類載入器
	}

類載入器的代理模式

代理模式:交給其他類載入器來載入指定的類。
    雙親委託機制:就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次追溯,
直到最高的引導類載入器,如果父類載入器可以完成類載入任務,就成功返回,只有父類載入器無法完成此載入任務時,才自己去載入。
    雙親委託機制是為了保證Java核心庫的型別安全。例如,這種機制就保證不會出現使用者自己
能定義java.lang.Object類的情況。類載入器除了用於載入類,也是安全的最基本的屏障。
    雙親委託機制是代理模式的一種,並不是所有的類載入器都採用雙親委託機制。tomcat伺服器類載入器也是使用代理模式,所不
同的是它是首先嚐試去載入某個類,如果找不到在交給父類載入器。這與一般類載入器的順序是相反的。
驗證方法之一:
//自己寫一個java.lang.String
package java.lang;

public class String {
}
package com.reflection.classloader;

public class TestClassLoader {
	public static void main(String[] args) throws Exception {
		String str = new String();
		ClassLoader classLoader = str.getClass().getClassLoader();
		System.out.println(classLoader);//null  引導類載入器
		//說明還是載入的核心類庫中的java.lang.String,不是你自己寫的String
		System.out.println(str.getClass().getMethod("substring", int.class));
	}
}
//驗證方法二:
package java.lang;

public class String {
	public static void main(String[] args)throws Exception {
		String str = new String();
		//執行錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
		//	   public static void main(String[] args)
	}
}

自定義類載入器:

自定義類載入器的流程:
1.繼承java.lang.ClassLoader
2.首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經被裝載,直接返回;
3.委派類載入器請求給父類載入器,如果父類載入器能夠完成,則返回父類載入器載入的Class例項;
4.呼叫本類載入器的findClass(...)方法,試圖獲取對應的位元組碼,如果獲取的到,則呼叫defineClass(...)匯入型別到方法區;
如果獲取不到對應的位元組碼或其他原因失敗,則異常,終止載入過程
注意:被兩個類載入器載入的同一個類,JVM不認為是相同的類。
  1. 檔案類載入器
package com.reflection.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

//繼承java.lang.ClassLoader
public class FileSystemClassLoader extends ClassLoader{
	private String rootDir;
	public FileSystemClassLoader(String rootDir){
		this.rootDir = rootDir;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		//首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經被裝載,直接返回;
		Class<?> c = findLoadedClass(name);
		if(c ==null){
			//委派類載入器請求給父類載入器,如果父類載入器能夠完成,則返回父類載入器載入的Class例項;
			ClassLoader parent = this.getParent();
			try {
				c = parent.loadClass(name);//加異常處理,父類載入不到,然後自己載入
			} catch (Exception e) {
//				e.printStackTrace();
			}
			
			//呼叫本類載入器的findClass(...)方法,試圖獲取對應的位元組碼,如果獲取的到,則呼叫defineClass(...)匯入型別到方法區;
			//如果獲取不到對應的位元組碼或其他原因失敗,則異常,終止載入過程
			if(c == null){
				byte[] classData = getClassData(name);
				if(classData == null){
					throw new ClassNotFoundException();
				}else{
					c = defineClass(name, classData, 0, classData.length);
				}
			}
		}
		return c;
	}
	
	//把.class檔案的內容讀取到一個位元組陣列中
	//為什麼要讀取的位元組陣列中,因為protected final Class<?> defineClass(String name,byte[] b,int off,int len)
	private byte[] getClassData(String name) {
		String path = rootDir + File.separator + name.replace(".", File.separator)+".class";
		InputStream is = null;
		ByteArrayOutputStream baos = null;
		try {
			is = new FileInputStream(path);
			baos =new ByteArrayOutputStream(); 
			byte[] buffer = new byte[1024];
			int len;
			while((len = is.read(buffer))!=-1){
				baos.write(buffer, 0, len);
			}
			return baos.toByteArray();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}finally{
			try {
				if(is!=null){
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
	
}

package com.reflection.classloader;

public class TestFileSystemClassLoader {

	public static void main(String[] args) throws ClassNotFoundException {
		FileSystemClassLoader fsc = new FileSystemClassLoader("D:/atguigu/android1020/code/day9/bin");
		Class clazz = fsc.loadClass("com.atguigu.shortcut.TestShortCut");
		System.out.println(clazz);
		
		//同一個類,第二次載入直接返回上一次的Class物件
		Class clazz2 = fsc.loadClass("com.atguigu.shortcut.TestShortCut");
		System.out.println(clazz == clazz2);
		
		//注意:被兩個類載入器載入的同一個類,JVM不認為是相同的類。
		FileSystemClassLoader fsc2 = new FileSystemClassLoader("D:/atguigu/android1020/code/day9/bin");
		Class clazz3 = fsc2.loadClass("com.atguigu.shortcut.TestShortCut");
		System.out.println(clazz == clazz3);
		
		//也可以使用自定義類載入器載入系統類,但是實際上會委託給父類載入器
		Class c = fsc.loadClass("java.lang.String");
		System.out.println(c);
		System.out.println(c.getClassLoader());//委託給父類載入器...一直到引導類載入器
	}
}

2.網路類載入器

package com.reflection.classloader;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

//1、繼承java.lang.ClassLoader
public class NetClassLoader extends ClassLoader{
	private String rootUrl;
	public NetClassLoader(String rootUrl){
		this.rootUrl = rootUrl;
	}
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		//2、首先檢查請求的型別是否已經被這個類裝載器裝載到名稱空間中了,如果已經被裝載,直接返回;
		Class<?> c = findLoadedClass(name);
	
		if(c ==null){
			//3、委派類載入器請求給父類載入器,如果父類載入器能夠完成,則返回父類載入器載入的Class例項;
			ClassLoader parent = this.getParent();
			try {
				c = parent.loadClass(name);//加異常處理,父類載入不到,然後自己載入
			} catch (Exception e) {
//				e.printStackTrace();
			}
			
			//4、呼叫本類載入器的findClass(...)方法,試圖獲取對應的位元組碼,如果獲取的到,則呼叫defineClass(...)匯入型別到方法區;
			//如果獲取不到對應的位元組碼或其他原因失敗,則異常,終止載入過程
			if(c == null){
				byte[] classData = getClassData(name);
				if(classData == null){
					throw new ClassNotFoundException();
				}else{
					c = defineClass(name, classData, 0, classData.length);
				}
			}
		}
		return c;
	}
	
	//把.class檔案的內容讀取到一個位元組陣列中
	//為什麼要讀取的位元組陣列中,因為protected final Class<?> defineClass(String name,byte[] b,int off,int len)
	private byte[] getClassData(String name) {
		String path = rootUrl + "/" + name.replace(".", "/")+".class";
		InputStream is = null;
		ByteArrayOutputStream baos = null;
		try {
			
			URL url = new URL(path);
			is = url.openStream();
			baos =new ByteArrayOutputStream(); 
			byte[] buffer = new byte[1024];
			int len;
			while((len = is.read(buffer))!=-1){
				baos.write(buffer, 0, len);
			}
			return baos.toByteArray();
		} catch (IOException e) {
			e.printStackTrace();
		}finally{
			try {
				if(is!=null){
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
	
}