<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
程式設計師編寫的Java源程式(.java檔案)在經過編譯器編譯之後被轉換成位元組程式碼(.class 檔案),類載入器將.class檔案中的二進位制資料讀入到記憶體中,將其放在方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。
類載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了存取方法區內的資料結構的介面。
以下是舉例說明類載入過程:
類的生命週期包括:載入、驗證、準備、解析、初始化、使用、解除安裝7個階段。其中載入、驗證、準備、初始化、解除安裝5個階段是按照這種順序按部就班的開始,而解析階段則不一定:某些情況下,可以在初始化之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結,其實就是多型),例如子類重寫父類別方法。
注意:這裡寫的是按部就班的開始,而不是按部就班地進行或完成,因為這些階段通常都是互相交叉混合式進行的,通常會在一個階段執行過程中呼叫、啟用另外一個階段。
載入階段會做3件事情:
此處第一點並沒指明要從哪裡獲取、怎樣獲取,因此這裡給開發人員預留了擴充套件空間。許多Java技術就建立在此基礎上,例如:
載入階段完成後,虛擬機器器外部的二進位制位元組流就按照虛擬機器器所需的格式儲存在方法區之中,而且在Java堆中也建立一個java.lang.Class類的物件,這樣便可以通過該物件存取方法區中的這些資料。
確保被載入的類的正確性,分為4個驗證階段:
驗證階段非常重要的,但不是必須的,它對程式執行期沒有影響,如果所參照的類經過反覆驗證,那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器器類載入的時間。
為類的靜態變數分配記憶體,並初始化預設值,這些記憶體是在方法區中分配,需要注意以下幾點:
解析階段是虛擬機器器將常數池內的符號參照替換為直接參照的過程,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符7類符號參照進行。符號參照就是一組符號來描述目標,可以是任何字面量。
直接參照就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制程式碼。
為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。初始化階段是執行類構造器<client>()
方法的過程。
<client>()
方法是由編譯器自動收集類中的所有類變數賦值動作和靜態語句static{}
塊中的語句合併產生的,編譯器收集的順序是由語句在原始檔出現的順序所決定的。靜態語句塊中只能存取到定義在靜態語句塊之前的變數,定義在之後的變數可以賦值,但不能存取。如下所示:
public class Test{ static{ i=0; System.out.print(i); } static int i=1; }
<clinit>()
方法與類建構函式不一樣,不需要顯示呼叫父類別建構函式,虛擬機器器會保證在子類的<clinit>()
方法執行之前,父類別的<clinit>()
方法已執行完畢。
由於父類別的<clinit>()
方法首先執行,意味著父類別中的靜態語句塊要優先於子類的變數賦值操作,如下所示,最終得出的值是2,而不是1。
public class TestClassLoader { public static int A = 1; static { A = 2; // System.out.println(A); } static class Sub extends TestClassLoader { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); } }
<clinit>()
方法對於類和介面來說,並不是必須的,若類沒有靜態語句塊,也沒有對變數賦值操作,則不會生成<clinit>()
方法。
介面與類不同的是,介面不需要先執行父類別的<clinit>()
方法,只有父介面定義的變數使用時,父介面才會被初始化。另外介面的實現類也不會先執行介面的<clinit>()
方法。
虛擬機器器保證當多執行緒去初始化類時,只會有一個執行緒去執行<clinit>()
方法,而其他執行緒則被阻塞。
<clinit>()
方法和<init>()
方法區別:
執行時機不同:init方法是物件構造器方法,在new一個物件並呼叫該物件的constructor方法時才會執行。clinit方法是類構造器方法,是在JVM載入期間的初始化階段才會呼叫。
執行目的不同:init是對非靜態變數解析初始化,而clinit是對靜態變數,靜態程式碼塊進行初始化。
在介紹雙親委派機制前,先來看下類載入器的層次關係圖,如下:
雙親委派機制是指如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。
為了更清楚的瞭解雙親委派機制,我們來看下jdk1.8原始碼java.lang.ClassLoader.loadClass()方法實現:
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } 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; } }
上面程式碼註釋寫的很清楚,首先呼叫findLoadedClass方法檢查是否已載入過這個類,如果沒有就呼叫parent的loadClass方法,從底層一級級往上。如果所有ClassLoader都沒有載入過這個類,就呼叫findClass方法查詢這個類,然後又從頂層逐級向下呼叫findClass方法,最終都沒找到就丟擲ClassNotFoundException。這樣設計的目的是保證安全性,防止系統類被偽造。
為了便於理解,以下是載入邏輯示意圖:
自定義類載入器通常有以下四種應用場景:
原始碼加密的本質是對位元組碼檔案進行操作。我們可以在打包的時候對class進行加密操作,然後在載入class檔案之前通過自定義classloader先進行解密操作,然後再按照標準的class檔案標準進行載入,這樣就完成了class檔案正常的載入。因此這個加密的jar包只有能夠實現解密方法的classloader才能正常載入。
我們常常遇到頭疼的事情就是jar包版本的依賴衝突,寫程式碼五分鐘,排包一整天。
舉個栗子:
工程裡面同時引入了 A、B 兩個 jar 包,以及 C 的 v0.1、v0.2 版本,v2 版本的 Log 類比 v1 版本新增了 error 方法,,打包的時候 maven 只能選擇 C 的一個版本,假設選擇了 v1 版本。到了執行的時候,預設情況下一個專案的所有類都是用同一個類載入器載入的,所以不管你依賴了多少個版本的 C,最終只會有一個版本的 C 被載入到 JVM 中。當 B 要去存取 Log.error,就會發現 Log 壓根就沒有 error 方法,然後就拋異常 java.lang.NoSuchMethodError。這就是類衝突的一個典型案例。
類隔離技術就是用來解決這個問題。讓不同模組的 jar 包用不同的類載入器載入。
JVM 提供了一種非常簡單有效的方式,我把它稱為類載入傳導規則:JVM 會選擇當前類的類載入器來載入所有該類的參照的類。例如我們定義了 TestA 和 TestB 兩個類,TestA 會參照 TestB,只要我們使用自定義的類載入器載入 TestA,那麼在執行時,當 TestA 呼叫到 TestB 的時候,TestB 也會被 JVM 使用 TestA 的類載入器載入。依此類推,只要是 TestA 及其參照類關聯的所有 jar 包的類都會被自定義類載入器載入。通過這種方式,我們只要讓模組的 main 方法類使用不同的類載入器載入,那麼每個模組的都會使用 main 方法類的類載入器載入的,這樣就能讓多個模組分別使用不同類載入器。這也是 OSGi 和 SofaArk 能夠實現類隔離的核心原理。
在應用執行的時升級軟體,無需重新啟動的方式有兩種,熱部署和熱載入。
對於Java應用程式來說,熱部署就是在伺服器執行時重新部署專案,熱載入即在執行時重新載入class,從而升級應用。
熱載入可以概括為在容器啟動的時候起一條後臺執行緒,定時的檢測類檔案的時間戳變化,如果類的時間戳變掉了,則將類重新載入。對比反射機制,反射是在執行時獲取類資訊,通過動態的呼叫來改變程式行為。而熱載入則是在執行時通過重新載入改變類資訊,直接改變程式行為。
熱部署原理類似,但它是直接重新載入整個應用,這種方式會釋放記憶體,比熱載入更加乾淨徹底,但同時也更費時間。
位元組碼檔案可以從資料庫、網路、移動裝置、甚至是電視機機上盒進行載入,可以與原始碼加密方式搭配使用。比如部分關鍵程式碼可以通過移動U盤讀取再載入到JVM。
到此這篇關於深入瞭解Java中的類載入機制的文章就介紹到這了,更多相關Java類載入機制內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45