說到類加載機制,又不得不提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 對象。
所以,如果兩個類的加載器不同,即使兩個類來源於同一個字節碼文件,那這兩個類就必定不相等——雙親委派模型能夠保證同一個類最終會被特定的類加載器加載。