1. 程式人生 > >深入理解Java虛擬機器-類載入連線和初始化解析

深入理解Java虛擬機器-類載入連線和初始化解析

不管學習什麼,我一直追求的是知其然,還要知其所以然,對真理的追求可以體現在方方面面。人生短短數十載,匆匆一世似煙雲,我認為,既然來了,就應該留下一些有意義的東西。本系列文章是結合張龍老師的《深入理解JVM》視訊做的一個筆記,其中將自己在學習過程中的實踐記錄、思考理解整合在了一起。希望在鞏固自己的知識時讓更多的朋友能夠通過我的整合文章少走一些彎路。文中不免會有錯誤之處,無論什麼東西,都應該帶著懷疑的眼光去看待,擁有自己的獨立思維是非常重要的一件事情,共勉。

方法論

每一個在學習的過程中都應該有一個方法論的存在,在學習之前我們要知道如何去學,學習本身也是一門功課,我們平時可以多多借鑑身邊一些優秀的人的方法論,這是一種非常好的方式。學習分為兩個方面,從人,從事。從事:只有自己親身經歷才能感悟到學習和理解相關技術所遵從的要點;從人: 這是一種更為高階的學習方式,我們應該吸收一些優秀的人身上的閃光點,就好比平時看書一樣,這種方式成本是較低的,因為並不是我們親身經歷的,一定要有辨別能力,從其他人身上去借鑑出真正適合自己的閃光點。

當然從事情學習我們要付出的成本是較高的,俗話說不撞南牆不回頭,當我們真正的遇到挫折了,失敗了,踩到坑了,那麼你才會真正的發現自己的方式存在哪些問題,哪些弊端。從這個過程中去吸收一些經驗,從失敗的教訓當中獲得一些總結,接著指導著你未來在前進過程中學習的方向。
更為高效的方式還是從人這個角度來去學習,因為不同的人做事情的方式是不一樣的,那麼你要從不同的人身上去吸收優秀的閃光點,通過自己的一系列的加工,比如說做筆記,寫部落格等等一系列的輸出方式將別人所擁有的技能進而轉換為自己所擁有的技能,並且把它真正固化成自己的一部分。這個跟我們去看書,看一些別的教程視訊道理是一樣的,在學習的過程中我們一定要保證自己有輸入,當我們在看的時候就是一種輸入,但是要注意到有輸入就必須要有輸出,沒有輸出的話你的輸入效率非常非常低的,那什麼是輸出呢,做專案,記筆記,寫部落格都是一種很好的輸出方式。
那麼更為高層次的輸出方式的話就是你給別人去講,給別人分享你所掌握的這些個技能,這個對於個人來說的話是一個莫大的提升,我們可以在掌握學習一門技術之後一定要爭取一切機會去給你周圍的人去講,這是一個非常非常好的學習方式,因為你在講解的過程中你會遇到之前自己在輸入的時候遺漏或者說根本就沒有理解的這麼一些技術點,要想給別人講明白的首先一個前提是你自己得明白。換句話說就是你自己都不明白,你是不可能去給別人講明白的。但是反過來說你自己明白了,真的能給別人講明白的?其實也不一定。比如說在日常的工作學習中,別人去問你一些技術問題,可能這個問題你已經知道了,但是你跟別人去講,那麼講的過程中可能會出現三種情況:第一種就是這個技術你明白了,講完之後別人也明白了。這是最好的結果,說明你真正已經掌握了這門技術,第二種情況呢,就是這個技術你已經明白了,但是給別人講完之後別人還是不明白,那麼這說明什麼呢,說明你可能在兩個方面出現了偏差,第一是你對這個技術本身理解就出現了一些問題,你沒有把他給真正有效的輸出出來,第二個是你的掌握是沒有問題的,但是呢你的表達方式是有問題的,第三種就是這個技術你明白,講完之後呢你自己也不明白,然後呢對方也不明白,這是最差的一種結果,簡稱:裝逼失敗。那麼一旦出現了這種問題你一定要去反思你對這個技術的理解或者是說你對這個技術的認識是不是真的達到了理解的程度。因為什麼?那是因為當別人提出他自己對某個細節的觀點時,如果理解不透徹時你會產生困惑,不知道哪個觀點是正確的,和自己的理解不太一樣,可能就顛覆了自己對於某一個技術點或者某一個技術體系的認知。接下來就好好好反思一下自己的學習是不是高效的,你所花費的時候是不是真的值得了,哪些時間是不是真的產生的相應的價值,這個價值最終體現在我們是不是真正徹底理解了這門技術。如何讓一個小時的學習真的就產生一個小時的價值,其實是值得我們每一個人去仔細思考的問題。
很多人都容易出現的一種問題就是學習一種技術,為什麼過了一段時間就忘了,很多細節當時理解的非常清楚透徹,但是過了一段時間就發現這些東西好像根本就沒有學過一樣,其實不光是你會產生這樣的疑問,我在剛開始學習的時候也和大家犯了同樣的錯誤:
這個問題我們可以從生物學的角度來分析一下,人的記憶分為瞬時記憶,短期記憶和長期記憶,當相同的訊息反覆地被輸送到海馬體之後,訊息就會不斷進入額葉,進而被長期儲存形成長期記憶。

事實上,對記憶內容的每次回憶都會重新啟動鞏固記憶的完整過程,其中包括為形成新的突觸終端而進行的蛋白質生成過程。一旦我們把顯性儲存的長期記憶送回工作記憶區,記憶內容就會再次變成短期記憶。當我們再次鞏固這些記憶的時候,它又會獲得一些新的神經連線這是一種新環境。約瑟夫.勒杜克斯解釋說:“恢復記憶的大腦不是那個形成初始記憶的大腦。為了讓老記憶能在當前大腦中生效,記憶必須及時更新。”生物記憶一直處於不斷更新的狀態。相形之下,儲存在計算機中的記憶內容是靜態的位元形式,你可以把這些位元資料從一個磁碟轉移到另一個磁碟上,只要你願意,轉移多少次都可以,這些內容永遠都會跟以前一樣無比精確。
提出記憶外包這個想法的那些人也把工作記憶和長期記憶混為一談了。當一個人無法在長期記憶區鞏固一個事實、一個想法或者一次經驗的時候,他是不會“釋放”大腦空間,用來執行其他功能的。工作記憶區容量有限,而長期記憶區則具有不受限制的伸縮彈性,因為大腦具有生髮、去除突觸終端,不斷調整神經連線強度的能力。二者因此形成鮮明對比。美國密蘇里大學記憶研究專家納爾遜.考恩(Nelson Cowan)寫道:“正常的人腦不會像計算機那樣,永遠不會出現個人經歷再也裝不進記憶中的情況,人腦不會被塞滿。”托克爾.科林博格表示:“長期記憶區能夠儲存的資訊量實質上是無限的。”此外,也有證據表明,隨著我們個人記憶內容的不斷增加,我們的大腦也會變得更加敏銳。臨床心理學家希拉.克羅威爾(Sheila Crowell)在《學習的神經生物學》(The Neurobiology of Learning)中解釋說:“記憶這項行為可以按照某種方式調整大腦,讓大腦今後更容易學會觀念和技能。”
在我們儲存新的長期記憶內容時,並不會抑制我們的腦力,相反還會提高腦力。記憶每增加一次,智力就會加強一些。網路為個人記憶提供了一個非常便利的補充,這種便利讓人難以抗拒。但是,當我們開始利用網路代替個人記憶,從而繞過鞏固記憶的內部過程時,我們就會面臨掏空大腦寶藏的風險。

  • 引用自《揭示短期記憶轉長期記憶分子機制》

當涉及長期記憶的儲存時,首先會在前額葉皮質形成一段靜默的拷貝;在海馬體的記憶痕跡被逐漸抹去的同時,這段記憶才被逐漸鞏固下來。至於鞏固長期記憶的因素是什麼,論文第一作者北村隆表示,這還需要進一步的研究才能確定。
鞏固記憶的另一個關鍵是前額葉皮質需要同時接收來自海馬體和杏仁核的資訊輸入。杏仁核是大腦的情緒中樞。當研究人員切斷其中任意一方的神經訊號輸入時(還是採用光控制技術),大腦皮層的記憶就無法鞏固下來。

  • 引用自《科學家剖開大腦深入研究記憶過程:操控人的記憶可行》

老生常談溫故而知新,要做到有輸入的同時有輸出,一定要把你學到的東西吐出來。將當時所理解的所有細節用筆記記下來,這樣即使以後忘記的時候看到筆記是也能迅速的記憶起相關知識點。感覺和聯想記憶差不多,當我們看到筆記時,總能聯想起當時的狀態,就好像找到了一把鑰匙,開啟記憶寶盒,雖然可能會是片段式記憶,但這就足夠了。如果沒有筆記,筆記記錄的並不詳細,就好像沒有這把鑰匙,那麼你可能怎麼也不會找到這片段的記憶,無法開啟記憶寶盒,最終這段資訊就會被大腦當作無用資訊而被抹去。

瞭解

大家都知道Java這門語言應用的是非常廣泛的,在語言程式設計的排行榜上Java與C總是不相上下的,Java一般來說都是佔據第一名的,與其說是Java這門語言設計的非常棒,那麼不如說呢是jvm這個平臺設計的非常好,為什麼這麼說呢,因為在jvm這個平臺上面它除了Java語言之外還可以執行其他基於jvm的語言。那麼java本身呢跟jvm之間呢並不是一種緊密的繫結關係,jvm上執行的是什麼?並不是java語言,而是所編譯好的class檔案。比如說位元組碼,任何一門語言只要能翻譯成jvm所能理解並且能夠執行的位元組碼,那麼這門語言就可以很好的在jvm平臺上去執行,去執行。這一點就體現出來jvm的強大所在。對於java來說他應該是jvm平臺所能支援的第一門語言。位元組碼本身是有一種規範的,這個規範定義好了位元組碼每一部分都是什麼樣的內容,jvm是可以讀懂的。一旦能夠理解並且確認這個位元組碼檔案是沒有任何問題的,他就可以將其載入進來並執行相關的指令。
jmap(命令列)、jvisualvm和jconsole是java自帶的jvm監控工具

類載入

在java程式碼中,型別的載入、連線與初始化過程都是在程式執行期間完成的。
這個型別指的是我們定義的class,我們定義的一個interface,列舉等等,注意這裡面不涉及物件的概念,也就是new class()。類載入是在建立物件之前,換句話說要想建立物件,一定得要有這個物件所屬的類,它的型別資訊。你才能根據這個型別資訊把這個物件在堆上面創建出來。型別在絕大多少情況下都是已經編寫好的,如JDK提供的Object類,還有一些可以在執行期間動態生成出來,在編譯之前是不存在的,最典型的就是動態代理,換句話說它是一種Runtime的概念,這一點和其他的很多編譯型語言存在一個明顯的區別,在很多語言處理過程中型別載入、連線與初始化特別是載入、連線是在編譯階段就完成了。但是java是在執行期間完成的,他為什麼要這樣去做?其實這樣做的目的是給程式的開發人員或者說一些比較有創意的開發人員提供很多很多的可能性,在程式執行期間採取一些特殊的方式把之前已經存在或者在執行期才生成出來的型別有機的裝配在一起。所以java本身是一門靜態語言,但是具有的很多特性是動態語言才擁有的
說完了概念我們再來聊聊類載入具體場景: 將已經編寫好編譯完成的類的class檔案從磁碟上面載入到記憶體,接下來是連線,連線其實要完成很多的處理過程,用比較簡潔的一句話描述就是將類與類之間的關係確定好,對位元組碼的校驗,因為位元組碼本身是可以人為生成和操縱的。校驗完之後可能還涉及到類與類之間的呼叫關係,比如說將類與類之間的符號引用換成直接引用
第三個階段叫初始化,我們對於型別裡面一些靜態的變數進行賦值。

類載入器深入剖析

Java裡面每一個型別比如說java.lang.String...最終的資料結構資訊都會進入jvm管理的記憶體當中,那麼是怎麼進入的呢?就是由類載入器去完成的,就是一個用於載入類的工具。也就是說每一個型別都是由類載入器載入到記憶體當中的

  • 在如下幾種情況下,Java虛擬機器將介紹生命週期
    • 顯式的執行了System.exit()方法
    • 程式正常執行結束
    • 程式在執行過程中遇到了異常沒有cache住被拋到了main方法main方法再往上拋程式就退出了或錯誤而異常終止
    • 由於作業系統出錯導致java虛擬機器程序終止

      類的載入、連線與初始化

  • 載入:查詢並載入類的二進位制資料
  • 連線
    • 驗證:確保被載入的類的正確性
    • 準備:為類的靜態變數(類名.變數)分配記憶體,並將其初始化為預設值(型別的預設值不是變數的預設值)
    • 解析:把類中的符號引用轉換為直接引用
  • 初始化:為類的靜態變數賦予正確的初始值
  • java程式對類的使用方式可分為兩種
    • 主動使用
    • 被動使用
  • 所以的Java虛擬機器實現必須在每個類或介面被Java程式"首次主動使用"時才初始化他們

    什麼是主動使用?

  • 建立類的例項 new一個class物件
  • 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
  • 呼叫類的靜態方法 第二種和第三種可以看作是一種情況,因為它們反映到位元組碼助記符上大體上是一樣的。訪問在JVM層面上位元組碼用的是getstatic指令,賦值寫入是putstatic,而呼叫類的靜態方式使用的是invokestatic
  • 反射(如Class.forName("com.a.b"))得到類的Class物件
  • 初始化一個類的子類 new一個父類的子類 父類會被初始化,以此類推 Object會被初始化
  • Java虛擬機器啟動時被標明為啟動類的類(Java a) 包含Main方法的類
  • JDK1.7開始提供的動態語言支援:Java.lang.invoke.MethodHandle例項的解析結果REF_getStatic,REF_putStatic,PEF_invokeStatic控制代碼對應的類沒有初始化,則初始化
    除了上述七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化,但是有可能載入或連線

    類載入解析

    類的載入指的是將類的.class檔案中的二進位制資料載入到記憶體裡,將其放在執行時資料區的方法區內(JDK1.8為元空間),然後在記憶體中建立一個Java.lang.Class物件(規範並未說明Class物件位於哪裡,HotSpot虛擬機器將其放在了方法區中)用來封裝類在方法區內的資料結構,一個類多個例項對應的Class物件只有一份,Class物件可以看作一面鏡子,它可以反射出class檔案在方法區裡所有的內容和結構。這就是反射的根源
  • 載入Class檔案的方式
    • 從本地系統中直接載入
    • 通過網路下載.class檔案
    • 從zip,jar等歸檔檔案中載入.class檔案 jar包
    • 從專有資料庫中提取.class檔案
    • 將Java原始檔動態編譯為.class檔案 動態代理 jsp編譯成為的class檔案

接下來我們來例項演示一下

package com.airsky.jvm.classloader;

public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(Child1.string);
    }
}
class Parent1{
    public static String string="Hello AirSky"; //使用static關鍵字定義一個靜態變數 值為Hello AirSky
    static { //定義一個靜態程式碼塊,在程式載入時會被執行
        System.out.println("父類初始化");
    }
}
class Child1 extends Parent1{
    static {
        System.out.println("子類初始化");
    }
}

來看一下輸出

可以發現子類是未被執行的,我們來看看程式到底做了什麼,通過Main函式列印輸出子類的str屬性,由於子類繼承了父類,所以他可以直接訪問到str屬性,再來做一個實驗

package com.airsky.jvm.classloader;

public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(Child1.string2); //呼叫子類的靜態變數
    }
}
class Parent1{
    public static String string="Hello AirSky"; //使用static關鍵字定義一個靜態變數 值為Hello AirSky
    static { //定義一個靜態程式碼塊,在程式載入時會被執行
        System.out.println("父類初始化");
    }
}
class Child1 extends Parent1{
    public static String string2="Hello World"; //在子類定義一個靜態變數
    static {
        System.out.println("子類初始化");
    }
}

這次我們在子類新增一個靜態變數,然後在Main函式中呼叫它,再來看看執行結果

我們發現這次父類和子類都被執行了 原因是什麼呢?原來對於靜態欄位來說,只有直接定義了該欄位的類才會被初始化,換句話說由於string是被父類定義的,我們訪問了父類的string屬性,這種情況稱為對Parent1的主動使用,因此父類被初始化了,雖然我們用了Child1的類名,但是卻沒有主動使用該類。第二種情況為什麼父類也被初始化了呢?我們再來看看主動使用中的一種情況就是初始化一個類的子類,所以父類也被初始化了 Java也聲明瞭當一個類在初始化時,要求其父類全部都已經初始化完畢,因為Java是單繼承的,父類還有父類的話,會引發鏈式初始化,一直進行到Object,並且初始化只進行一次,所以Object類是第一個被初始化