JVM裏面Java類的生命週期,一篇搞定

若是說核心類庫的 API 比作數學公式的話,那麼 Java 虛擬機的知識就比如公式的推導過程java

類生命週期 web

JVM

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

可是,若是將jvm只與Java語言綁定在一塊兒,那麼理解就過於狹隘了,Java虛擬機發展到如今已經脫離了Java語言,造成了一套相對獨立,高性能的執行方案。數組

image
image

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

jvm內存結構規範
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/bin目錄放的不少命令
JAVA_HOME/lib目錄
JAVA_HOME/lib目錄
JAVA_HOME/jre/lib目錄
JAVA_HOME/jre/lib目錄
JAVA_HOME/jre/lib/ext目錄
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對象沒有任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

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

相關文章
相關標籤/搜索