您好,登錄后才能下訂單哦!
本篇內容介紹了“Java類加載機制原理是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
在具體介紹類加載機制之前,我們先來看下網上關于理解類加載機制的經典題目:
public class Singleton { private static Singleton singleton = new Singleton(); public static int counter1; public static int counter2 = 0; private Singleton() { counter1++; counter2++; } public static Singleton getSingleton() { return singleton; } } // 打印靜態屬性的值 public class TestSingleton{ public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); System.out.println("counter1=" + singleton.counter1); System.out.println("counter2=" + singleton.counter2); } } // 輸出結果: >>> counter1=1 >>> counter2=0
關于為什么counter2=0
,這里就不具體解釋了,只是想說下它考核了那幾個點:
類加載過程的5個階段先后順序:準備階段在初始化之前
準備階段和初始化階段各自做的事情
靜態初始化的細節:先后順序
言歸正傳,我們先從類加載的定義說起,一句話概述,
虛擬機將class文件中的二進制數據流加載到JVM運行時數據區的方法區內,并進行驗證、準備、解析和初始化等動作后,在內存中創建java.lang.class對象,作為對方法區中該類數據結構的訪問入口。
這里有幾點要解釋下,class文件是指符合class文件格式的二進制數據流,也就是我們常說的字節碼文件,它是我們與JVM約定的格式協議,只要是符合class文件格式的二進制流,都可被JVM加載,這也是JVM跨平臺的基礎;另外,java.lang.class對象只是說在內存創建,并沒有明確規定是否在Java堆中,對于Hotspot虛擬機,是存放在方法區的。
類的加載方式分為兩種:隱式加載和顯式加載。
實際就是不用我們代碼主動聲明,而是JVM在適當的時機自動加載類。比如主動引用某個類時,會自動觸發類加載和初始化階段。
則通常是指通過代碼的方式顯式加載指定類,常見以下幾種:
通過
Class.forName()
加載指定類。對于forName(String className)
方法,默認會執行靜態初始化,但如果使用另一個重載函數forName(String name, boolean initialize, ClassLoader loader)
,實際上是可以通過initialize
來控制是否執行靜態初始化通過
ClassLoader.loadClass()
加載指定類,這種方式僅僅是將.class加載到JVM,并不會執行靜態初始化塊,這個等后面談到類加載器的職責時會再強調這一點
關于Class.forName()
是否執行靜態初始化,通過源碼就能一目了然:
public static Class<?> forName(String className) // 執行初始化,因為initialize為true throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); } ... public static Class<?> forName(String name, boolean initialize, ClassLoader loader) // 可控的,通過initialize來指定初始化與否 throws ClassNotFoundException { ... return forName0(name, initialize, loader, caller); }
類加載的第一個階段——加載階段具體什么時候開始,虛擬機規范并未指明,由具體的虛擬機實現決定,可分為預加載和運行時加載兩種時機:
預加載:對于JDK中的常用基礎庫——JAVA_HOME/lib
下的rt.jar,它包含了我們最常用的class,如java.lang.*
、java.util.*
等,在虛擬機啟動時會提前加載,這樣用到時就省去了加載耗時,能加快訪問速度。
運行時加載:大多數類比如用戶代碼,都是在類第一次被使用時才加載的,也就是常說的惰性加載,這么做的比較直觀的原因大概是節省內存吧。
先上代碼(JDK1.7源碼):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
loadClass
是類加載機制中最核心的代碼,這段代碼基本闡述了以下最核心的兩點:
findLoadedClass(name)
,首先第一步就檢查這個name
表示的類是否已被某個類加載器實例加載過,若已加載,則直接返回已加載的c
,否則才繼續下面的委派等邏輯。即JVM層會對已加載的類進行緩存,那具體是怎么緩存的的呢?在JVM實現中,有個類似于HashTable
的數據結構叫做SystemDictionary,對已加載的類信息進行緩存,緩存的key
是類加載器實例+類的全限定名,value
則是指向表示類信息的klass數據結構。這也是為什么我們常說,JVM中加載類的類加載器實例加上類的全限定名,這兩者一起才能唯一確定一個類。每個類加載器實例就相當于是一個獨立的類命名空間,對于兩個不同類加載器實例加載的類,即便名稱相同,也是兩個完全不同的類。
對于新加載的類,緩存沒命中后走雙親委派邏輯——當parent
存在時,會先委派給parent
進行loadClass
,然后parent.loadClass
內部又會進行同樣的向上委派,直至parent
為null
,委派給根加載器。也就是說委派請求會一直向上傳遞,直到頂層的引導類加載器,然后再統一用ClassNotFoundException異常的方式逐層向下回傳,直到某一層classLoader
在其搜索范圍內找到并加載該類;當parent
不存在時,即沒有父類加載器,此時直接委派給頂層加載器——BootstrapClassLoader
。
從這里可以看到雙親委派結構中,類加載器之間的父子層級關系并不是通過繼承來實現,而是通過組合的方式即子類加載器持有parent
代理以指向父類加載器來實現的。
由于委派是單向的,處于子類加載器層級的類,可以訪問父類加載器層級的類,反過來不行
各執其責,各層級的類加載器只負責加載本層級下的類。實現方式:各層級類加載器有自己的加載路徑,路徑隔離,互不可見。URLClassLoader的ucp
屬性了解下~
ClassNotFoundException——父子類加載器之間的協議。只有當父類加載器拋出此異常時,加載請求才會向下層傳遞,其他異常不認!
上下層的這種優先級進一步保證了Java程序的穩定性,對于JDK庫中核心的類不會因為用戶誤定義的同名類而導致被覆蓋。
還是給個定義吧:
通過一個類的全限定名來獲取描述此類的二進制字節流的代碼模塊
經典的三層加載器結構:
1、啟動類加載器(或稱為引導類加載器):只負責加載<JAVA_HOME>/lib
目錄中的,或是啟動參數-Xbootclasspath
所指定路徑中的特定名稱類庫。該加載器由C++實現,對Java程序不可見,對于自定義加載器,若是未指定parent,則會委派該加載器進行加載。
2、擴展類加載器:負責加載<JAVA_HOME>/lib/ext
目錄中的,或是java.ext.dirs
系統變量所指定的路徑下所有類庫。該加載器由sum.misc.Launcher$ExtClassLoader
實現,可直接使用。
3、應用程序類加載器(或稱為系統類加載器):負責加載用戶類路徑ClassPath
中所有類庫。該加載器由sum.misc.Launcher$AppClassLoader
實現,可由ClassLoader.getSystemClassLoader()
方法獲得。
ExtClassLoader
和AppClassLoader
都是繼承自URLClassLoader
,各自負責的加載路徑都是保存在ucp
屬性中,這個看源碼就能得知。
雙親委派并不是一個強制性約束模型,畢竟它自身也有局限性。無論是歷史代碼層面、SPI設計問題、還是新的熱部署需求,都不可避免地會違背該原則,累計有三次“破壞”。
通過ClassLoader的源碼可知,雙親委派的實現細節都在loadClass方法中,而該方法是一個protected的,意味著子類可以覆蓋該方法,從而可繞過雙親委派邏輯。雙親委派模型是在JDK1.2之后才被引入,在此之前的JDK1.0,已有部分用戶通過繼承ClassLoader重寫了loadClass邏輯,這使得后面引入的雙親委派邏輯在這些用戶程序中不起作用。
為了向前兼容,ClassLoader新增了findClass方法,提倡用戶將自己的類加載邏輯放入findClass中,而不要再去覆蓋loadClass方法。
雙親委派的層次優先級就決定了用戶代碼和JDK基礎類之間的不對等性,即只能用戶代碼調用基礎類,反之不行。對于SPI之類的設計,比如已經成為Java標準服務的JNDI,其接口代碼是在基礎類中,而具體的實現代碼則是在用戶Classpath下,在雙親委派的限制下,JNDI無法調用實現層代碼。
開個后門——引入線程上下文類加載器(Thread Context ClassLoader),該加載器可通過java.lang.Thread.setContextClassLoader()
進行設置,若創建線程時未設置,則從父線程繼承;若應用程序的全局范圍都未設置過,則默認設置為應用程序類加載器,這個可在Launcher的源碼中找到答案。
有了這個,JNDI服務就可使用該加載器去加載所需的SPI代碼。其他類似的SPI設計也是這種方式,如JDBC、JCE、JAXB、JBI等。
模塊化熱部署,在生產環境中顯得尤為有吸引力,就像我們的計算機外設一樣,不用重啟,可隨時更換鼠標、U盤等。 OSGi已經成為業界事實上的Java模塊化標準,此時類加載器不再是雙親委派中的樹狀層次,而是復雜的網狀結構。
通常Web服務器需要解決幾個基本問題:
同一個服務器上,部署兩個及以上的Web應用程序,各自使用的Java類庫可以相互隔離。
多個Web應用程序,共享所使用的部分Java類庫。比如都用到了同樣版本的spring,共享一份,無論是本地磁盤,還是Web服務器內存(主要是方法區),都是不錯的節省。
保證服務器自身安全不受部署的Web應用程序影響,這跟前面談到的雙親委派保證Java程序穩定性是一個道理。
支持JSP的話,需要支持HotSwap功能。
為了應對以上基本問題,主流的Java Web服務器都會提供多個Classpath存放類庫。對于Tomcat,其目錄結構劃分為以下4組:
/common
目錄,存放的類庫被Tomcat和所有的Web應用程序共享。
/server
目錄,僅被Tomcat使用,其他Web應用程序不可見。
/shared
目錄,可被所有Web應用程序共享,但對Tomcat不可見。
/WebApp/WEB-INF
目錄,僅被所屬的Web應用程序使用,對Tomcat和其他Web應用程序不可見。
跟以上目錄對應的,是Tomcat經典的雙親委派類加載器架構:
上圖中,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader分別負責/common/*
、/server/*
、/shared/*
和/WebApp/WEB-INF/*
目錄下的Java類庫加載,其中WebApp類加載器和Jsp類加載器通常會存在多個實例,每一個Web應用程序對應一個WebAppClassLoader,每一個JSP文件對應一個Jsp類加載器。
OSGi(Open Service Gateway Initiative)是OSGi聯盟制定的一個基于Java語言的動態模塊化規范,其最著名的應用案例就是Eclipse IDE,它是Eclipse強大插件體系的基礎。
OSGi中的每個模塊稱為Bundle,一個Bundle可以聲明它所依賴的Java Package(通過Import-Package描述),也可以聲明它允許導出發布的Java Package(通過Export-Package描述)。Bundle之間的依賴關系為平級依賴,Bundle類加載器之間只有規則,沒有固定的委派關系。假設存在BundleA、BundleB和BundleC,
BundleA:聲明發布了packageA,依賴了java.*的包 BundleB:聲明依賴了packageA和packageC,同時也依賴了java.*的包 BundleC:聲明發布了packageC,依賴了packageA
一個簡單的OSGi類加載器架構示例如下:
上圖的這種網狀架構帶來了更好的靈活性,但同時也可能產生許多新的隱患。比如Bundle之間的循環依賴,在高并發場景下導致加載死鎖。
本文以一個關于類加載的編程題為切入點,闡述了類加載階段的具體細節,包括加載方式、加載時機、加載原理,以及雙親委派的優劣點。并以具體的類加載器實例Tomcat和OSGi為例,簡單分析了類加載器在實踐過程中的多種選擇。
“Java類加載機制原理是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。