Java內存模型和類加載過程

參考文章:html

http://gityuan.com/2016/01/09/java-memory/ (推薦牛人的博客)java

https://www.guru99.com/java-stack-heap.htmlgit

https://www.zhihu.com/question/21539353bootstrap

http://www.javashuo.com/article/p-wkgtrwav-gc.html多線程

深刻Java虛擬機函數

Java虛擬機在執行java程序的過程當中會把其所管理的內存區域劃分爲若干個不一樣的數據區域。 詳情以下圖所示:操作系統

程序計數器

程序計數器是一塊較小的空間,能夠當作當前線程所執行的字節碼的行號指示器,在虛擬機的概念模型中,字節碼解釋器工做經過改變程序計數器的值來選取下一條須要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能須要依賴這個計數器完成。.net

因爲虛擬機的多線程是經過線程輪流切換分配處理器執行時間的方式來實現的,因此任何一個時刻,一個處理器都會執行一條線程中的指令。所以,爲了切換線程能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器,各個線程之間互不影響,獨立存儲,是一個線程私有的內存。插件

若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是執行Natvie方法,這個計數器值則爲空。線程

這個內存區域是惟一一個在Java虛擬機規範中沒有規定任何Out Of Memory Erorr狀況的區域。

Java棧

Java棧,即Java虛擬機棧是線程私有的,他的生命週期和線程相同。Java棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表,操做數棧,動態連接,方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程.

局部變量表:存放了編譯器可知的各類基本數據類型,對象引用類型(reference類型,不一樣於對象自己,他多是一個指向對象起始地址的引用指針,也多是指向一個表明對象句柄或者其餘於此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。

局部變量表的所需內存空間在編譯期間完成分配,當進入一個方法時候,這個方法所須要分配的局部變量空間是徹底肯定的,方法運行時不會改變局部變量表的大小。

Java棧是爲虛擬機執行Java方法(即字節碼)服務。

本地方法棧

本地方法棧執行的是Native方法服務

Java堆

Java堆是被全部線程共享的一塊內存區域。 堆的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。 Java堆也是垃圾收集器管理的主要區域,即"GC堆"。

Java堆能夠出於物理上不連續的內存空間中,邏輯上連續便可。

方法區

方法區與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。

存儲內容:類中的靜態常量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息

運行時常量池 是方法區的一部分,Class文件中除了有類的版本,字段,方法,接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。

運行時常量池相對於Class常量池來講,更具備動態性,Java語言不要求常量必定只有在編譯期才能產生,也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入常量池,如String.intern()方法。

關於各個部分的聯繫,能夠查看下圖(雖然這張圖講的是GC Root的,可是我以爲對於理解JMM挺有用的):

圖片來源,侵刪


類加載的過程

這裏首先總結一下對象的建立過程:

  1. 虛擬機在遇到一條new指令的時候,首先會去檢查這個指令的參數是否能在常量池(運行時常量池)中定位到一個類的符號引用,並去檢查這個符號引用表明的類是否已被加載,解析和初始化過。若是沒有,則執行類加載過程。
  2. 經過類加載檢查後,虛擬機將爲新生的對象分配內存。對象所需的內存大小在類加載完成後即可以徹底肯定。而後虛擬機對對象進行一些必要的設置(如對象是哪一個類的實例,對象的哈希碼,對象的GC分代年齡等等)
  3. 上述工做完成以後,從虛擬機視角來看,一個新的對象已經產生,但從Java程序視角來看的話,對象建立剛剛開始,<init>方法尚未執行,全部的字段都爲零。因此通常而言,執行完new指令事後,會接着執行<init>方法,把對象按照咱們的意願進行初始化,這樣纔算一個真正可用的對象徹底產生出來。
類加載過程

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這一個過程爲類加載機制。

類從被加載到虛擬機內存開始,到卸載出內存爲止,他的整個生命週期包括以下階段:

加載,驗證,準備,初始化和卸載階段的順序是肯定的,而解析階段則不必定了,他在某些狀況下可在初始化完成後在開始,這是爲了支持Java語言的運行時綁定

關於何時開始加載的過程,Java虛擬機規範中沒有進行強制約束,可由虛擬機自由把握。可是對於初始化階段,虛擬機嚴格規定了有且只有5種狀況必須當即對類進行"初始化"(加載,驗證,準備天然須要在此以前開始)。

  • 遇到new,getstatic,putstatic,invokestatic四條字節碼指令時候,若是類沒有初始化,則先觸發初始化操做,生成該四條指令的最多見狀況是:
  1. 使用new關鍵字實例化對象。
  2. 讀取一個類的靜態字段時候。
  3. 獲取一個類的靜態字段時候。
  4. 調用一個類的靜態方法的時候。
  • 使用反射調用時候,若是類沒有初始化,則先觸發初始化操做。
  • 初始化一個類時候,若是發現他的父類還沒初始化,則先觸發父類的初始化。
  • 虛擬機啓動時候,用戶須要指定一個要執行的主類(main()函數那個類),觸發初始化。
  • 當時用動態語言支持時候,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokestatic的方法的句柄,而且這個方法句柄對應的類沒有初始化,則觸發初始化。

加載

加載階段,虛擬機須要完成三件事情:

  • 經過一個類的全限定名來獲取定義此類的二進制字節流。
  • 將字節流所表明的靜態存儲結構轉換爲方法區的運行時結構數據。
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

在加載階段可使用系統提供的引導類加載器來完成,也能夠由用戶自定的類加載器完成(如Android的插件化技術)。

詳細可參考IBM這篇文章

引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。

擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。

系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過ClassLoader.getSystemClassLoader()來獲取它。

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區裏,方法區中的數據存儲格式由虛擬機自行定義。而後在內存中實例化一個java.lang.Class對象,這個對象做爲程序訪問方法區中這些類型數據的外部接口。

加載階段和鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始了,但這些夾在加載階段之中進行的動做,仍屬於鏈接階段的內容。

驗證

驗證是鏈接的第一步,目的是爲了確保Class文件字節流中包含的信息符合虛擬機要求。

驗證階段包含4個階段的檢驗動做:

  • 文件格式驗證:保證輸入的字節流可以正確解析而且存儲在方法區以內,格式上符號描述一個Java類型信息的要求。這一階段基於二進制字節流進行,驗證經過後,會將字節流存入內存中的方法區中,後面的驗證基於方法區的存儲結構驗證,不會再直接操做字節流。

(如驗證是否以魔數開頭,主次版本號是否在虛擬機處理範圍以內等等)

  • 元數據驗證:這一階段主要對字節碼的描述信息進行語義分析,保證其描述的信息符合Java語言規範的要求。

如這個類是否有父類?

這個類的父類是否繼承了不被容許繼承的類(final關鍵字)

...

  • 字節碼驗證:該階段主要目的是經過數據流和控制流分析,肯定語義是否合法,符合邏輯。 第二階段對元數據驗證完畢後,這個階段對類的方法體進行校驗分析。
  • 符號引用驗證:這個階段發生在虛擬機將符號引用轉化成直接引用的時候,**這個轉化發生在鏈接的第三階段,解析階段發生。**當前階段的驗證能夠看作對類自身之外的信息進行匹配性校驗,一般會校驗以下內容:

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

再指定類中是否存在符合方法字段描述符以及簡單名稱所描述的方法和字段。

符號引用中的類,字段,方法的訪問性(private,protected,public,default)是否能夠被當前類訪問.。

準備

準備階段是正式將類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。

須要注意的是,這個時候進行內存分配得僅僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量會在對象實例化時候隨着對象一塊兒分配在Java堆中。其次,上述所說的設置類變量初始值是指的0值,假設類變量定義以下:

public static final num=3;

則num在準備階段初始化的值爲0而不是3,由於這個時候還沒有開始執行任何Java方法,而把num指定爲3的putstatic指令是在程序被編譯後,存放於類構造器<clinit>()方法中的,因此num=3的動做在初始化階段纔會執行。

But,若是上述代碼加了一個final字段,那就不同了:

public static final final num=3;

這時候Javac將會爲num生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue的設置將num賦值爲3。

public class Test {
    private static int num=3;
    private static final int finalnum=3;

    public static void main(String[] args) {

    }
}
//使用javap反編譯查看Test.class,省略無關部分
//javap -v -p Test.class
 private static int num;
   descriptor: I
   flags: ACC_PRIVATE, ACC_STATIC

 private static final int finalnum;
   descriptor: I
   flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
   ConstantValue: int 3

這裏順便記錄一下<init>和<clinit>的區別,能夠看這個

  • <init> is the (or one of the) constructor(s) for the instance, and non-static field initialization.

  • <clinit> are the static initialization blocks for the class, and static field initialization.

class X {

   static Log log = LogFactory.getLog(); // <clinit>

   private int x = 1;   // <init>

   X(){
      // <init>
   }

   static {
      // <clinit>
   }

}

解析

解析階段是虛擬機將常量池內的符號引用替換成直接引用的過程,在上面的符號引用驗證中提到過。

  • 符號引用:以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時候可以無歧義的定位到目標便可。
  • 直接引用:直接引用是直接指向目標的指針,相對偏移量或者一個間接定位到目標的句柄。

符號引用替換成直接引用的過程有以下幾種狀況(詳細內容見深刻理解Java虛擬機P223):

  • 類或者接口的解析
  • 字段解析
  • 類方法解析
  • 接口方法解析

上述階段經過了,則說明解析的工做就完成了。

初始化

初始化工做是類加載過程的最後一步,前面介紹的類加載過程當中,除了在加載階段用戶能夠經過自定義ClassLoader參與外,其他的皆有虛擬機主導完成。到初始化階段,才真正執行類定義中的Java代碼。

init是對象構造器方法,也就是說在程序執行 new 一個對象調用該對象類的 constructor 方法時纔會執行init方法

在準備階段,系統已經默認給類變量(被static修飾的變量)賦值過一次初始值(由系統決定)了,初始化階段就是執行程序猿賦值的過程。或者說初始化階段是執行<clinit>方法的過程。

clinit方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的。 編譯器收集的順序是由語句在源文件中出現的順序決定靜態語句塊只能訪問到定義在靜態語句塊以前>的變量,定義在他以後的變量,只能賦值,不能訪問[1]

clinit方法不須要顯示的調用父類構造器,虛擬機會保證在子類的clinit方法執行以前,父類的>clinit已經執行完畢。第一個執行clinit方法的確定是java.lang.Object。

因爲父類的clinit方法先執行,也就意味着父類中定義的靜態語句塊優於子類變量賦值操做。[2]

clinit方法對類或者接口來講不是必須的,若是一個類沒有靜態語句塊,也沒有對static變量賦值的操做,那麼編譯器能夠不爲這個類生成clinit方法。

接口中也會有clinit方法,惟一與類中執行不一樣的是,執行接口clinit方法不須要先執行父接口的>clinit方法,只有當父接口中的變量使用時候,父接口才會初始化。

虛擬機保證一個類的clinit方法只會執行一次。

[1]的驗證:

static {
        num=2323;//能夠賦值
        System.out.print(num+"");//IDE會提示不能訪問
    }
 private static int num=3;

//----------------------------------------下面狀況經過
 private static int num=3;
 static {
        num=2323;
        System.out.print(num+"");
    }

[2]的驗證:

public static void main(String[] args) {
        Child child = new Child();
        System.out.print(child.K+"");
    }

    static class Parent{
        public static int lll=23;
        static {
            lll=666;
        }
    }
    static class Child extends Parent{
        public static int K=lll;
    }
	//輸出666

恰好整理一下static在代碼中的執行順序

若是類尚未被加載:

  1. 先執行父類的靜態代碼塊和靜態變量初始化,而且靜態代碼塊和靜態變量的執行順序只跟代碼中出現的順序有關。

  2. 執行子類的靜態代碼塊和靜態變量初始化。

  3. 執行父類的實例變量初始化

  4. 執行父類的構造函數

  5. 執行子類的實例變量初始化

  6. 執行子類的構造函數

若是類已經被加載:

  • 則靜態代碼塊和靜態變量就不用重複執行,再建立類對象時,只執行與實例相關的變量初始化和構造方法。

.class文件是二進制字節流形式

隨着虛擬機的不斷髮展,不少程序語言開始選擇與操做系統和機器指令集無關的格式做爲編譯後的存儲格式(Class文件),從而實現」Write Once, Run Anywhere」。 Java設計之初,考慮後期能讓Java虛擬機運行其餘語言,目前有愈來愈多的其餘語言均可以直接須要在Java虛擬機,虛擬機只能識別Class文件,至因而由何種語言編譯而來的,虛擬機並不關心。

類卸載的過程及觸發條件

在類使用完以後,知足下面的情形,會被卸載:

  1. 該類在堆中的全部實例都已被回收,即在堆中不存在該類的實例對象。

  2. 加載該類的classLoader已經被回收。

  3. 該類對應的Class對象沒有任何地方能夠被引用,經過反射訪問不到該Class對象。

若是類知足卸載條件,JVM就在GC的時候,對類進行卸載,即在方法區清除類的信息。

相關文章
相關標籤/搜索