關於 Java虛擬機:內存處理與執行引擎

一.Java技術體系簡介:

Java技術體系包括如下幾個組成部分:java

  1. java程序設計語言
  2. 各類硬件平臺上的java虛擬機
  3. Class文件格式
  4. Java API 類庫
  5. 來自商業機構和開源社區的第三方類庫

 

JDK(java Development Kit):包括java程序設計語言,java虛擬機,java API類庫。JDK是用於支持java程序開發的最小環境。程序員

JRE(java Runtime Environment) 包括java API類庫中的java SE API子集,java虛擬機。JRE是支持java程序運行的標準環境。算法

下圖展現了Java技術體系所包含的內容,以及JDK和JRE所涵蓋的範圍:小程序

按照技術所服務的領域來分,Java技術體系能夠分爲四個平臺,分別是:api

 

  1. java Card:支持一些java小程序(Applet)運行在小內存設備上的平臺
  2. Java ME(Micro Edition):支持java程序運行在移動終端上的平臺,對java APi 有所精簡,並加入了針對移動終端的支持。
  3. Java SE(Standard Edition):支持面向桌面級應用的java平臺,提供了完整的java核心API。
  4. Java EE(Enterprise Edition):支持使用多層架構的企業應用的java平臺,除了提供java SE API以外,還對其作了大量的擴充並提供了相關的部署支持。

二,java內存管理機制

  1. 運行時數據區域:

java虛擬機在執行java程序的過程當中會把它所管理的內存劃分爲若干不一樣的數據區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則依賴於用戶線程的啓動和結束而創建和銷燬。數組

 

Java虛擬機所管理的內存包括如下幾個運行時數據區域:安全

1.1    程序計數器:數據結構

程序計數器是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。多線程

在虛擬機的概念模型裏,字節碼解釋器工做時就是經過改變程序計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴於程序計數器來完成。架構

 

因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個核心)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲線程私有的內存

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

程序計數器是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。

 

1.2  Java虛擬機棧

與程序計數器同樣,java虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame,棧幀是方法運行時的基礎數據結構)用於存儲局部變量表,操做數棧,動態連接,方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

對於C/C++等程序來講,其內存管理經常分爲棧、堆等。對於Java,棧即指代虛擬機棧,或者說是虛擬機棧中局部變量表部分。

局部變量表存放了編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同於對象自己,多是一個指向對象起始地址的引用地址,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。

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

能夠經過 -Xss 這個虛擬機參數來指定一個程序的 Java 虛擬機棧內存大小:

java -Xss=512M HackTheJava

該區域可能拋出如下異常:

  • 當線程請求的棧深度超過最大值,會拋出StackOverflowError 異常;
  • 棧進行動態擴展時若是沒法申請到足夠內存,會拋出OutOfMemoryError 異常。

在Java虛擬機規範中,對這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是虛擬機棧能夠動態擴展,若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常

 

1.3本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們的區別是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。與虛擬機棧同樣,本地方法棧也會拋出StackOverflowError異常和OutOfMemoryError異常。

 

 

1.4     Java堆(Heap)

對於大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例。在JVM中,幾乎全部的對象實例都在這裏分配內存。Java虛擬機規範中的描述是:全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展和逃逸技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也變得不是那麼絕對了

Java堆是垃圾收集器管理的主要區域,所以,Java堆也被稱爲「GC堆」(Garbage Collected Heap)。

現代的垃圾收集器基本都是採用分代收集算法,該算法的思想是針對不一樣的對象採起不一樣的垃圾回收算法,所以虛擬機把Java堆分紅如下三塊:

  • 新生代(Young Generation)
  • 老年代(Old Generation)
  • 永久代(Permanent Generation)

當一個對象被建立時,它首先進入新生代,以後有可能被轉移到老年代中。新生代存放着大量的生命很短的對象,所以新生代在三個區域中垃圾回收的頻率最高。爲了更高效的進行垃圾回收,把新生代繼續劃分爲如下三個空間:

  • Eden
  • From Survivor
  • To Survivor
  • 從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer, TLAB)。根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間,只要邏輯上是連續的便可。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。

     

    能夠經過 -Xms 和 -Xmx 兩個虛擬機參數來指定一個程序的 Java 堆內存大小,第一個參數設置最小值,第二個參數設置最大值。

    java -Xms=1M -XmX=2M HackTheJava

     

    1.5方法區

    方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該與Java堆區分開來。

     

    Java虛擬機規範對方法區的限制很是寬鬆,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾回收。相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入了方法區就如同永久代名字同樣永久存在。該區域的內存回收目標主要是針對常量池的回收和對類型的卸載。

     

    根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。

     

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

     

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

     

    既然運行時常量區是方法區的一部分,當常量池沒法申請到內存時會拋出OutOfMemoryError異常。

     

     

    Class類文件解析

    java:一次編寫,處處運行。Write Once,Run Anywhere。

    各類不一樣平臺的虛擬機與全部平臺都統一使用的程序存儲格式——字節碼(ByteCode是構成平臺無關性的基石。

    Java虛擬機不和包括Java在內的任何語言綁定,它只與「Class文件」這種特定的二進制文件格式所關聯。

    Class文件中包含了Java虛擬機指令集和符號表以及若干其餘輔助信息。

    1. 1.    Class類文件的結構

     

    任何一個Class文件都對應着惟一一個類或接口的定義信息,但反過來講,類或接口並不必定都得定義在文件裏(譬如類或接口也能夠經過類加載器直接生成)。

    Class文件是一組以8位字節爲基礎單元的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符。

    Class文件中只有兩種數據類型:無符號數和表

     

    無符號數屬於基本的數據類型,能夠用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。

    表是有多個無符號數或者其餘表做爲數據項構成的複合數據類型,全部表都習慣性地以「_info」結尾。

    Class文件中的數據項,不管是順序仍是數量,甚至於數據存儲的字節序,都是被嚴格限定的,哪一個字節表明什麼含義,長度是多少,前後順序如何,都不容許改變。

    Class文件格式以下:

    • 常量池(constant_pool
  • 常量池入口,常量池能夠理解爲Class文件之中的資源倉庫,它是Class文件結構中與其餘項目關聯最多的數據類型,也是佔用Class文件空間最大的數據項目之一,同時它仍是在Class文件中第一個出現的表類型數據項目。

     

    常量池中主要存放兩大類常量:字面量和符號引用

     

    字面量比較接近於Java層面的常量概念,如文本字符串、被聲明爲final的常量值等。

     

    符號引用總結起來則包括了下面三類常量:

    • 類和接口的全限定名(即帶有包名的Class名,如:org.lxh.test.TestClass)
    • 字段的名稱和描述符(private、static等描述符)
    • 方法的名稱和描述符(private、static等描述符)

     

    虛擬機在加載Class文件時纔會進行動態鏈接,也就是說,Class文件中不會保存各個方法和字段的最終內存佈局信息,所以,這些字段和方法的符號引用不通過轉換是沒法直接被虛擬機使用的。當虛擬機運行時,須要從常量池中得到對應的符號引用,再在類加載過程當中的解析階段將其替換爲直接引用,並翻譯到具體的內存地址中。

     

    符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到了內存中。

    直接引用:直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那說明引用的目標一定已經存在於內存之中了。

     

    • 訪問標誌(access_flags

     

    訪問標誌(access_flags,這個標誌用於識別一些類或者接口層次的訪問信息。包括:

    • 這個Class是類仍是接口;
    • 是否認義爲public類型;
    • 是否認義爲abstract類型;
    • 若是是類的話,是否被聲明爲final等。

     

    訪問標誌包括public/protected/private/abstract/final等等。

     

     

     

    虛擬機類加載機制:

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

     

    類加載過程

    在Java語言裏面,類型的加載、鏈接、初始化過程都是在程序運行期間完成的。

     

    特色:靈活性、動態擴展(運行期動態加載和動態鏈接)

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

     

    • 加載(Loading
    • 驗證(Verification
    • 解析(Resolution
    • 初始化(Initialization
    • 使用(Using
    • 卸載(Unloading

    那麼,什麼狀況下須要開始類加載過程的第一個階段加載呢?!!有且只有五種狀況!!

    • 遇到new/getstatic/putstatic/invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化(分別對應於:使用new實例化對象、讀取或設置類的靜態字段、調用一個類的靜態方法)。
    • 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
    • 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
    • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
    • 當使用動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。

     

    被動引用:

    • 經過子類引用父類的靜態字段,不會致使子類初始化;
    • 經過數組定義來引用類,不會觸發此類的初始化;
    • 常量在編譯階段會調入類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。(常量傳播優化)

    對於接口的加載過程,咱們須要注意的是:當一個類在初始化時,要求其父類所有都已經初始化過了,可是一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有在真正使用到父接口時纔會初始化。

     

    類加載過程主要包括加載、驗證、準備、解析和初始化5個階段。

     

    1.1 加載

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

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

     

    1.2 驗證

    驗證階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害到虛擬機自身的安全。驗證是虛擬機對自身保護的一項重要工做。

     

    從總體上看,驗證階段大體上會完成下面4個階段的檢驗動做:

    • 文件格式驗證

    第一階段要驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。該驗證階段的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內,格式上符合一個Java類型信息的要求

    • 元數據驗證

    第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。該驗證階段的主要目的是對類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息

    • 字節碼驗證

    第三階段是對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件。該驗證階段的主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的

    • 符號引用驗證

    第四階段是對類自身之外的信息進行匹配性校驗。該驗證階段的主要目的是確保解析動做能正常執行,若是沒法經過符號引用驗證,那麼將會拋出一個java.lang.IncompatibleClassChangeError異常的子類

    對於虛擬機的類加載機制來講,驗證階段是一個很是重要的、但不是必定必要的階段。

    1.3 準備

    準備階段是爲類變量分配內存並設置類變量初始值的階段。

    注意:此時進行內存分配的僅包括類變量(static修飾的變量),而不包括實例變量。

    考慮下面一個問題:

    試比較下面兩種狀況下在準備階段後value對應的值是多少。

    // 情形一

    public static int value = 123;

     

    // 情形二

    public static final int value = 123;

    答案是:對於情形一,準備階段後value的值爲0;對於情形二,準備階段後value的值爲123。

    緣由在於,情形一下value的賦值操做是在<init>部分完成的,而在情形二下,value對應爲ConstantValue屬性。

     

    1.4 解析

    解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程

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

    虛擬機規範中並未規定解析階段發生的具體時間。

    解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

    1.5 初始化

    類初始化階段是類加載的最後一步。在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源。也就是說,初始化階段是執行類構造器<clinit>方法的過程。

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

    public class Test {

        static {

            i = 0;    // 給變量賦值能夠正常編譯經過

            System.out.println(i); // 非法前向引用!!!

        }

     

        static int i = 1;

    }

     

    • <clinit>方法與類的構造函數(實例構造器<init>)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>方法執行以前,父類的<clinit>方法已經執行完畢
    • 因爲父類的<clinit>方法先執行,也就有,父類中定義的靜態語句塊要優先於子類的變量賦值操做
    • <clinit>方法對於類或接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>方法。
    • 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做。可是接口與類不一樣的是,執行接口的<clinit>方法不須要先執行父接口的<clinit>方法,只有當父接口中定義的變量使用時,父接口才會初始化
    • 虛擬機會保證一個類的<clinit>方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>方法完畢。同時,須要注意的是,其餘線程雖然會被阻塞,但若是執行<clinit>方法的那條線程退出<clinit>方法後,其餘線程喚醒以後不會再次進入<clinit>方法。同一個類加載器下,一個類型只會初始化一次。

     

     

    2. 類加載器

    虛擬機設計團隊把類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流 ( 即字節碼 )」這個動做放到 Java 虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼模塊稱爲「類加載器」。

    2.1 類與類加載器

    對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在 Java 虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。通俗而言:比較兩個類是否「相等」(這裏所指的「相等」,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果,也包括使用 instanceof() 關鍵字作對象所屬關係斷定等狀況),只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。

    2.2 類加載器分類

    從 Java 虛擬機的角度來說,只存在如下兩種不一樣的類加載器:

    • 啓動類加載器(Bootstrap ClassLoader),這個類加載器用 C++ 實現,是虛擬機自身的一部分;
    • 全部其餘類的加載器,這些類由 Java 實現,獨立於虛擬機外部,而且全都繼承自抽象類 java.lang.ClassLoader。

    從 Java 開發人員的角度看,類加載器能夠劃分得更細緻一些:

    • 啓動類加載器(Bootstrap ClassLoader) 此類加載器負責將存放在 <JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即便放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。 啓動類加載器沒法被 Java 程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給啓動類加載器,直接使用 null 代替便可。
    • 擴展類加載器(Extension ClassLoader) 這個類加載器是由 ExtClassLoader實現的。它負責將<JAVA_HOME>/lib/ext或者被 java.ext.dir系統變量所指定路徑中的全部類庫加載到內存中,開發者能夠直接使用擴展類加載器。
    • 應用程序類加載器(Application ClassLoader) 這個類加載器是由AppClassLoader實現的。因爲這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
相關文章
相關標籤/搜索