前言:最近兩個月公司實行了996上班制,加上了熬了兩個通宵上線,狀態很很差,頭疼、牙疼,一直沒有時間和精力寫博客,也懼怕在這樣的狀態下寫出來的東西出錯。爲了避免讓本身荒廢學習的勁頭和習慣,今天週日,也打算寫一篇博客,就算是爲了給本身之前立的flag(每月必須寫幾篇博客)的實現。那麼本次博客的主題我選擇了java的類加載過程的探究以及雙親委派機制模型以及它被破壞的場景,搞清楚這個對於咱們理解java的類加載過程以及面試中都是頗有必要的。java
本篇博客的目錄程序員
一:類加載器面試
二:類加載的過程和階段數據庫
三:雙親委派機制編程
四:雙親委派機制被破壞網絡
正文數據結構
一:類加載器多線程
1.1:類加載器的解釋jvm
類加載器是什麼?在平時的開發過程當中,咱們會定義各類不一樣的類,這些類最終都會被類加載加載到jvm中,而後再解析字節碼運行。若是非得給類加載器一個定義,那麼它是這樣的:經過一個類的全限定名來獲取描述此類的而二進制字節流,這個動做是在java虛擬機外部實現的,實現這個動做的代碼模塊稱爲'類加載器';這句話乍聽有些抽象,其實不難理解。拿現實中的栗子來比擬的話,好比咱們去用電腦光驅放光碟這個過程:光碟就是咱們寫的類,光驅就是類加載器,只有經過光驅加載以後,光碟上的內容纔會被解析,咱們才能在屏幕上看到光碟上放入的內容。另外,對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。這句話什麼意思呢?就是說若是兩個類在你寫的內容是如出一轍的,可是隻要他們是由不一樣的類加載器加載的,那麼這兩個類就是不一樣的!jsp
二:類加載過程
類加載一共分爲七個過程,他們的具體的順序是:加載->驗證->準備->解析->初始化,接下來咱們來一一介紹這些過程:
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編程的路上越走越遠。本篇博客的設計的開發代碼比較少,都是一些關於概念的理解。在開發過程當中,咱們也是不只僅只注重寫代碼,修煉內功也是必不可少的一部分。