天天看點

深入Java虛拟機讀書筆記第五章Java虛拟機Java虛拟機

Java虛拟機

  Java虛拟機之是以被稱之為是虛拟的,就是因為它僅僅是由一個規範來定義的抽象計算機。是以,要運作某個Java程式,首先需要一個符合該規範的具體實作。

Java虛拟機的生命周期

  一個運作時的Java虛拟機執行個體的天職就是:負責運作一個Java程式。當啟動一個Java程式時,一個虛拟機執行個體就誕生了。當該程式關閉退出,這個虛拟機執行個體也就随之消亡。每個Java程式都運作于它自己的Java虛拟機執行個體中。

  Java虛拟機執行個體通過調用某個初始類的main()方法來運作一個Java程式。而這個main()方法必須是共有的public、靜态的static、傳回值為void,并且接受一個字元串數組作為參數。任何擁有這樣一個main()方法的類都可以作為Java程式運作的起點。

比如,考慮這樣一個Java程式,它列印出傳給它的指令行參數:

Class Echo{

  Public static void main(String[] args){

    Int len = args.length;

    For(int i = 0; i < len; ++i){

      System.out.print(args[i] + “ ”);

    }

    System.out.println();

  }

}

  必須告訴Java虛拟機要運作的Java程式中初始類的名字,這個程式将從它的main()方法開始運作。如在Windows上使用指令:

Java Echo Greetings, Planet.

  Java程式初始類中的main()方法,經作為該程式初始線程的起點,任何其他的線程都是由這個線程啟動的。

  在Java虛拟機内部有兩種線程:守護線程和非守護線程。守護線程通常是由虛拟機自己用的,比如執行垃圾收集任務的線程。但是,Java程式也可以把它建立的任何線程标記為守護線程。而Java程式中的初始線程---就是開始于main()的那個,是非守護線程。

  隻要還有任何非守護線程在運作,那麼這個Java程式也在繼續運作(虛拟機仍然存活)。當該程式所有的非守護線程都終止時,虛拟機執行個體将自動退出。假若安全管理器運作,程式本身也能夠通過調用Runtime類或者System類的exit()方法退出。

Java虛拟機的體系結構

  在Java虛拟機規範中,一個虛拟機執行個體的行為是分别按照子系統、記憶體區、資料類型以及指令這幾個術語來描述的。

  下圖為Java虛拟機的結構框圖,包括在規範中描述的主要子系統和記憶體區。前面提到,每個Java虛拟機都有一個類裝載器子系統,它根據給定的全限定名來裝入類型。同樣,每個Java虛拟機都有一個執行引擎,它負責執行那些包含在被裝載類的方法中的指令。

深入Java虛拟機讀書筆記第五章Java虛拟機Java虛拟機

  當Java虛拟機運作一個程式時,它需要記憶體來存儲許多東西,例如,位元組碼,從已裝載的class檔案中得到的其他資訊,程式建立的對象、傳遞給方法的參數、傳回值、局部變量以及運算的中間結果等,Java虛拟機把這些東西都組織到幾個“運作時資料區”中,以便于管理。Java虛拟機規範對“運作時資料區”的描述是抽象的,由具體實作的設計者決定。

  某些運作時資料區是由程式中所有線程共享的,還有一些則隻能由一個線程擁有。每個Java虛拟機執行個體都有一個方法區以及一個堆,它們是由該虛拟機執行個體中所有線程共享的。當虛拟機裝載一個class檔案時,它會從這個class檔案包含的二進制資料中解析類型資訊。然後,它把這些類型資訊放到方法區中。當程式運作時,虛拟機會把所有該程式在運作時建立的對象都放到堆中。

  當每一個新線程被建立時,它都将得到它自己的PC寄存器(程式計數器)以及一個Java棧:如果線程正在執行的是一個Java方法(非本地方法),那麼PC寄存器的值将總是訓示下一條将被執行的指令,而它的Java棧則總是存儲該線程中Java方法調用的裝載---包括它的局部變量,被調用時傳進來的參數,它的傳回值,以及運算的中間結果等。而本地方法調用的狀态,則是以某種依賴于具體實作的方式存儲在本地方法棧中,也可能是在寄存器或者其他某些與特定實作相關的記憶體中。

  Java棧是由許多棧幀(stack frame)或幀(frame)組成的,一個棧幀包含一個Java方法調用的狀态。當線程調用一個Java方法時,虛拟機壓入一個新的棧幀到該線程的Java棧中,當該方法傳回時,這個棧幀從Java棧中彈出。

  Java虛拟機沒有寄存器,其指令集使用Java棧來存儲中間資料。這樣設計的原因是為了保持Java虛拟機的指令集盡量緊湊,同時也便于Java虛拟機在那些隻有很少通用寄存器的平台上實作。另外Java虛拟機的這種基于棧的體系結構也有助于運作時某些虛拟機實作的動态編譯器和即時編譯器的代碼優化。

  下圖描繪了Java虛拟機為每一個線程建立的記憶體區,這些記憶體區域是私有的,任何線程都不能通路另一個線程的PC寄存器或者Java棧。

深入Java虛拟機讀書筆記第五章Java虛拟機Java虛拟機

 資料類型

    Java虛拟機是通過某些資料類型來執行計算的,資料類型及其運算都是由Java虛拟機規範嚴格定義的,資料類型可以分為兩種:基本類型和引用類型。基本類型的變量持有原始值,而引用類型的變量持有引用值。術語“引用值”指的是對某個對象的引用,而不是該對象的本身,與此相對,原始值則是真正的原始資料。

深入Java虛拟機讀書筆記第五章Java虛拟機Java虛拟機

  Java語言中的所有基本類型同樣也都是Java虛拟機中的基本類型。但是boolean有點特别,雖然Java虛拟機也把boolean看做基本類型,但是指令集對boolean隻有很有限的支援。當編譯器把Java源碼編譯為位元組碼時,它會用int或byte來表示boolean。在Java虛拟機中,false是由整數零來表示的,所有非零整數都表示true,涉及boolean值的操作則會使用int。另外,boolean數組是當做byte數組來通路的,但是在“堆”區,它也可以被表示為位域。

  Java虛拟機中還有一個隻在内部使用的基本類型:returnAddress,Java程式猿不能使用這個類型,這個基本類型被用來實作Java程式的finally字句。

  Java虛拟機的引用類型被統稱為“引用”(reference),有三種引用類型:類類型、接口類型以及數組類型,它們的值都是對動态建立對象的引用。類類型的值是對類執行個體的引用;數組類型的值是對數組對象的引用,在Java虛拟機中,數組是個真正的對象;而接口類型的值,則是對實作了該接口的某個類執行個體的引用。

  Java虛拟機規範定義了每一種資料類型的取值範圍,但是卻沒有定義它們的位寬。位寬由具體的虛拟機實作設計者決定。

字長的考量

  Java虛拟機中,最基本的資料單元就是字,它的大小是由每個虛拟機實作的設計者來決定的。字長必須足夠大,至少是一個字單元就足以持有byte、short、int、char、float、returnAddress或者reference類型的值,而兩個字單元就足以持有long或者double類型的值。是以,虛拟機實作的設計者至少得選擇32位作為字長,或者選擇更為高效的字長大小。通常根據底層主機平台的指針長度來選擇字長。

  在Java虛拟機規範中,關于運作時資料區的大部分内容,都是基于“字”這個抽象概念的。比如,關于棧幀的兩個部分---局部變量和操作數棧---都是按照“字”來定義的。這些記憶體區能夠容納任何虛拟機資料類型的值,當把這些值放到局部變量或者操作數棧中時,它将占用一個或兩個字單元。

  在運作時,Java程式無法偵測到底層虛拟機的字長大小;同樣,虛拟機的字長大小也不會影響程式的行為---它僅僅是虛拟機實作的内部屬性。

類裝載子系統

  在Java虛拟機中,負責查找并裝載類型的那部分被稱為類裝載子系統。

  Java虛拟機有兩種類裝載器:啟動類裝載器和使用者自定義類裝載器。前者是Java虛拟機實作的一部分,後者則是Java程式的一部分。由不同的類裝載器裝載的類将被放在虛拟機内部的不同命名空間中。

  類裝載子系統涉及Java虛拟機的其他幾個組成部分,以及幾個來自java.lang庫的類。比如,使用者自定義的類裝載器是普通的Java對象,它的類必須派生自java.lang.ClassLoader類。ClassLoader中定義的方法為程式提供了通路類裝載器機制的接口。此外,對于每一個被裝載的類型,Java虛拟機都會為它建立一個java.lang.Class類的執行個體來代表該類型。和所有其他對象一樣,使用者自定義的類裝載器以及Class類的執行個體都放在記憶體中的堆區,而裝載的類型資訊則都位于方法區。

  裝載、連接配接以及初始化  類裝載子系統除了要定位和導入二進制class檔案外,還必須負責驗證被導入類的正确性,為類變量配置設定并初始化記憶體,以及幫助解析符号引用。這些動作必須嚴格按以下順序進行:

1、裝載---查找并裝載類型的二進制資料

2、連接配接---執行驗證,準備,以及解析

  驗證---確定被導入類型的正确性

  準備--為類變量配置設定記憶體,并将其初始化為預設值

  解析---把類型中的符号引用轉換為直接引用

3、初始化---把類變量初始化為正确初始值

  啟動類裝載器  隻要是符合Java class檔案格式的二進制檔案,Java虛拟機實作都必須能夠從中辨識并裝載其中的類和接口。某些虛拟機實作也可以識别其他的非規範的二進制格式檔案,但它必須能夠辨識class檔案。

  每個Java虛拟機實作都必須有一個啟動類裝載器,它知道怎麼裝載受信任的類,比如Java API的class檔案。Java虛拟機規範并未規定啟動類裝載器如何去尋找class檔案。

  隻要給定某個類型的全限定名,啟動類裝載器就必須能夠以某種方法得到定義該類型的資料。在JDK1.2中,啟動類裝載器隻在系統類(Java API的類檔案)的安裝路徑中查找要裝入的類;而搜尋CLASSPATH目錄的任務,現在交給了系統類裝載器---它是一個自定義的類裝載器,當虛拟機啟動時就被自動建立。

  使用者自定義類裝載器    盡管“使用者自定義類裝載器”本身是Java程式的一部分,但類ClassLoader中的四個方法是通往Java虛拟機的通道:

  protected final class defineClass(String name, byte data[], int offset, int length)

  protected final class defineClass(String name, byte data[], int offset, int length, ProtectionDomain protectionDomain)

  protected final Class findSystemClass(String name);

  protected final void resolveClass(Class c);

  任何Java虛拟機實作都必須把這些方法連接配接到内部的類裝載器子系統中。

  兩個被重載的defineClass()方法都要接受一個名為data[]的位元組數組作為輸入參數,并且在data[offset]到data[offset+length]之間的二進制資料必須符合Java class檔案格式---它表示一個新的可用類型。而name參數是個字元串,它給出指定類型的全限定名。使用第一個defineClass()時,該類型被賦以預設的保護域,使用第二個時該類型的保護域由它的protectionDomain參數指定。每個Java虛拟機實作都必須保證ClassLoader類的defineClass()方法能夠把新類型導入到方法區中。

  findSystemClass()方法接受一個字元串作為參數,它指出被裝入類型的全限定名。在版本1.2中,該方法使用系統類裝載器來裝載指定類型。任何Java虛拟機實作都必須保證findSystemClass()方法能夠以這種方式調用系統類裝載器。

  resolveClass()方法接受一個Class執行個體的引用作為參數,它将對該Class執行個體表示的類型執行連接配接動作。而defineClass()方法則隻負責裝載。當defineClass方法傳回一個Class執行個體時,也就表示指定的class檔案已經被找到并裝載到方法區了,但是卻不一定被連接配接和初始化了。Java虛拟機實作必須保證ClassLoader類的resolveClass方法能夠讓類裝載器子系統執行連接配接動作。

  命名空間  每個類裝載器都有自己的命名空間,其中維護着由它裝載的類型。一個Java程式可以多次裝載具有同一個全限定名的多個類型,當多個類裝載器都裝載了同名的類型時,為了唯一地辨別該類型,還要在類型名稱前加上裝載該類型的類裝載器的辨別。

  Java虛拟機中的命名空間,其實是解析過程的結果。對于每一個被裝載的類型,Java虛拟機都會記錄裝載它的類裝載器。當虛拟機解析一個類到另一個類的符号引用時,它需要被引用類的類裝載器。

方法區

    在Java虛拟機中,關于被裝載類型的資訊存儲在一個邏輯上被稱為方法區的記憶體中。當虛拟機裝載某個類型時,它使用類裝載器定位相應的class檔案,然後讀人這個class檔案---一個線性二進制資料流---然後将它傳輸到虛拟機中、.緊接着虛拟機提取其中的類型資訊,并将這些資訊存儲到方法區。該類型中的類(靜态)變量同樣也是存儲在方法區。Java虛拟機在内部如何存儲類型資訊,這是由具體實作的設計者來決定的。

  當虛拟機運作Java程式時,它會查找使用存儲在方法區中的類型資訊。設計其應當為類型資訊的内部表示設計适當的資料結構,以盡可能在保持虛拟機小巧緊湊的同時加快程式的運作效率。如果正在設計一個需要在少量記憶體的限制中操作的實作,設計者可能會決定以犧牲某些運作速度來換取緊湊性。另外一方面,如果設計一個将在虛拟記憶體系統中運作的實作,設計者可能會決定在方法區中儲存一些備援倍息,以此來加快執行速度。(如果底層主機沒有提供虛拟記憶體,但是提供了一個硬碟,設計者可能會在實作中建立一個虛拟記憶體系統。Java虛拟機的設計者可以根據目标平台的資源限制和需求,在空問和時間上做出權衡.選擇實作什麼樣的資料結構和資料組織。

  由于所有線程都共享方法區,是以它們對方法區資料的通路必須被設計為是線程安全的。比如,假設同時有兩個線程都企圖通路一個名為Lava的類,而這個類還沒有被裝人虛拟機,那麼,這時隻應該有一個線程去裝載它,而另一個線程則隻能等待。方法區的大小不必是固定的,虛拟機可以根據應用的需要動态調整。同樣,方法區也不必是連續的,方法區可以在一個堆(甚至是虛拟機自己的堆)中自由配置設定。另外,虛拟機也可以允許使用者或者程式員指定方法區的初始大小以及最小和最大尺寸等。

  方法區也可以被垃圾收集,因為虛拟機允許通過使用者定義的類裝載器來動态擴充Java程式,是以一些類也會成為程式“不再引用”的類。當某個類變為不再被引用的類時,Java虛拟機可以解除安裝這個類(垃圾收集)進而使方法區占據的記憶體保持最小。

  類型資訊  對每個裝栽的類型,虛拟機都會在方法區中存儲以下類型資訊:

    •這個類型的全限定名。

    •這個類型的直接超類的全限定名(除非這個類型是java.lang.Object,它沒有超類)

    •這個類型是類類型還是接口類型

    •這個類型的通路修飾符(public、abstract或final的某個子集)

    •任何直接超接口的全限定名的有序清單。

  在Java class檔案和虛拟機中,類型名總是以全限定名出現在Java源代媽中,全限定名由類所屬包的名稱加一個再加一個“.”,再加上類名組成。例如,類Object的所屬包為java.lang,那它的全限定名應該是java.lang.Object,但在class檔案裡,所有的“.”都被斜杠“/”代替.這樣就成為java/lang/Objectc。至于全限定名在方法區中的表示,則因不同的設計者有不同的選擇而不同,可以用任何形式和資料結構來代表。

  除了上面列出的基本類型息外,虛拟機還得為每個被裝載的類型存儲以下資訊:

    •該類型的常量池。

    •字段資訊。

    •方法資訊

    •除了常量以外的所有類(靜态)變量。

    •一個到類ClassLoader的引用。

    •一個到Class類的引用。

  常量池  虛拟機必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用常量的一個有序集合,包括直接常量(string、integer和floating point常量)和對其他類型、字段和方法的符号引用。池中的資料項就像數組一樣是通過索引通路的。因為常量池存儲了相應類型所用到的所有類型、字段和方法的符号引用,是以它在Java程式的動态連接配接中起着核心的作用。

  字段資訊    對于類型中聲明的每一個字段,方法區中必須儲存下面的資訊。除此之外,這些字段在類或接口中的聲明順序也必須儲存。下面是字段資訊的清單:

  •字段名。

  •字段的類型。

  •字段的修飾符(public、private、protected.、static、final、volatile、transient的某個子集)。

  方法資訊    對于類型中聲明的每一個方法,方法區中必須儲存下面的資訊。和字段一樣,這些方法在類或者接口中的聲明順序也必須儲存。下面是力法資訊的清單:

  •方法名。

  •方法的傳回類型(或void)

  •方法參數的數量和類型(按聲明順序).

  •方法的修飾符(public、private、protected、static, find、synchronized、native、abstract的某個子集)。

除上面的清單中列出的條目之外,如果某個方法不是抽象的和本地的,它還必須儲存下列資訊:

  •方法的位元組碼(bytecodes)。

  •操作數棧和該方法的棧幀中的局部變量區的大小。

  •異常表。

  類(靜态)變量類變量是由所有類執行個體共享的,但是即使沒有任何類執行個體,它也可以被通路。這些變量隻與類有關——而非類的執行個體,是以它們總是作為類型資訊的一部分而存儲在方法區。除了在類中聲明的編譯時常量外,虛拟機在使用某個類之前,必須在方法區中為這些類變量配置設定空間。

  而編譯時常量(就是那些用final聲明以及用編譯時已知的值初始化的類變量)則和一般的類變量的處理方式不同,每個使用編譯時常量的類型都會複制它的所有常量到自己的常量池中,或嵌人到它的位元組碼流中。作為常量池或位元組碼流的一部分,編譯時常量儲存在方法區中——就和一般的類變量一樣。但是當一般的類變量作為聲明它們的類型的一部分資料面儲存的時候,編譯時常量作為使用它們的類型的一部分而儲存。

    指向ClassLoader類的引用    每個類型被裝載的時候,虛拟機必須跟蹤它是由啟動類裝載器還是由使用者自定義類裝載器裝載的。如果是使用者自定義類裝載器裝載的,那麼虛拟機必須在類型資訊中存儲對該裝載器的引用。這是作為方法表中的類型資料的一部分儲存的。

  虛拟機會在動态連接配接期間使用這個資訊。當某個類型引用另一個類型的時候,虛拟機會請求裝載發起引用類型的類裝載器來裝載被引用的類型。這個動态連接配接的過程,對于虛拟機分離命名空間的方式也是至關重要的。為了能夠正确地執行動态連接配接以及維護多個命名空間,虛拟機需要在方法表中得知每個類都是由哪個類裝載器裝載的。

  指向Class類的引用    對于每一個被裝載的類型(不管是類還是接口)虛拟機都會相應地為它建立一個java.lang.Class類的執行個體,而且虛拟機還必須以某種方式把這個執行個體和存儲在方法區中的類型資料關聯起來。在你的Java程式中,你可以得到并使用指向Class對象的引用。Class類中的一個靜态方法可以讓使用者得到任何己裝載的類的Class執行個體的引用。

  public static Class forName(String classHame)  //連接配接資料庫常用此方法

  比如,如果調用forName("java.lang.Object"),那麼将得到一個代表java.lang.Object的Class對象的引用。如果調用forName("java.util.Enumeration"),那麼得到的是代表java.util包中java.util.Enumeration接口的Class對象的引。可以使用forName()來得到代表任何包中任何類型的Class對象的引用,隻要這個類型可以被(或者已經被)裝載到目前命名空間中。如果虛拟機無法把請求的類型裝載到目前命名空間,那麼forName ()會抛出ClassNotFoundException異常。

  另一個得到Class對象引用的方法是,可以調用任何對象引用的getClass()方法。這個方法被來自Object類本身的所有對象繼承:

Public final class getClass();

   比如,如果你有一個到java.lang.Integer類的對象的引用,那麼你隻需簡單地調用Integer對象引用的getClass()方法,就可以得到表不java.lang,Integer類的Class對象。給出一個指向Class對象的引用,就可以通過Class類中定義的方法來找出這個類型的相關資訊。如果檢視這些方法,會很快意識到,Class類使得運作程式可以通路方法區中儲存的資訊。

  下面是Class類中生明的方法:

  public String getNameO;

  public Class getSuperClass();

  public boolean islnterface();

  public Class[] getlnterface();

  public ClassLoader getClassLoader ();

  這些方法僅能傳回已裝載類型的資訊。getName()傳回類型的全限定名,getSuperChss()傳回類型的直接超類的Class執行個體。如果類型是java.lang.Object類或者是一個接口,它們都沒有超類,getSuperClass()傳回null。Islntcrface()判斷該類型是否是接口,如果Class對象描述一個接口就傳回true;如果它描述一個類則傳回false。getlnterfaces()傳回一個Class對象數組,其中每個Class對象對應一個直接超接口,超接口在數組中以類型聲明超接口的順序出現。如果該類型沒有直接超接口,getlnterfaces()則傳回一個長度為零的數紐。getClassLoader()傳回裝載該類型的ClassLoadeT對象的引用,如果類型是由啟動類裝載器裝載的,則傳回null。所有這些資訊都直接從方法區中獲得。

  方法表    為了盡可能提高通路效率,設計者必須仔細設計存儲在方法區中的類型資訊的資料結構,是以,除了以上讨論的原始類型資訊,實作中還可能包括其他資料結構以加快通路原始資料的速度,比如方法表。虛拟機對每個裝載的非抽象類,都生成一個方法表,把它作為類資訊的一部分儲存在方法表中。方法表是一個數組,它的元素是所有它的執行個體可能被調用的執行個體方法的直接引用,包括那些從超類繼承過來的執行個體方法。(對于抽象類和接口,方法表沒有什麼幫助,因為程式決不會生成它們的執行個體。)運作時可以通過方法表快速搜尋在對象中調用的執行個體方法。

  方法區使用示例,為了展示虛拟機如何便用方法表中的資訊,我們舉個例子,看下面這個類:

Class Lava {

Private int speed = 5;

Void flow() {

  }

}

Class Volcano{

  Lava lava = new Lava();

  lava.flow();

}

  下面的段落描述了某個實作中是如何執行Volcano程式中main()方法的位元組碼中第一條指令的。不同的虛拟機實作可能會用完全不同的方法來操作,下面描述的隻是其中一種可能——但并不是僅有的一種,下面看一下Java虛拟機是如何執行Volcano程式中main ()方法的第一條指令的。

  要運作Vokano程式,首先得以某種“依賴于實作的”方式告訴虛拟機“Volcano”這個名字。之後,虛拟機将找到并讀人相應的class檔案“Volcano.class”,然後它會從導人的class檔案裡的二進制資料中提取類型資訊并放到方法區中。通過執行儲存在方法區中的位元組碼,虛拟機開始執行main()方法,在執行時,它會一直持有指向目前類(Volcano類)的常量池(方法區中的一個資料結構)的指針。

  注意,虛拟機開始執Volcano類中main()方法的位元組碼的時候,盡管Lava類還沒被裝載,但是和大多數(.也許所有)虛拟機實作一樣,它不會等到把程式中用到的所有類都裝載後才開始運作程式。恰好相反,它在需要時才裝載相應的類。main()的第一條指令告知虛拟機為列在常量池第一項的類配置設定足夠的記憶體。是以虛拟機使用指向Volcano常量池的指針找到第一項,發現它是一個對Lava類的符号引用,然後它就檢查方法區,看Lava類是否已經被裝載了。

  這個符導引用僅僅是一個給出類Lava的全限定名“Lava”的字元串。為了能讓虛拟機盡可能快地從一個名稱找到類,設計者應當選擇最佳的資料結構和算法。這裡可以采用各種方法,如散清單,搜尋樹等等。同樣的算法也可以用于實作Class類的forName()方法,這個方法根據給定的全限定名返同Class引用。

  當虛拟機發現還沒有裝載過名為“Lava”的類時,它就開始査找并裝載檔案“Lava.class”,并把從讀人的二逬制資料中提取的類型資訊放在方法區中。緊接着,虛拟機以一個直接指向方法區Lava類資料的指針來替換常量池第一項(就是那個字元串“Lava”)——以後就可以用這個指針來快速地通路Lava類。這個替換過揮稱為常量池解析,即把常量池中的符号引用替換為直接引用。這是逋過在方法區中搜尋被引用的元素實作的,在這期間可能又需要裝載其他類。在這裡,我們替換掉符号引用的“直接引用”是一個本地指針。

  終于,虛拟機準備為一個新的Lava對象配置設定記憶體。此吋,它又需要方法區中的資訊。還記得剛剛放到Volcano類常量池第一項的指針嗎?現在虛拟機用它來通路Lava類型倍息(此前剛放到方法區中的),找出其中記錄的這樣一個資訊:一個Lava對象需要配置設定多少堆空間。

  Java虛拟機總能夠通過存儲于方法區的類型資訊來确定一個對象需要多少記憶體,但是,某個特定對象事實上需要多少記憶體,是跟特定實作相關的。對象在虛拟機内部的表示是由實作的設計者來決定的。

  當Java虛拟機确定了一個Lava對象的大小後,它就在堆上配置設定這麼大的空間,并把這個對象執行個體的變量speed切始化為預設初始值0。假如Lava類的超類Object也有執行個體變量,則也會在此時被初始化力相應的預設值。

  當把新生成的Lava對象的引用壓到棧中,main()方法的第—條指令也完成了。接下來的指令通過這個引用調用Java代碼(該代碼把speed變量初始化為正确初始值5)。另外一條指令将用這個引用調用Lava對象引用的flow()方法。

  Java程式在運作時建立的所有類執行個體或數組都放在同一個堆中。而一個Java虛拟機執行個體中隻存在一個堆空間,是以所有線程都将共享這個堆。又由于一個Java程式獨占一個Java虛拟機執行個體,因而每個Java程式都有它自己的堆空間——它們不會彼此幹擾。但是同一個Java程式的多個線程卻共享着同一個堆空間,在這種情況下,就得考慮多線程通路對象(堆資料)的同步問題了。

  Java虛拟機有一條在堆中配置設定新對象的指令,卻沒存釋放記憶體的指令。正如你無法用Java代碼去明确釋放一個對象一樣,位元組碼指令也沒有對應的功能。虛拟機自己負責決定如何以及何時釋放不再被運作的程式引用的對象所占據的記憶體。程式本身不用去考慮何時需回收對象所占用的記憶體,通常,虛拟機把這個任務交給垃圾收集器。

  垃圾收集    垃圾收集器的主要工作就是自動回收不再被運作的程式引用的對象所占用的記憶體。此外,它也可能去移動那些還在使用的對象,以此減少堆碎片。

  Java虛拟機規範并沒有強制規定垃圾收集器,它隻要求虛拟機實作必須“以某種方式”管理自己的堆空間。舉個例子,某個實作可能隻有固定大小的堆空問可用,當空間填滿,它就簡單地拋出OutOfMemory異常,根本不去考慮回收垃圾對象的問題。這樣的一個實作雖然簡陋,擔卻是符合規範的。總之,Java虛拟機規範并沒有規定具體的實作必須為Java程式準備多少記憶體,也沒有說它必須怎麼管理自已的堆空間,它僅僅告訴實作的投計者:Java稈序需要從堆中為對象配置設定空間,并且程式本身不會主動釋放它。是以堆空間的管理(包括垃圾收集)問題得由設計者自行去考慮處理方式。

  Java虛拟機規範沒有指定垃圾收集應該采用什麼技術。這些都由虛拟機的設計者根據他們的目标、考慮所受的限制、用自己的能力去決定什麼才是最好的技術。因為到對象的引用可能很多地方都存在,如Java棧、堆、方法區、本地方法棧,是以垃圾收集技術的使用在很大程度上會影響到運作時資料區的設計。

  和方法區一樣,堆空間也不必是連續的記憶體區。在程式運作時,它可以動态擴充或收縮。事實上,一個實作的方法區可以在堆頂實作。換句話說,就是虛拟機需要為一個新裝載的類配置設定記憶體時,類型資訊和實際對象可以都在同一個堆上。是以,負責回收無用對象的垃圾收集器可能也要負責無用類的釋放(解除安裝)。另外,某些實作可能也允許使用者或程式員指定堆的初始大小、最大最小值等等。

  對象的内部表示    Java虛拟機規範并沒有規定lava對象在堆中是如何表示的。對象的内部表示也影響着整個堆以及垃圾收集器的設計,它由虛拟機的實作者決定。

  Java對象中包含的基本資料由它所屬的類及其所有超類聲明的執行個體變量組成。隻要有一個對象引用,虛拟機就必須能夠快速地定位對象執行個體的資料。另外,它也必須能通過該對象引用通路相應的類資料(存儲于方法區的類型資訊)。是以在對象中通常會有一個指向方法區的指針。一種可能的堆空間設計就是,把堆分為兩部分:一個句栖池,一個對象池,如圖5-5所示。而一個對象引用就是一個指向句栖池的本地指針。句柄池的每個條目有兩部分:一個指向對象執行個體變量的指針,一個指向方法區類型資料的指針。這種設計的好處是有利于堆碎片的整理,當移動對象池中的對象時,句柄部分隻需耍更改一下指針指向對象的新位址就可以了——就是在句柄池中的那個指針。缺點是每次通路對象的執行個體變量都要舒過兩次指針傳遞。

  另一種設計方式是使對象指針宜接指向一組資料,而讀資料包括對象執行個體資料以及指向方法區中類資料的指針。這樣設計的優缺點正好與前面的方法相反,它隻需要一個指針就可以通路對象的執行個體資料,但是移動對象就變得更加複雜。當使用這種堆的虛拟機為了減少記憶體碎片。而移動對象的時候,它必須在整個運作時資料K中更新指向被移動對象的引用。圖5-6描繪了這種表示對象的方法。

  有如下幾個理由要求虛拟機必須能夠通過對象引用得到類(類權)資料:當程式在運作時需要轉換某個對象引用為另一種類型時,虛拟機必須要檢查這種轉換是否被允許,被轉換的對象是否的确足被引用的對象或者它的超類型。當程式在執行instanceof操作時,虛拟機也進行了同樣的檢查。在這兩種情況下,虛拟機部需要檢視被引用的對象的類資料。最後,當程式中調用某個執行個體方法時,虛拟機必須迸行動态綁定,換句話說,它不能按照引用的類型來決走将要調用的方法.而必須報據對象的'實際類。為此,虛戒機必須再次通過對象的引用去通路類資料。

  不管虛拟機的實作使用什麼樣的對象表示法,很可能每個對象都有一個方法表,因為方法表加快了調用執行個體方法時的效率,進而對Java虛拟機實作的整體性能起着非常重要的正面作用;但是Java虛拟機規範并未要求必須使用方法表,是以并是所有實作中都會使用它。比如那些有嚴格記憶體資源限制的實作,或許它們裉本不可能有足夠的額外記憶體資源來存儲方法展。如果一個實作使用方法表,那麼僅僅使用一個指向對象的引用,就可以很快地通路到對象的方法表。

  下圖展示了一種把方法表和對象引用聯系起來的實作方式。每個對象的資料都包含一個指向特殊資料結構的指針,這個資料結構位于方法區,它包括兩部分:

  •一個指向方法區對應類資料的指針。

  •此對象的力法表。

深入Java虛拟機讀書筆記第五章Java虛拟機Java虛拟機

  方法表是個指針數組,其中的每一項都是一個指向“執行個體方法資料”的指針,執行個體方法可以被那類的對象調用。方法表指向的執行個體方法資料包括以下資訊:

  •此方法的操作數棧和局部變裡區的大小。

  •此方法的位元組碼。

  •異常表。

  這些足夠虛拟機去用一個方法了,方法表中包含有方法指針---指向類或其超類聲明的方法的資料:也就是說,方法表所指向的方法可能是此類聲明的,也可能是它繼承下來的。

  堆上的對象資料中還有一個邏輯部分,那就是對象鎖。這是—個互斥對象,虛拟機屮的每個對象都有一個對象鎖,它被用于協調多個線程通路同一個對象時的同步。在任何時刻,隻能有一個線程“擁有”這個對象鎖,是以隻有這個線程才能通路該對象的資料。此時其他希望通路這個對象的線程隻能等待,直到擁有對象鎖的線程釋放鎖。當某個線程擁有一個對象鎖後,可以繼續對這個鎖追加請求。但請求幾次,必須對應地釋放幾次,之後才能輪到其他線程。比如一個線程清求了三次鎖,在它釋放三次鎖之前,它一直保持“擁有”這個鎖。

  很多對象在其整個生命周期内都沒有被任何線程加鎖。線上程實際請求某個對象的鎖之前,實作對象鎖所需要的資料是不必要的。很多實作不在對象自身内部儲存一個指向鎖資料的指針。而隻有當第一次需要加鎖的時候才配置設定對應的鎖資料,但這時虛拟機要用某種間接方法來聯系對象資料和對應的鎖資料,例如把鎖資料放在一個以對象位址為索引的搜尋樹中。

  除了實作鎖所需要的資料外,每個Java對象邏輯上還與實作等待集合(wait  set)的資料相關聯。鎖是用來實作多個線程對共享資料的互斥通路的,而等待集合是用來讓多個線程為完成一個共同目标而協調工作的。

  等待集合由等待方法和通知方法聯合使用。每個類都從Object那裡繼承了三個等待方法(三個名為wait()的重載方法)和兩個通知方法(notify()及notifyAll())。當某個線程在一個對象上調用等待方法吋,虛拟機就阻塞這個線程,并把它放在了這個對象的等待集合中。直到另一個線程在同一個對象上調用通知方法,虛拟機會在之後的某個時刻喚醒一個或多個在等待集合中被阻塞的線程。正像鎖資料一樣,在實際調用對象的等待方法或通知方法之前,實作對象的等待集合的數椐并不是必需的。是以,許多虛拟機實作都把等待集合資料與實際對象資料分開,隻有在需要時才為此對象建立同步資料(通常是在第一次調用等待方法或通知方法時)。

  最後一種資料類型——可以作為堆中某個對象映像的一部分,是與拉圾收集器有關的資料。垃圾收集器必須(以某種方式)跟蹤程式引用的每個對象,這個任務不可避免地要附加一些資料給這些對象,資料的類型要視拉圾收集使用的算法而定。例如,假如垃圾收集器使用“标記并清除”算法,這就需要能夠标記對象能否被引用。此外,對于不再被引用的對象,還需要指明它的終結方法(finalize)是否已經運作過了。像線程鎖一樣,這些資料也可以放在對象資料外。有一些垃圾收集技術隻在垃圾收集器運作時需要額外資料。例如“标記并清除”算法就使用一個獨立的位圖來标記對象的引用情況。

  除了标記對象的引用情況外,垃圾收集器還要區分對象是否調用了終結方法。對幹在其類中聲明了終結方法的對象,在回收它之前,垃圾收集器必須調用它的終結方法。Java語言規範指出,拉圾收集器對每個對象隻能調用一次終結方法,但是允許終結方法複活(resurrect)這個對象,即允許該對象被再次引用。這樣當這個對象再次被回收時,就不用再調用終結方法了。需要終結方法的對象不多,而需要複活的更少,是以對一個對象回收兩次的情況很少見。這種用來标志終結方法的資料雖然邏輯上是對象的一部分,但通常實作上不随對象儲存在堆中。大部分情況下,垃圾收集器會在一個單獨的空間儲存這個資訊。

  數組的内部表示  在Java中,數組是真正的對象。和其他對象一樣,數組總是存儲在堆中。同樣,和普通對象一樣,實作的設計者将決定數組在堆中的表示形式。

  和其他所有對象一樣,數組也擁有一個與它們的類相關聯的Class執行個體,所有具有相同次元和類型的數組都是同一個類的執行個體,而不管數組的長度(多元數組每一維的長度)是多少,例如一個包含3個int整數的數組和一個包含300個int整數的數組擁有同一個類。數組的長度隻與執行個體資料有關。

  數組類的名稱由兩部分組成:每一維用一個方括号“[”表示,用字元或字元串表示類型。比如,元素類型為int整數的、一維數組的類名為“[I”,元素類型為byte的三維數組為“[[[B”,元素類型為Object的二維數組“[[Ljava/lang/Object”。

  多元數組被表示為數組的數組。比如,int類型的二維數組,将表示為一個一維數組,其中的毎個元素是一個一維int數組的引用。

  在堆中的每個數組對象還必須儲存的資料是數組的長度、數組資料,以及某些指向數組的類資料的引用。虛拟機必須能夠通過一個數組對象的引用得到此數組的長度,通過索引通路其元素(其間要檢查數組邊界是否越界),調用所有數組的直接超類Object聲明的方法等等。

程式計數器

  對于一個運作中的Java程式而言,其中的每一個線程都有它自己的PC(程式計數器)寄存器,它是在該線程啟動時建立的。PC寄存器的大小是一個字長,是以它既能夠持有一個本地指針,也能夠持有一個returnAddress。當線程執行某個Java方法時,PC寄存器的内容總是下一條将被執行指令的“位址”,這裡的“位址”可以是一個本地指針,也可以是在方法位元組碼中相對于該方法起始指令的偏移量。如果該線程正在執行一個本地方法,那麼此PC寄存器的值是“undefined'”。

Java棧

  每當啟動一個新線程時,Java虛拟機都會為它配置設定一個Java棧。前面我們曾經提到,Java棧以幀為機關儲存線程的運作狀态。虛拟機隻會直接對Java棧執行兩種操作:以幀為機關的壓棧或出棧。某個線程正在執行的方法被稱為該線程的目前方法,目前方法使用的幀棧稱為目前幀,目前方法所屬的類稱為目前類,目前類的常量池稱為目前常量池。線上程執行一個方法時,它會跟蹤目前類和目前常量池。此外,當虛拟機遇到棧内操作指令時,它對目前幀内資料執行操作。

  每當線程調用一個Java方法時,虛拟機都會在該線程的Java棧中壓人一個新幀。而這個新幀自然就成為了目前偵。在執行這個方法時,它使用這個幀來存儲參數、局部變量、中間運算結果等等資料。

  Java方法可以以兩種方式完成。一種通過return傳回的,稱為正常傳回;一種是通過抛出異常而異常中止的。不管以哪種方式傳回,虛拟機都會将目前幀彈出Java棧然後釋放掉,這樣上—個方法的幀就成為目前幀了。

  Java棧上的所有資料都是此線程私有的。任何線程都不能通路另一個線程的棧資料,是以我們不需要考慮多線程情況下棧資料的通路同步問題。當一個線程調用一個方法時,方法的局部變量儲存在調用線程Java找的幀中。隻有一個線程能總是通路那些局部變最,即調用方法的線程。

  像方法區和堆一樣,Java棧和幀在記憶體中也不必是連續的。幀可以分布在連續的棧裡,也可以分布在堆裡,或者二者兼而有之。表示Java棧和棧幀的實際資料結構由虛拟機的實作者決定,某些實作允許使用者指定Java棧的初始大小和最大最小值。

棧幀

  棧幀由三部分組成:局部變量區、操作數棧和幀資料區。局部變量區和操作數棧的大小要視對應的方法而定,它們是按字長計算的。編譯器在編譯時就确定了這些值并放在class檔案中。而幀資料區的大小依賴于具體的實作。

  當虛拟機調用一個Java方法時,它從對應類的類型資訊中得到此方法的局部變量區和操作數棧的大小,并據此配置設定棧幀記憶體,然後壓入Java棧中。

  局部變量區  Java棧幀的局部變量區被組織為一個以字長為機關、從0開始計數的數組。位元組碼指令通過從0開始的索引來使用其中的資料類型。類型為int、float、reference和returnAddress的值在數組中隻占據一項,而類型為byte、short和char的值在存入數組前都将被轉換為int值,因而同樣占據一項。但是類型為long和double的值在數組中卻占據連續的兩項。

  在通路局部變量中的long和double值的時候,指令隻需指出連續兩項中第一項的索引值。例如某個long值占據第3、4項,那麼指令會取索引為3的long值。局部變量區的所有值都是字對齊的,long和double這樣占據兩項數組元素的值同樣可以起始于任何索引。

  局部變量區包含對應方法的參數和局部變量。編譯器首先按聲明的順序把這些參數放入局部變量數組。

  除了Java方法的參數(編譯器首先嚴格按照它們的聲明順序放到局部變量數組中,而對于真正的局部變量,它可以任意決定放置順序,甚至可以用一個索引指代兩個局部變量---比如當兩個局部變量的作出域不重疊時,像下面Example3b中的局部變量i和j就是這種情形:在方法的前半段,在開始生效之前,0号索引的入口可以被用來代表i。在方法的後半段,i經超過了有效作用域,0号入口就可以用來表示j了。

class Example3b{

  public static void runtwoLoops(){

  for(int i=0; i < 10; ++i){

    System.out.println(i);  

  }

  for(int j=9; j >=0; --j){

    System.out.println(j);  

  }

  }

}

  和其他運作時記憶體區一樣,虛拟機的實作者可以為局部變量區設計任意的資料結構。比如對于怎樣把long和double類型的值存儲到兩個數組項中,Java虛拟機規範沒有指定。假如某個虛拟機實作的字長為64位,這時就可以把整個long或double資料放在數組中相鄰兩數組項的低項内,而使高項保持為空。

  操作數棧  和局部變量區一樣,操作數棧也是被組織成一個以字長為機關的數組。但是和前者不同的是,它不是通過索引來通路,而是通過标準的棧操作——壓棧和出棧——來通路的。比如,如果某個指令把一個值壓人到操作數棧中,稍後另一個指令就可以彈出這個值來使用。

  虛拟機在操作數棧中存儲資料的方式和在局部變量區中是一樣的,如int、long、float、double、reference和returnType的存儲。對于byte、short以及char類型的值在壓入到操作數棧之前,也會被轉換為int。

  不同于程式計數器,Java虛拟機沒有寄存器,程式計數器也無法被程式指令直接通路。Java虛拟機的指令是從操作數棧中而不是從寄存器中取得操作數的,是以它的運作方式是基于棧的而不是基于寄存器的。雖然指令也可以從其他地方取得操作數,比如從位元組流中跟随在操作碼(代表指令的位元組)之後的位元組中或從常量池中,但是主要還是從操作數棧中獲得操作數。

  虛拟機把操作數棧作為它的工作區——大多數指令都要從這裡彈出資料,執行運算,然後把結果壓回操作數棧。比如,iadd指令就要從操作數棧中彈出兩個整數,執行加法運算,其結果又壓回到操作數棧中。看看下面的示例,它示範了虛拟機是如何把兩個int類型的局部變量相加,再把結果儲存到第三個局部變量的:

iload_0  //push the int in local variable 0

iload_l  // push the int in local variable 1

iadd // pop two ints, add them, push result

istore_2 /7 pop int, store into local variable 2

  在這個位元組碼序列裡,前兩個指令iload_0和iload_l将存儲在局部變量區中索引為0和1的整數壓人操作數棧中,其後iadd指令從操作數棧中彈出那兩個整數相加,再将結果壓入操作數棧。第四條指令istore_2則從操作數棧中彈出結果,并把它存儲到局部變量區索引為2的位置。圖5-10詳細表述了這個過程中局部變量和操作數棧的狀态變化,閣中沒有使用的局部變量區和操作數棧區域以空白表示。

  幀資料區  除了局部變量區和操作數棧外,Java棧幀還需要一些資料來支援常量池解析、正常方法傳回以及異常派發機制,這些資訊都儲存在Java棧幀的幀資料區中。

  Java虛拟機中的大多數指令都涉及到常量池入口。有些指令僅僅是從常量池中取出資料然後壓人Java棧(這些資料的類型包括int、long、float、double和String);還有些指令使用常暈池的資料來訓示要執行個體化的類或數組、要通路的字段,或要調用的方法;還有些指令需要常量池中的資料才能确定某個對象是否屬于某個類或實作了某個接口。

  每個虛拟機要執行某個需要到常量池資料的指令時,它都會通過幀數掘區中指向常量池的指針來通路它。以前講過,常景池中對類型、字段和方法的引用在開始時都是符号。當虛拟機在常量池中搜尋的時候,如果遇到指向類、接口、字段或者方法的入口,假若它們仍然是符号,虛拟機那時候才會(也必須)進行解析。

  除了用于常量池的解析外,幀資料區還要幫助虛拟機處埋Java方法的正常結束或異常中止。如果是通過return正常結束,虛拟機必須恢複發起調用的方法的棧幀,包括設定PC寄存器指向發起調用的方法中的指令---即緊跟着調用了完成方法的指令的下一個指令。假如方法有傳回值,虛拟機必須将它壓入到發起調用的方法的操作數棧,為了處理Java方法執行期間的異常退出情況,幀資料區還必須儲存一個對此方法異常表的引用。異常表會在第17章深入描述,它定義了在這個方法的位元組碼中受catch子句保護的範圍,異常表中的每一項都有一個被catch子句保護的代碼的起始和結束位置(譯者注:即try子句内部的代碼),可能被catch的異常類在常量池中的索引值,以及catch子句内的代開始的位置。

  當某個方法抛出異常時,虛拟機根據幀資料區對應的異常表來決定如何處理。如果在異常表中找到了比對的catch子句,就會把控制權轉交給catch子句内的代碼。如果沒有發現,方法會立即異常終止。然後虛拟機使用幀資料區的資訊恢複發起調用的方法的幀,然後在發起調用的方法的上下文中重新抛出同樣的異常。

  除了上述資訊(支援常量池解析、正常方法傳回和異常派發的資料)外,虛拟機的實作者也可以将其他資訊放人幀資料區,如用于調試的資料等。

本地方法棧

  前面提到的所有運作時資料區都是在Java虛拟機規範中明确定義的,除此之外,對于一個運作中的Java程式而言,它還可能會用到一些跟本地方法相關的資料區。當某個線程調用一個本地方法時,它就進入了一個全新的并且不再受虛拟機限制的世界。本地方法可以通過本地方法接口來通路虛拟機的運作時資料區,但不止于此,它還可以做任何它想做的事情。比如,它甚至可以直接使用本地處理器中的寄存器,或者直接從本地記憶體的堆中配置設定任意數量的記憶體等等。總之,它和虛拟機擁有同樣的權限(或者說能力)。

  本地方法本質上是依賴于實作的,虛拟機實作的設計者們可以自由地決定使用怎樣的機制來讓Java程式調用本地方法。

  任何本地方法接口都會使用某種本地方法棧。當線程調用Java方法時,虛拟機會建立一個新的棧幀并壓人Java棧。然而當它調用的是本地方法時,虛拟機會保持Java棧不變,不再線上程的Java棧中壓人新的幀,虛拟機隻是簡單地動态連接配接并直接調用指定的本地方法。可以把這看做是虛拟機利用本地方法來動态擴充自己。就如同lava虛拟機的實作在按照其中運作的Java程式的吩附,調用屬于虛拟機内部的另一個(動态連接配接的)方法。

  如果某個虛拟機實作的本地方法接口是使用C連接配接模型的話,那麼它的本地方法棧就是C棧。我們知道,當C程式調用一個C函數時,其棧操作都是确定的。傳遞給該函數的參數以某個确定的順序壓入棧,它的傳回值也以确定的方式傳回調用者。同樣,這就是該虛拟機實作中本地方法棧的行為。

  很可能本地方法接口需要回調Java虛拟機中的Java方法(這也是由設計者決定的),在這種情形下,該線程會儲存本地方法棧的狀态并進人到另一個Java棧。

  圖5-13描繪了這種情況,就是當一個線程調用一個本地方法時,本地方法又回調虛拟機中的另一個Java方法。這幅圖展示了Java虛拟機内部線程運作的全景圖。一個線程可能在整個生命周期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉。

深入Java虛拟機讀書筆記第五章Java虛拟機Java虛拟機

  如圖5-13所示,該線程首先調用了兩個Java方法,而第二個Java方法又調用了一個本地方法,這樣導緻虛拟機使用了一個本地方法棧。圖中的本地方法棧顯示為一個連續的記憶體空間。假設這是一個C語言棧,其間有兩個C函數,它們都以包圍在虛線中的灰色塊表示。第一個C函數被第二個Java方法當做本地方法調用,而這個C函數又調用了第二個C函數。之後第二個C函數又通過本地方法接口回調了一個Java方法(第三個Java方法),最終這個Java方法又調用了一個Java方法(它成為圖中的目前方法)。

  就像其他運作時記憶體區一樣,本地方法棧占用的記憶體區也不必是固定大小的,它可以根據需要動态擴充或者收縮。某些實作也允許使用者或者程式員指定該記憶體區的初始大小以及最大、最小值。

轉載于:https://www.cnblogs.com/waimai/p/3280185.html