什麼是Java虛擬機?
Java虛擬機,從字面上來看,像是某種機器,但Java虛擬機之所以被稱之爲“虛擬”的,是因爲它是由一個規範來定義的抽象計算機,所以在我們說Java虛擬機的時候,可能指的是如下三種不同的東西:
抽象規範
一個具體的實現
一個運行中的虛擬機實例
Java虛擬機的生命週期
當啓動一個Java程序時,一個虛擬機實例也就誕生了。該程序關閉退出時,虛擬機實例也就隨之消亡。Java虛擬機通過調用某個初始類的main()方法作爲Java程序運行的起點。在Java虛擬機內容,有兩種線程,守護線程和非守護線程。守護線程一般爲虛擬機自己使用,比如垃圾收集線程,非守護線程比如運行main()的線程。當程序中所有非守護線程終止時,則虛擬機實例自動退出。
Java虛擬機的體系結構
在Java虛擬機規範中,一個虛擬機實例的行爲按照子系統、內存區、數據類型以及指令幾個術語來描述。每個Java虛擬機都有一個類裝載子系統,它根據全限定名來裝入系統,同樣,每個Java虛擬機都有一個執行引擎,它負責執行那些包含在被裝載類的方法中的指令。
當Java虛擬機運行一個程序時,它需要內存來存儲許多東西,比如,字節碼、對象、局部變量、運算的中間結果等。某些運行時數據區由程序中所有線程共享,還有一些只能由一個線程擁有。每個虛擬機都有一個方法區和一個堆,他們是由該虛擬機中所有線程共享的。當虛擬機裝載一個class文件時,會把類型信息存入方法區,程序運行時虛擬機會把運行時創建的對象存入堆中。
每當一個新線程創建的時候,它都將獲得它自己的PC寄存器和一個Java棧,若線程正在執行一個Java方法(非本地方法),那麼PC計數器的值總是指示下一條將被執行的指令,Java棧則包含線程中Java方法的調用狀態—-包括它的局部變量,被調用時傳進來的參數、它的返回值、計算的中間結果等。本地方法調用,則是以某種依賴於具體實現的方式存儲在本地方法棧中,也可能是寄存器和其他某些與特定實現相關的內存區。
Java棧是由許多棧幀或者說幀組成,一個棧幀包含一個Java方法的調用狀態。當線程調用一個Java方法時,虛擬機壓入一個新的棧幀到棧中,方法返回後此棧幀彈出並拋棄。
數據類型
數據類型可分爲兩類:基本類型和引用類型,基本類型持有原始值,引用類型持有引用值。Java基本類型的值域在任何地方都是一致的,比如一個long類型在任何虛擬機中都是64位二進制補碼錶示的有符號整數。
注意:boolean有些特別,雖然boolean也是基本類型,但是在編譯爲字節碼時,它會用int或者byte來表示boolean,false表示爲整數0,true爲整數1。另外boolean數組是被當做byte數組類訪問的。
字長的考量
Java虛擬機中,最基本的數據單元就是字,虛擬機實現者最少選擇32位作爲字長,或者選擇更爲高效的字長。通常根據底層主機平臺的指針長度來選擇字長。
類裝載器子系統
Java虛擬機中有兩種類裝載器:啓動類裝載器和用戶自定義裝載器。前者是Java虛擬機實現的一部分,後者是Java程序的一部分。不同類裝載器放在虛擬機內部的不同命名空間中。
ClassLoader中定義的方法爲程序提供了訪問類裝載器機制的接口。每一個被裝載的類型,Java虛擬機都會爲它創建一個java.lang.Class類的實例來代表該類型。用戶自定義的類裝載器以及Class類的實例都放在內存中的堆區,而裝載的類型信息則都位於方法區。
類裝載器除了要定位和導入二進制class文件外,還需要負責導入類的正確性,分配變量和初始化內存,解析符號引用等。這些動作必須嚴格按照以下步驟進行:
1、裝載 ——– 查找並載入二進制數據
2、連接 ——– 執行驗證,準備,已經解析(可選)
驗證: 確保被導入類型的正確性
準備:爲類變量分配內存,並將其初始化爲默認值
解析:把類型中的符號引用轉換爲直接引用
3、初始化 ——– 把類變量初始化爲正確的初始值
方法區
在Java虛擬機中,關於被裝載類型的信息存儲在一個邏輯上被稱爲方法區的內存中。當虛擬機裝載某個類型時,首先使用類裝載器定位相應的class文件,然後讀入class文件 ——- 一個線性二進制數據流,然後將它傳輸到虛擬機中。之後虛擬機提取出其中的類型信息存入方法區,同時該類中的靜態變量也是存儲在方法區中。
所有線程都共享方法區,所以它們對方法區的訪問必須爲線程安全的,比如,如果兩個線程同時都企圖訪問名爲Lava的類,而此類尚未裝載進虛擬機,那麼,這時只應該有一個線程去裝載它,另一個只能等待。
方法區大小不是固定的,可以根據需要自己調整。同樣方法區也不必是連續的,方法區可以在同一個堆中自由分配,也可以由程序員指定方法區的初始大小的最大尺寸和最小尺寸等。
方法區也可以被垃圾收集,虛擬機允許用戶定義的類裝載器來動態擴展Java程序(反射),因此一些類也會成爲程序“不再引用”的類。當某個類不再被引用時,Java虛擬機可以卸載此類。
對應每個裝載的類型,虛擬機會在方法區存儲以下類型信息:
此類的全限定名
此類的直接超類全限定名
此類是類類型還是接口類型
此類的訪問修飾符
任何直接超類的全限定名的有序列表
除以上列出的基本類型信息,虛擬機還得爲每個裝載的類型存儲以下信息
該類型的常量池
字段信息
方法信息
除了常量以外所有類的(靜態)變量
一個到類的ClassLoader引用
一個到Class類的引用
堆
Java程序運行時創建的所有類實例和數組都放在同一個堆中,而一個Java虛擬機實例中只有存在一個堆空間,因此所有線程都將共享這個堆。由於每一個Java程序獨佔一個堆空間,因此所有的線程將共享這個堆。但是同一個程序的多個線程卻共享着同一個空間,此種情況下,需要考慮多線程訪問對象(堆數據)的同步問題。
Java虛擬機有一條在堆中分配新對象的指令,卻沒有釋放內存的指令。正如我們無法用Java代碼去明確釋放一個對象一樣,字節碼中也沒有相關功能。需要虛擬機自己負責決定如何已及何時開始垃圾收集。程序本身也不需要關心何時回收,通常虛擬機把這個任務交給垃圾收集器。
Java虛擬機規範並沒有規定Java對象在堆中是如何表示的。對象的內部表示影響整個堆以及垃圾收集器的設計,它由虛擬機的實現者決定。
一種可能的堆空間設計爲,把堆分爲兩個部分:一個句柄池,一個對象池,而一個對象引用則是指向句柄池的本地指針。句柄池分爲兩個部分:一個指向對象實例變量的指針,一個指向方法區類型的指針。這種設計的有點爲有利於堆碎片的整理,缺點爲每次訪問對象都要經過兩次指針的傳遞。