我們爲什麼要學習類加載機制?在實際編程中有什麼用?

說到類加載機制,又不得不提Java代碼執行過程,源碼(.java)文件被編譯成字節碼(.class)文件,再由Jvm進行後續處理。其實這個後續處理過程,就是JVM的類加載機制,簡單來說,就是把.class文件裝載到內存,進行校驗、解析、轉換和初始化,最終形成可以被虛擬機直接使用的Java類型。
這一個過程就是類加載的生命週期,類加載的生命週期總共分爲7個階段:

加載、驗證、準備、解析、初始化、使用和卸載。其中驗證、準備、解析三個步驟又可統稱爲連接。類加載機制包括了前五個階段,即加載、驗證、準備、解析、初始化。

1)加載(Loading)
從字面意思就可以理解,該步的主要目的就是將字節碼從不同的數據源(class文件、jar包、war包,甚至網絡、其它文件生成[比如將JSP文件轉換成對應的Class類])轉化爲二進制文件加載到內存中,並生成一個代表該類的java.lang.Class對象,作爲方法區這個類的各種數據的入口。

2)驗證(Verification)
這一階段的主要目的就是對二進制文件進行校驗,校驗是否符合Jvm字節碼規範,校驗是否會危害Jvm安全,簡單來說,就是做各種檢查,主要包括:

確保二進制字節流格式是否規範;

是否所有方法都遵守訪問控制關鍵字的限定;

方法調用的參數個數和類型是否正確;

確保變量在使用之前是否被正確初始化;

檢查變量是否被賦予恰當類型的值。

3)準備(Preparation)
JVM會在這個階段對類變量(靜態變量,即static修飾)分配內存並設置類變量的初始值(對應數據類型的默認初始值,如0、0L、null、false等),即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,比如一個類變量定義爲:

public int aa = 10;

public static int bb = 66;

public static final int cc = 88;

aa不會被分配內存,而bb會分配內存,但bb的初始值是0而不是66,需要注意的是,static修飾的量是類變量,也叫做靜態變量,而static final修飾的被稱作爲常量,所以cc的初始值是88。

4)解析(Resolution)
該階段將常量池中的符號引用轉化爲直接引用。

符號引用:就是class文件中的CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等類型的常量。

在編譯時,Java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如我們有一個com.test類引用了阿里巴巴的com.aliyun類,編譯時Test類並不知道阿里巴巴類的實際內存地址,因此只能使用符號com.aliyun。

直接引用:通過對符號引用進行解析,找到引用的實際內存地址。

5)初始化(Initialization)
初始化階段是執行類構造器方法的過程,即真正執行類中定義的Java程序代碼。

比如:String xiaoming = new String("need a Girlfriend");
使用new實例化一個String對象,此時就會調用String類的構造方法對xiaoming進行實例化。

說完類加載過程,就不得不說類加載器,什麼是類加載器?

還記得使用JdbcTemplate連接數據庫過程嗎?我們把數據庫連接封裝爲工具類DruidUtils方法,該方法就用到了類加載器:

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

/**
 * 提供連接
 */
public class DruidUtils {

	private static DataSource dataSource = null;

	static { // 必須優先執行 只執行一次

		try {
			// 需要一個文件流
			InputStream is = DruidUtils.class.getClassLoader().getResourceAsStream("db.properties");
			// 創建配置文件對象
			Properties props = new Properties();
			props.load(is);
			// 核心類
			dataSource = DruidDataSourceFactory.createDataSource(props);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 返回數據源方法
	 * 
	 * @return
	 */
	public static DataSource getDataSource() {
		return dataSource;
	}

	/**
	 * 提供連接的方法
	 * 
	 * @return
	 */
	public static Connection getConnection() throws SQLException {

		return dataSource.getConnection();
	}
}

其中,class.getClassLoader()就是類加載器的標誌了。Jvm設計之初就把加載動作放到外部實現,以便讓應用程序決定如何獲取所需的類,所以提供了3種類加載器:
1)啓動類加載器(Bootstrap ClassLoader):加載jre/lib包下面的jar文件,比如說常見的rt.jar。
(java.time.*、java.util.*、java.nio.*、java.lang.*、java.text.*、java.sql.*、java.math.*等等都在rt.jar包下)

import java.net.URL;

/**
 * 該程序可以獲得根類加載器所加載的核心類庫,
 * 並會看到本機安裝的Java環境變量指定的jdk中提供的核心jar包路徑
 *
 */
public class ClassLoaderTest {

	public static void main(String[] args) {
		URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
		for (URL url : urls) {
			System.out.println(url.toExternalForm());
		}
	}
}

2)擴展類加載器(Extension or Ext ClassLoader):加載jre/lib/ext包下面的jar文件。由Java語言實現,父類加載器爲null。

3)應用類加載器(Application or App ClasLoader):根據程序的類路徑(classpath)來加載Java類。由Java語言實現,父類加載器爲ExtClassLoader。

類加載器加載Class大致要經過如下8個步驟:

1、檢測此Class是否曾載入過(緩衝區中是否有此Class),如果有直接進入第8步,否則進入第2步。
2、如果沒有父類加載器,則要麼Parent是根類加載器,要麼本身就是根類加載器,則跳到第4步,如果父類加載器存在,則進入第3步。
3、請求使用父類加載器去載入目標類,如果載入成功則跳至第8步,否則接着執行第5步。
4、請求使用根類加載器去載入目標類,如果載入成功則跳至第8步,否則跳至第7步。
5、當前類加載器嘗試尋找Class文件,如果找到則執行第6步,如果找不到則執行第7步。
6、從文件中載入Class,成功後跳至第8步。
7、拋出ClassNotFountException異常。
8、返回對應的java.lang.Class對象。

如果以上3種類加載器不能滿足要求,我們還可以自定義類加載器(繼承 java.lang.ClassLoader 類),它們的層級關係如圖:

這種層次關係就叫作雙親委派模型:如果一個類加載器收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,父類再委派父類,一直到最頂層的類加載器,因此所有的加載請求都應該傳送到啓動類加載器。只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class),子類加載器纔會嘗試自己去加載。

採用雙親委派的好處是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係,保證Java程序的穩定性。比如加載位於rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object 對象。

所以,如果兩個類的加載器不同,即使兩個類來源於同一個字節碼文件,那這兩個類就必定不相等——雙親委派模型能夠保證同一個類最終會被特定的類加載器加載。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章