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編程的路上越走越遠。本篇博客的設計的開發代碼比較少,都是一些關於概念的理解。在開發過程中,我們也是不僅僅只注重寫代碼,修煉內功也是必不可少的一部分。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM