若是說核心類庫的 API 比作數學公式的話,那麼 Java 虛擬機的知識就比如公式的推導過程java
類生命週期 web
每本Java入門書籍在介紹Java這門語言的時候都會提到Java跨平臺,「一次解釋,處處運行的特色「,功臣就是jvm(Java Virtual Machine,Java虛擬機)。設計模式
可是,若是將jvm只與Java語言綁定在一塊兒,那麼理解就過於狹隘了,Java虛擬機發展到如今已經脫離了Java語言,造成了一套相對獨立,高性能的執行方案。數組
除了以上提到的幾種語言以外,scala,熱門的kotlin均可以運行在jvm上面。緩存
先簡單看一下 JVM 內存結構,以後會詳細講解這一塊的具體存儲。安全
類從被加載到虛擬內存中開始,到卸載內存爲止,它的整個生命週期包括:數據結構
小提示:jvm
加載,是指查找字節流,而且據此建立類的過程。是類加載
過程的一個階段。 虛擬機須要在這個過程完成三件事情:編輯器
從虛擬機的角度來講,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),該類加載器使用C++語言實現,屬於虛擬機自身的一部分。另一種就是全部其它的類加載器,這些類加載器是由Java語言實現,獨立於JVM外部,而且所有繼承自抽象類java.lang.ClassLoader。函數
啓動類加載器負責加載最爲基礎、最爲重要的類。負責將 JAVA_HOME/lib 下面的類庫加載到內存中(好比rt.jar)。因爲引導類加載器涉及到虛擬機本地實現細節,開發者沒法直接獲取到啓動類加載器的引用,因此不容許直接經過引用進行操做。
注:啓動類加載器是由 C++ 實現的,沒有對應的 Java 對象,所以在 Java 中只能用 null 來指代。除了啓動類加載器以外,其餘的類加載器都是 java.lang.ClassLoader 的子類,所以有對應的 Java 對象。這些類加載器須要先由另外一個類加載器,好比說啓動類加載器,加載至 Java 虛擬機中,方能執行類加載。
它負責加載相對次要、但又通用的類,負責將 JAVA_HOME/jre/lib/ext 或者由系統變量 java.ext.dirs指定位置中的類庫加載到內存中。
它負責將系統類路徑(CLASSPATH) 中指定的類庫加載到內存中。因爲這個類加載器是ClassLoader中的 getSystemClassLoader()方法的返回值,所以通常稱爲系統(System)加載器
除了由 Java 核心類庫提供的類加載器外,咱們還能夠加入自定義的類加載器,來實現特殊的加載方式。舉例來講,咱們能夠對 class 文件進行加密,加載時再利用自定義的類加載器對其解密。
之因此寫這個是由於平時開發中不多有人翻開這個文件夾來看,上面講到這個目錄順便帶着你們來看看。
雙親委派機制的工做流程:
每一個類加載器都有本身的加載緩存,當一個類被加載了之後就會放入緩存,等下次加載的時候就能夠直接返回了。
當前classLoader的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用一樣的策略,首先查看本身的緩存,而後委託父類的父類去加載,一直到bootstrp ClassLoader.
當全部的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它本身的緩存中,以便下次有加載請求的時候直接返回。
試想一下黑客自定義一個 java.lang.String 類,該 String 類具備系統的 String 類同樣的功能,只是在某個函數稍做修改。 這個函數常用,假如在這這個函數中植入一些「病毒代 碼」。而且經過自定義類加載器加入到 JVM 中。完了,程序涼涼,這是比較直觀的理解。
要徹底理解這個問題還須要引入一個概念,類的命名空間
。
類須要類的全限定名(類的全路徑)以及加載此類的ClassLoader來共同肯定。也就是說即便兩個類的全限定名是相同的,可是由於不一樣的ClassLoader加載了此類,那麼在JVM中它是不一樣的類。
好比上面說的,咱們 JDK 本生提供的類庫,好比 string,hashmap,linkedlist 等等,這些類由bootstrp 類加載器加載了之後,不管你程序中有多少個類加載器,那麼這些類其實都是能夠共享的,這樣就避免了不一樣的類加載器加載了一樣名字的不一樣類之後形成混亂。
歸納:
當一個類被加載以後,必需要驗證一下這個類是否合法,好比這個類是否是符合字節碼的格式、變量與方法是否是有重複、數據類型是否是有效、繼承與實現是否合乎標準等等。
咱們日常寫代碼不少時候第一步都是寫校驗,jvm也是這個思路,Java 編譯器生成的類文件必然知足 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 > 方法僅被執行一次。
當虛擬機啓動時,初始化用戶指定的主類(main函數);
當遇到用以新建目標類實例的 new 指令時,初始化 new 指令的目標類;
當遇到調用靜態方法的指令時,初始化該靜態方法所在的類;
子類的初始化會觸發父類的初始化;
若是一個接口定義了 default 方法,那麼直接實現或者間接實現該接口的類的初始化,會觸發該接口的初始化;
使用反射 API 對某個類進行反射調用時,初始化這個類;
當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。
設計模式中單例延遲加載,即是充分利用了這個特色。
那麼多的類,什麼時候
卸載誰
呢?關於卸載誰,知足以下條件:
該類全部的實例都已經被回收,也就是java堆中不存在該類的任何實例;
加載該類的ClassLoader已經被回收;
該類對應的java.lang.Class對象沒有任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
關於何時卸載,當以上條件都知足了,垃圾回收時候回在方法區清空類信息進行卸載,英雄遲暮,這個類的一輩子也就走到了盡頭了。