Java類的生命週期,不懂這個都不好意思和別人說我是搞JAVA的

類生命週期

  • 加載(Loading)

    • 四種類加載器:

    • JAVA_HOME 目錄裏面的內容

    • 雙親委任

  • 連接(Linking)

    • 驗證階段

    • 準備階段

    • 解析階段

  • 初始化

    • 類初始化的七種觸發情況:

  • 卸載

JVM

每本Java入門書籍在介紹Java這門語言的時候都會提到Java跨平臺,“一次解釋,到處運行的特點“,功臣就是jvm(Java Virtual Machine,Java虛擬機)。

但是,如果將jvm只與Java語言綁定在一起,那麼理解就過於狹隘了,Java虛擬機發展到現在已經脫離了Java語言,形成了一套相對獨立,高性能的執行方案。

image

除了以上提到的幾種語言之外,scala,熱門的kotlin都可以運行在jvm上面。

jvm內存結構規範

先簡單看一下 JVM 內存結構,之後會詳細講解這一塊的具體存儲。

類生命週期

類從被加載到虛擬內存中開始,到卸載內存爲止,它的整個生命週期包括:

類從加載到卸載整個生命週期

小提示:

  1. 加載階段和連接階段有時候是交叉進行的,不需要等到完全加載結束。

  2. 解析階段有時候可以再初始化之後再做。Jvm僅僅規定了:如果某些字節碼使用了符號引用,那麼在執行這些字節碼之前,需要完成對這些符號引用的解析。

  3. 但是這些過程總的開始時間和完成時間都是上圖固定順序。

  4. 這裏的“加載階段”和我們常說的“類加載”是兩回事,“類加載”指的是虛線框中三部分加起來。

加載(Loading)

加載,是指查找字節流,並且據此創建類的過程。是類加載過程的一個階段。虛擬機需要在這個過程完成三件事情:

  • 通過一個類的全限定名來獲取此類的二進制字節流;

  • 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;

  • 在內存中生成一個代表這個類的java.lang.Class對象,作爲這個方法區這個類的各種數據的訪問入口。

類加載

從虛擬機的角度來說,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),該類加載器使用C++語言實現,屬於虛擬機自身的一部分。另外一種就是所有其它的類加載器,這些類加載器是由Java語言實現,獨立於JVM外部,並且全部繼承自抽象類java.lang.ClassLoader。

四種類加載器:

  1. 啓動(Bootstrap)類加載器

啓動類加載器負責加載最爲基礎、最爲重要的類。負責將 JAVA_HOME/lib 下面的類庫加載到內存中(比如rt.jar)。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啓動類加載器的引用,所以不允許直接通過引用進行操作。

注:啓動類加載器是由 C++ 實現的,沒有對應的 Java 對象,因此在 Java 中只能用 null 來指代。除了啓動類加載器之外,其他的類加載器都是 java.lang.ClassLoader 的子類,因此有對應的 Java 對象。這些類加載器需要先由另一個類加載器,比如說啓動類加載器,加載至 Java 虛擬機中,方能執行類加載。

  1. 標準擴展(Extension)類加載器

它負責加載相對次要、但又通用的類,負責將 JAVA_HOME/jre/lib/ext 或者由系統變量 java.ext.dirs指定位置中的類庫加載到內存中。

  1. 應用程序(Application)類加載器

它負責將系統類路徑(CLASSPATH) 中指定的類庫加載到內存中。由於這個類加載器是ClassLoader中的 getSystemClassLoader()方法的返回值,因此一般稱爲系統(System)加載器

  1. 自定義類加載器

除了由 Java 核心類庫提供的類加載器外,我們還可以加入自定義的類加載器,來實現特殊的加載方式。舉例來說,我們可以對 class 文件進行加密,加載時再利用自定義的類加載器對其解密。

JAVA_HOME 目錄裏面的內容

之所以寫這個是因爲平時開發中很少有人翻開這個文件夾來看,上面講到這個目錄順便帶着大家來看看。

JAVA_HOME/bin目錄放的很多命令
JAVA_HOME/lib目錄
JAVA_HOME/jre/lib目錄
JAVA_HOME/jre/lib/ext目錄

雙親委任

雙親委任工作流程

雙親委派機制的工作流程:

  1. 當前ClassLoader首先從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。

每個類加載器都有自己的加載緩存,當一個類被加載了以後就會放入緩存,等下次加載的時候就可以直接返回了。

  1. 當前classLoader的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用同樣的策略,首先查看自己的緩存,然後委託父類的父類去加載,一直到bootstrp ClassLoader.

  2. 當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。

爲什麼需要雙親委任安全機制?

  1. 直觀理解

試想一下黑客自定義一個 java.lang.String 類,該 String 類具有系統的 String 類一樣的功能,只是在某個函數稍作修改。這個函數經常使用,假如在這這個函數中植入一些“病毒代 碼”。並且通過自定義類加載器加入到 JVM 中。完了,程序涼涼,這是比較直觀的理解。

  1. 真實原因

要完全理解這個問題還需要引入一個概念,類的命名空間

類需要類的全限定名(類的全路徑)以及加載此類的ClassLoader來共同確定。也就是說即使兩個類的全限定名是相同的,但是因爲不同的ClassLoader加載了此類,那麼在JVM中它是不同的類。

比如上面說的,我們 JDK 本生提供的類庫,比如 string,hashmap,linkedlist 等等,這些類由bootstrp 類加載器加載了以後,無論你程序中有多少個類加載器,那麼這些類其實都是可以共享的,這樣就避免了不同的類加載器加載了同樣名字的不同類以後造成混亂。

概括:

  1. 檢查順序:自底向上

  2. 加載順序:自頂向下

連接(Linking)

驗證階段

當一個類被加載之後,必須要驗證一下這個類是否合法,比如這個類是不是符合字節碼的格式、變量與方法是不是有重複、數據類型是不是有效、繼承與實現是否合乎標準等等。

我們平常寫代碼很多時候第一步都是寫校驗,jvm也是這個思路,Java 編譯器生成的類文件必然滿足 Java 虛擬機的約束條件,但是爲了防止“解字節碼注入”。

驗證階段大致會完成下面四個階段的檢驗動作:

  • 文件格式驗證 (主要驗證是否符合Class文件格式規範,並且能被當前版本的虛擬機處理。)

基於二進制字節流進行驗證,只有通過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,所以後面的驗證階段全是基於方法區的存儲結構進行的,不會再直接操作字節流。

  • 元數據驗證(對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求)

如驗證這個類是否有父類(除了java.lang.Object是所有類的父類),如果這個類不是抽象類是否實現了父類或者接口中要求實現的所有方法等。

  • 字節碼驗證

  • 符號引用驗證(發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三階段解析階段中發生)

如驗證符號引用中通過字符串描述的全限定名是否能找到對應的類。

準備階段

就是爲類的靜態變量分配內存並設爲 jvm 默認的初值,而不是我們設置的,我們設置的會在後面一個階段“初始化”期間來做,對於非靜態的變量,則不會爲它們分配內存。

jvm默認的初值是這樣的:

基本類型(int、long、short、char、byte、boolean、float、double)的默認值爲0。其中boolean只有true,false兩種類型,對應到jvm值分別是數據1,0。

引用類型(對象,數組)的默認值爲null。

構造其他跟類層次相關的數據結構,比如說用來實現虛方法的動態綁定的方法表。

在 class 文件被加載至 Java虛擬機之前,這個類無法知道其他類及其方法、字段所對應的具體地址,甚至不知道自己方法、字段的地址。因此,每當需要引用這些成員時,Java 編譯器會生成一個符號引用。在運行階段,這個符號引用一般都能夠無歧義地定位到具體目標上(因爲驗證階段進行符號引用驗證了)。

例外:public static final int value=123,常量直接賦值爲設置的123.

解析階段

上面說到的“在運行階段,這個符號引用一般都能夠無歧義地定位到具體目標上”,就是在解析階段進行的符號解析。

這個階段目的正是將常量池中的符號引用轉換解析成爲實際引用。在解析階段,jvm會將所有的類或接口名、字段名、方法名轉換爲具體的內存地址,從而讓用到了別的類或者接口的類能找到和加載其他的類/接口。

如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那麼解析將觸發這個類的加載(但未必觸發這個類的鏈接以及初始化)

初始化

在 Java 代碼中,如果要初始化一個靜態字段,我們可以在聲明時直接賦值,也可以在靜態代碼塊中對其賦值。除了 final static 修飾的常量,直接賦值操作以及所有靜態代碼塊中的代碼,則會被 Java 編譯器置於同一方法中,並把它命名爲 < clinit >

類加載的最後一步是初始化,目的便是爲標記爲常量值的字段賦值,以及執行< clinit > 方法的過程。Java 虛擬機會通過加鎖來確保類的 < clinit > 方法僅被執行一次。

類初始化的七種觸發情況:

  1. 當虛擬機啓動時,初始化用戶指定的主類(main函數);

  2. 當遇到用以新建目標類實例的 new 指令時,初始化 new 指令的目標類;

  3. 當遇到調用靜態方法的指令時,初始化該靜態方法所在的類;

  4. 子類的初始化會觸發父類的初始化;

  5. 如果一個接口定義了 default 方法,那麼直接實現或者間接實現該接口的類的初始化,會觸發該接口的初始化;

  6. 使用反射 API 對某個類進行反射調用時,初始化這個類;

  7. 當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

設計模式中單例延遲加載,便是充分利用了這個特點。

卸載

那麼多的類,什麼時候卸載呢?關於卸載誰,滿足如下條件:

  1. 該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例;

  2. 加載該類的ClassLoader已經被回收;

  3. 該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。

關於什麼時候卸載,當以上條件都滿足了,垃圾回收時候回在方法區清空類信息進行卸載,英雄遲暮,這個類的一生也就走到了盡頭了。

下期預告:

Class類文件結構和字節碼指令

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章