1. 程式人生 > >【大神之路】熟悉而又陌生的JVM (叄)

【大神之路】熟悉而又陌生的JVM (叄)

類載入可以說是每個Java程式設計師必須要知道的一個機制,他不僅在面試中遇到,更多的,是在開發中可以靈活的排查一些錯誤以及方便的編寫自己的類載入器。

開局兩張圖,內容全靠編

類載入順序 類載入器模型-雙親委派模型

類載入機制

類載入分成5個主要部分。 階段:載入-驗證-準備-解析-初始化載入 編譯過的java檔案會變成class檔案,載入就是把class檔案讀取到記憶體中。 載入過程會分配記憶體,方法區的記憶體和堆中的記憶體。 載入會在方法區儲存具體的資料結構,堆中是對這個資料結構的封裝,是一個物件。 載入的時候是根據包名+類名進行查詢到具體的類。 類載入的來源可以有很多,本地,網上,jar中,zip中,資料庫中。

驗證

校驗位元組碼的正確性,主要分為以下幾個校驗: 1、檔案格式,表示是否是java的class檔案,版本是否正確。 2、元資料,是為了校驗位元組碼的正確性,就是是否是java語言規範,比如是否有父類。 3、位元組碼校驗,最複雜,是真正執行程式碼的,檢視程式碼是否可以正確執行。 4、符號引用,比如檢視這個類的所用到別的類的資料,是否是private型別的。

準備 為所有的靜態變數賦值一個預設值,public static int a = 1。 此時會將a賦值一個預設值,int的預設值是0。 為所有的常量賦值正確的值,public static final int a = 1. 此時會將a賦值1,因為是一個常量,後面不會修改。 準備工作也會為這些變數進行分配記憶體。

解析 將符號引用變成直接引用的過程。 符號引用:符號引用是一個規定死的一個符號,比如CONSTANT_Class_info這個就可以被所有的虛擬機器識別,但是並不能做些什麼,此時需要轉換成直接引用。 直接引用:這就是具體的對某個目標的指標,可以說是一個偏移量,反正就是可以明確的指出所指的目標在記憶體中哪個位置。

初始化 最後階段。 真正執行邏輯程式碼的地方。 賦值給變數真正的邏輯中的值。 初始化執行就是執行構造器的方法,目的就是給靜態變數賦值。 方法是編譯器在收集類中靜態成員時候,自動產生的。 所以說如果沒有靜態變數和靜態塊,可以不執行方法,都沒生產咋執行。 所以說static的宣告和使用是有順序的,因為編譯器收集的時候就是按照順序的。 以上幾個步驟可以先執行,但是初始化並不是跟著上面一起執行的。 類載入器先把class檔案載入然後準備解析等操作,就不管了,先不初始化。 那麼什麼時候初始化:

1、new。 2、反射。 3、序列化 4、呼叫一個類的靜態方法的時候。 5、呼叫靜態欄位,對靜態欄位操作的時候。 6、發現父類沒有被初始化的時候,去初始化父類。 7、JVM啟動的時候,main方法所在類。 那麼什麼時候不進行初始化: 1、定義物件陣列。 2、通過類名獲取Class物件。 3、Class.forName的時候,引數設定為false。 4、ClassLoader.loadClass方法。 等。

類載入器

三個機制:委派、可見性、單一性。
分類:Bootstrap,Extension,Application,User。
Bootstrap是最高階載入器,載入JRE/lib下的jar。
Extension是第二層載入器,載入JRE/lib/ext下的jar。
Application是應用載入器,載入應用程式的所依賴的jar。
User的是自定義的,載入實現ClassLoad的自己寫的載入器。

原理 原理就是拿三個機制,委派、可見性、單一性。 委派: 委派是交給父類進行載入,父類載入不了,子類才載入。 比如自定義一個載入器,然後指定父類,父類再去指定父類,直到最上面的Bootstrap都無法載入,然後一級一級向下走,到發起的類依然無法載入,會報錯。 可見性: 子類載入器可以看到父類載入的東西,但是父類卻看不到子類。 這樣的好處和雙親委派有內在聯絡。 單一性: 單一性是一個好東西,可以理解為一個類只能保證是一個載入器載入的。 判斷兩個類是否一樣,首先是判斷是否一個載入器,比如A類先讓Application載入了,又讓自定義載入了,那麼這兩個載入的A類是不相等的,因為所載入之後,在記憶體中分配的記憶體都是不一樣的。 父類載入過的類不能被子類進行載入,但是不確定哦。 熱部署什麼的,其實就是多個載入器進行不同的載入,達到使用最新的載入到的類。

雙親委派模型

何為委派,就是收到載入任務,自己先不動,交給父類。
何為雙親,因為載入器一共有四層,必然有多個父類。
那麼組成一個模型就是最底層的載入器依次交由上面的父類,父類載入不了,自己才載入。
可以看到好處:
	確定了一個類是同一個載入器進行載入的,而不是多個載入器,因為每次都會派給父類。
	每個載入器都在自己的範圍內查詢,不會亂。
	避免相同的類而使用不同的載入器。
	安全,因為一些類不能被重新實現,爺爺載入器載入過一次就不載入了,就達到安全了。
	還有一些好處可以自己去悟出來的。

破壞雙親委派模型

破壞雙親委派,就要自己實現類載入器,但是一般不要破壞。
要破壞的時候,比如熱部署,就是一個打破雙親委派的。

自定義類載入器

通過User.class.getClassLoader()可以得到User類載入的時候用的是哪個載入器。
User.class.getClassLoader().getParent()得到父載入器。這裡有個坑:噹噹前類是Ext的時候,父載入器是Bootstrap,那麼此時ext.parent得到的是一個null。null是對的。
因為父載入器並不一定是父類。Bootstrap載入器是C寫的,是虛擬機器內建的,當然獲取不到了。而ext和app都是具體的java類。
Bootstrap載入器載入之後會生成一個ext和app,並且把ext載入器當做app的父載入器,邏輯是這樣的。
自定義的載入器如果沒有定義parent,那麼他的parent就是AppClassLoader。

重要方法:loadClass(),findClass(),findLoadedClass(),defineClass();

loadClass():
	在ClassLoader類中,看原始碼可以得到,採用遞迴的形式執行父載入器的loadClass方法,如果都沒有載入到,那麼就要呼叫findClass方法,所以在自定義的時候我們一般只需要重寫findClass方法,因為前面的都是一樣的,找不到最終會呼叫findClass方法。
	
defineClass():
	是把位元組轉換成class物件。將二進位制內容轉換成class。
	
findLoadedClass():
	是在已經載入過得類中去找,看看有沒有載入過。返回已經載入的該類。
	
findClass():
	自定義查詢類檔案。
	自定義步驟:繼承ClassLoader,重寫findClass方法,呼叫defineClass方法。
	繼承ClassLoader不說了,ClassLoader是最上級的一個載入類。
	重寫findClass方法剛才說過了,因為loadClass方法在各種載入器都找不到的時候會呼叫findClass,既然是我們自己的檔案,所以最終還是會用到findClass這個方法的,所以只需要重寫這個即可。
	然後就開始讀取位元組碼,將讀取到的位元組碼給defineClass方法,這個方法會把位元組碼轉換成class物件,最終返回。
	自定義載入器是一個無底洞,用好了,很牛逼的,比如熱部署,等各種技術。

ClassLoader類

類載入器的繼承關係:
ClassLoader < SecureClassLoader < URLClassLoader < AppClassLoader...
AppClassLoader同級有很多個的。

如有錯誤,請多多指正,互相學習,互相進步。