1. 程式人生 > >java類載入過程以及雙親委派機制

java類載入過程以及雙親委派機制

前言:最近兩個月公司實行了996上班制,加上了熬了兩個通宵上線,狀態很不好,頭疼、牙疼,一直沒有時間和精力寫部落格,也害怕在這樣的狀態下寫出來的東西出錯。為了不讓自己荒廢學習的勁頭和習慣,今天週日,也打算寫一篇部落格,就算是為了給自己以前立的flag(每個月必須寫幾篇部落格)的實現。那麼本次部落格的主題我選擇了java的類載入過程的探究以及雙親委派機制模型以及它被破壞的場景,搞清楚這個對於我們理解java的類載入過程以及面試中都是很有必要的。

本篇部落格的目錄

一:類載入器

二:類載入的過程和階段

三:雙親委派機制

四:雙親委派機制被破壞

正文

一:類載入器

1.1:類載入器的解釋

  類載入器是什麼?在平時的開發過程中,我們會定義各種不同的類,這些類最終都會被類載入載入到jvm中,然後再解析位元組碼執行。如果非得給類載入器一個定義,那麼它是這樣的:通過一個類的全限定名來獲取描述此類的而二進位制位元組流,這個動作是在java虛擬機器外部實現的,實現這個動作的程式碼模組稱為'類載入器';這句話乍聽有些抽象,其實不難理解。拿現實中的栗子來比擬的話,比如我們去用電腦光碟機放光碟這個過程:光碟就是我們寫的類,光碟機就是類載入器,只有通過光碟機載入之後,光碟上的內容才會被解析,我們才能在螢幕上看到光碟上放入的內容。另外,對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。這句話什麼意思呢?就是說如果兩個類在你寫的內容是一模一樣的,但是隻要他們是由不同的類載入器載入的,那麼這兩個類就是不同的!

二:類載入過程

類載入一共分為七個過程,他們的具體的順序是:載入->驗證->準備->解析->初始化,接下來我們來一一介紹這些過程:

2.1:載入

類載入過程中,虛擬機器需要完成以下三件事:

1)通過一個類的全限定名來獲取定義此類的二進位制位元組流

(2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構

(3)在記憶體中生成一個代表此類的java.lang.class的物件,作為方法區的這個類的訪問入口

      對於我們第一印象可能是二進位制位元組流是從class檔案中獲取的,但是其實並不是這樣。設計者在對類的位元組流獲取上並沒有做出明確的約束。一個類的全限定名並不一定是從class檔案中獲取的,而有可能是從jar、war、ear、網路中、執行時(比如動態代理、反射技術)、jsp、資料庫等,正是由於這樣的開放式設計,所以java才能在如此多的平臺上大放溢彩。換言之,如果java設定只有從class檔案中獲取的話,那麼java的使用場景就會大受限制,比如反射技術就無法實現,jsp就無法直接從servlet中獲取。當獲取類的二進位制位元組流後,虛擬就按照虛擬機器所需的格式儲存在方法區之中,然後在記憶體中例項化一個class物件,這個物件將作為程式訪問方法區的這些型別的外部入口。

2.2:驗證

2.2.1:檔案格式的驗證

該驗證階段主要是保證輸入的位元組流能正確的解析並存儲於方法區之內,格式上符合描述一個java型別資訊的要求,主要的目的是保證輸入的位元組流能正確的解析並存儲於方法區之內,該階段的驗證主要基於二進位制位元組流進行的 ,主要包含以下的驗證:

①:是否以魔數開頭②:主、次版本號是否早當前虛擬機器的處理範圍之內③:常量池的常量中是否有不被支援的常量型別

③:指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量

④:class檔案中各個部分以及檔案本身是否有被刪除的活附加的其他資訊

2.2.2:元資料的驗

這個階段主要是保證位元組碼描述的資訊符合java語言規範,這個階段可能包含的驗證點如下:

①:這個類是否有父類 ②這個類的父類是否繼承了不允許被繼承的類(比如被final修飾的類)

②:如果這個類不是抽象類,是否實現了其父類或介面之中要求實現的所有方法

③:類中的欄位、方法是否與父類產生矛盾

該階段主要是對類的元資料資訊進行語義驗證,保證不存在不符合java語言負擔的元資料資訊

2.2.3:位元組碼驗證

①保證任意時刻的運算元棧的資料型別和指令程式碼序列都能配合工作,不會出現java型別的錯誤基本型別載入

②:控制跳轉,保證跳轉指令不會跳轉到方法體以外的位元組碼指令上

③:保證方法體重的型別轉換是有效的,比如在強制轉換的過程中,只能將父類物件轉換為子類物件,而不能將子類物件轉換為父類物件。比如(Person peson =(Person)method.getObject(String inputParam)),但是無法實現(Object obj =(Object)method.getPerson(String inputParam))這就是java中的強制型別的轉換過程控制發生在此時

2.2.4:符號引用的驗證

①:符號引用中通過字串描述的全限定名是否能找到對應的類

②:在指定類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位

③:符號引用中的類、欄位、方法中的訪問性(private、protected、public、default)是否可以被當前類訪問

這個階段如果找不到的類會丟擲java.lang.NosuchMethodError、java.lang.IllegalAcessError、java.lang.NoSuchFieldError等異常

2.3:準備

該階段會正式為類變數分配記憶體並設定類變數初始值的階段,這個階段只會初始化類變數(靜態欄位)而不會初始化例項變數。比如以一個欄位 public static Long value = 1235L;在例項化的過程中,初始化欄位的初始值是0而不是1235L,但是注意一點:對於常量類或者列舉,會例項化對應的值:比如public static final Integer num = 45; 那麼在準備階段,會將num直接初始化為45,而不是0

 2.4:解析

解析階段是將常量池中的符號引用替換為直接引用的過程,解析動作主要是針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號進行解析.當進行欄位解析的時候,首先會按照繼承關係從下往上遞迴搜尋各個介面和它的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。如果不是java.lang.object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果查詢到了與目標相匹配的欄位,則返回這個欄位的直接引用。

如果找不到,就會丟擲java.lang.NoSuchFieldError異常,如果查詢過程中會對這個欄位進行許可權驗證,如果發現不具備這個欄位的訪問許可權,將會丟擲java.lang.IiieagalAccessError異常!

2.5:初始化

準備階段,類變數(靜態欄位)已經賦值過一次系統要求的初始化值,二在初始化階段,就開始根據程式碼中指定的值去初始化變數或者其他資源,初始化階段是執行類構造器方法的過程。在初始化階段,會通過執行類構造器<clinit>()方法的過程,cliint()方法與構造方法還不是完全相同的,它不需要顯式的呼叫父類構造器,虛擬機器會保證子類的clinit()方法在執行前,父類的clinit()方法已經執行完畢,因此在虛擬機器中第一個被執行的clinit方法一定是java.lang.Object。

注意:clinit方法對於類或者介面來說都不是必須的,如果一個類沒有靜態語句塊,有沒有對變數的賦值操作,那麼編譯器可以不為這個類生成clinit方法

介面和類都有可能生成clinit()方法;虛擬機器會保證在多執行緒環境下,clinit方法也只會執行一次,而不會執行多次。

 

三:雙親委派機制

3.1:類載入器的分類

3.1.1:啟動類載入器

這個載入器主要負責將存放在<JAVA_HOME>的lib目錄下的,或者被--Xbootclasspath引數所指定的路徑中的,並且被虛擬機器識別的(比如rt.jar).名字不符合的類庫即使放在lib目錄下也會被載入。

3.1.2:擴充套件類載入器

這個載入器主要負責載入存放在<JAVA_HOME>/lib/ext目錄下的java類庫,或者而被java.ext.dirs系統變數所指定的路徑的所有類庫,開發者可以直接使用擴充套件類載入器

3.1.3:應用程式載入器

這個類載入器負責載入使用者類路徑上所指定的類庫,如果程式中沒有定義過自己的類載入器,那麼一般情況下這個就是程式中預設的類載入器。

3.2:雙親委派機制

指的是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,(每一個層次的類載入器都是如此)。只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己載入完成。

3.3:雙親委派機制的好處

3.3.1:java類隨著它的載入器一起具備了一種帶有優先層級的層次關係,維護基礎類環境的穩定和高效的運轉。例如類:java.lang.object,它存放在rt.jar中。如果沒有雙親委派機制,那麼如果程式設計師自定義了一個叫做java.lang.object的類,並且放在程式的classPath模型下,那麼系統將會出現多個不同的object類,java最基礎的行為也就無法得到保證,程式也會混亂一片。

3.3.2:雙親委派機制的實現

雙親委派機制的實現比較簡單,主要的原理就是在類載入過程中,首先檢查請求的類是否已經在被載入過了,如果沒有就呼叫父類的載入器進行載入,如果父類載入器為null(不存在),就預設使用啟動類載入器作為父類載入器,如果父類載入失敗,就會丟擲classNotFoundException類,再呼叫自己的findClass方法進行載入。

 

四:3次破壞雙親委派機制

4.1:第一次被破壞

第一次發生在jdk1.2釋出之前,由於雙親委派模型在jdk1.2之後才被引入,而類載入器和抽象類java.lang.ClassLoader則在jdk1.0時代已經存在,意思就是設計這個東西出來的時候1.0的jdk無法滿足雙親委派模型(當時也並沒有考慮到),那麼java的jdk設計者就為java.lang.classLoader添加了新的protected方法的findClass(),在1.0時代,classLoader只有一個loadClass()方法,而在1.2之後,findclass()方法的主要目的就是就是進行自身的類載入。

4.2:雙親委派模型的缺陷

雙親委派模型很好的解決了各個類載入的基礎類的統一問題,但是假如基礎類要回呼叫戶的程式碼怎麼辦呢?而在JNDI(Java Naming and Directory Interface,Java命名和目錄介面))服務中它的程式碼由啟動類載入器去載入,但JNDI的目的就是對資源進行集中管理與資源,它需要會呼叫由獨立廠商實現並部署在應用程式的classPath下的JNDI介面的提供者的程式碼,但是啟動類載入器又不認識這些程式碼,因此雙親委派此刻就無法完成了。

如何解決這個問題?

執行緒上下文類載入器,這個類載入器可以通過java.lang.Thread類的setContextClassLoader方法進行設定,如果建立執行緒時未設定,它將會從父執行緒中繼承一個。有了上下文類載入器,JNDI就可以通過父類載入器去請求子類載入器去完成類載入器的動作,這實際上已經違背了雙親委派模型的設計初衷,但是這也是無可奈何的事情。java中的涉及SPI的東西,比如JDBC、JAXB、JBI等載入工作都採用了這種方式!

4.3:程式碼熱替換、模組熱部署

為了達到java程式碼的熱更新替換技術,OSGI模型經過一系列角逐,最終成了行業的標準。它的實現模組化部署的時候直接阿靜一個程式模組(Bundle)連同類載入器一起換掉以實現程式碼的熱部署,在OSGI下,類載入器不再是雙親委派模型中的樹狀結構,而是進一步發展為更復雜的網狀結構,在OSGI的實際載入過程中,只有開頭符合雙親委派機制,其餘的類查詢都在平級的類載入器中進行載入。

 五:總結

  本篇部落格主要介紹了類載入機制和它的載入過程,以及對雙親委派機制對於java的基礎平臺的重大意義,如何理解類載入機制並實現在java開發平臺中類載入的過程對於我們實際的開發程式碼都是一門內功的修煉,只有修煉好了內功,才能在java程式設計的路上越走越遠。本篇部落格的設計的開發程式碼比較少,都是一些關於概念的理解。在開發過程中,我們也是不僅僅只注重寫程式碼,修煉內功也是必不可少的一部分。