Java基礎:Java虛擬機器(JVM)

南方吳彥祖_藍斯發表於2021-10-25

1. JVM是什麼

JVM是Java Virtual Machine的縮寫。它是一種基於計算裝置的規範,是一臺虛擬機器,即虛構的計算機。
JVM遮蔽了具體作業系統平臺的資訊(顯然,就像是我們在電腦上開了個虛擬機器一樣),當然,JVM執行位元組碼時實際上還是要解釋成具體操作平臺的機器指令的。
透過JVM,Java實現了平臺無關性,Java語言在不同平臺執行時不需要重新編譯,只需要在該平臺上部署JVM就可以了。因而能實現一次編譯多處執行。(就像是你的虛擬機器也可以在任何安了VMWare的系統上執行)

2. JRE和JDK

JRE:Java Runtime Environment,也就是JVM的執行平臺,聯絡平時用的虛擬機器,大概可以理解成JRE=虛擬機器平臺+虛擬機器本體(JVM)。類似於你電腦上的VMWare+適用於VMWare的Ubuntu虛擬機器。這樣我們也就明白了JVM到底是個什麼。
JDK:Java Develop Kit,Java的開發工具包,JDK本體也是Java程式,因此執行依賴於JRE,由於需要保持JDK的獨立性與完整性,JDK的安裝目錄下通常也附有JRE。目前Oracle提供的Windows下的JDK安裝工具會同時安裝一個正常的JRE和隸屬於JDK目錄下的JRE。

3. JVM結構

JVM主要包括:程式計數器(Program Counter),Java堆(Heap),Java虛擬機器棧(Stack),本地方法棧(Native Stack),方法區(Method Area)
詳細的結構如下:
Java基礎:Java虛擬機器(JVM)

現在我來分別介紹一下每一部分的功能。

3.1. 程式計數器(PC, Program Counter)

是一個暫存器,可以看作是程式碼行號指示器,類似於實際計算機裡的PC,用於指示,跳轉下一條需要執行的命令。Java的基礎操作以及異常處理等都十分依賴PC。
JVM多執行緒是透過執行緒輪流切換並分配處理器執行時間的方式來實現的。在一個確定的時刻,一個處理器(或者說多核處理器的一個核心)只會執行一條執行緒中的命令。因此,為了正常的切換執行緒,每個執行緒都會有一個獨立的PC,各執行緒的PC不會互相影響。這個私有的PC所佔的這塊記憶體即是執行緒的“私有記憶體”。
如果執行緒在執行的是Java方法,那麼PC記錄的是正在執行的虛擬機器位元組碼指令的地址。如果正在執行的不是Java方法即Native方法,那麼PC的值為undefined。
PC的記憶體區域是唯一的沒有規定任何OutOfMemoryError的Java虛擬機器規範中的區域。

3.2. Java虛擬機器棧(Stack,Java Virtual Mechine Stacks)

同PC一樣(從工作流程圖裡我們可以看到,實際上,PC也是存在於JVM Stack上的),也是執行緒私有的,生命週期與執行緒相同。虛擬機器棧描述Java方法執行的記憶體模型,每個方法被執行時都會建立一個棧幀(Stack Frame),棧幀會利用區域性變數陣列儲存區域性變數(Local Variables),操作棧(Operand Stack),方法出口(Return Value),動態連線(Current Class Constant Pool Reference)等資訊。
區域性變數陣列儲存了編譯可知的八個基本型別(int, boolean, char, short, byte, long, float, double),物件引用(根據不同的虛擬機器實現可能是引用地址的指標或者一個handle),returnAddress型別。64位的long和double會佔用兩個Slot,其餘型別會佔用一個Slot。在編譯期間,區域性變數所需的空間就會完成分配,動態執行期間不會改變所需的空間。
操作棧在執行位元組碼指令時會被用到,這種方式類似於原生的CPU暫存器,大部分JVM把時間花費在操作棧的花費上,操作棧和區域性變數陣列會頻繁的交換資料。
動態連線控制著執行時常量池和棧幀的連線。所有方法和類的引用都會被當作符號的引用存在常量池中。符號引用是實際上並不指向實體記憶體地址的邏輯引用。JVM 可以選擇符號引用解析的時機,一種是當類檔案載入並校驗透過後,這種解析方式被稱為飢餓方式。另外一種是符號引用在第一次使用的時候被解析,這種解析方式稱為惰性方式。無論如何 ,JVM 必須要在第一次使用符號引用時完成解析並丟擲可能發生的解析錯誤。繫結是將物件域、方法、類的符號引用替換為直接引用的過程。繫結只會發生一次。一旦繫結,符號引用會被完全替換。如果一個類的符號引用還沒有被解析,那麼就會載入這個類。每個直接引用都被儲存為相對於儲存結構(與執行時變數或方法的位置相關聯的)偏移量。
對Java虛擬機器棧這個區域,Java虛擬機器規範規定了兩種異常:
  • 執行緒請求的棧深度大於虛擬機器所允許的深度,丟擲StackOverFlow異常。
  • 對於支援動態擴充套件的虛擬機器,當擴充套件無法申請到足夠的記憶體時會丟擲OutOfMemory異常。

3.3. 本地方法棧(Native Stack)

本地方法棧如其名字,和Java Virtual Machine Stack其實極為類似,只是執行的是Native方法,為Native方法服務。在JVM規範中,沒有對它的實現做具體規定。

3.4. Java 堆(Heap, Garbage Collection Heap)

Java基礎:Java虛擬機器(JVM)

Java堆是被所有執行緒共享的一塊區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體(隨著技術的發展,已不絕對)。
Java堆是垃圾收集器管理的主要區域,因而也被稱為GC堆。收集器採用分代回收法,GC堆可以分為新生代(Yong Generation)和老生代(Old Generation)。新生代包括Eden Space和Survivor Space。但無論哪個區域,如何劃分,儲存的都是Java物件例項,進一步的劃分是為了更好的回收記憶體或快速的分配記憶體。
根據Java虛擬機器規範,堆所在的實體記憶體區間可以是不連續的,只要邏輯連續就可以。實現時既可以是固定大小,也可以是可擴充套件的。如果堆無法擴充套件時,就會丟擲OutOfMemoryError。

3.5. 方法區(Method Area)

方法區和Java堆類似,也屬於各執行緒共享的記憶體區域。用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼資料等。它屬於非堆區(Non Heap),和Java堆區分開。對於存在永久代(Permanent)概念的虛擬機器(HotSpot)而言,方法區存在於永久代。Java虛擬機器規範對方法區的規定很寬鬆,甚至可以不實現GC。不過並非進入方法區的資料就會永久存在了,這塊區域的記憶體回收主要為常量池的回收和型別的解除安裝。這個區域的回收處理不善也會導致嚴重的記憶體洩漏。
當方法區無法滿足記憶體分配需求時也會丟擲OutOfMemoryError。

3.6. 程式碼快取(Code Cache)

用於編譯和儲存那些被 JIT 編譯器編譯成原生程式碼的方法。

3.7. 類資訊(Class Data)

類資訊儲存在方法區,其主要構成為執行時常量池(Run-Time Constant Pool)和方法(Method Code)。
一個編譯後的類檔案包括以下結構:
結構
解釋
magic, minor_version, major_version
類檔案的版本資訊和用於編譯這個類的 JDK 版本。
constant_pool
類似於符號表,儘管它包含更多資料。下面有更多的詳細描述。
access_flags
提供這個類的描述符列表。
this_class
提供這個類全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar。
super_class
提供這個類的父類符號引用的常量池索引。
interfaces
指向常量池的索引陣列,提供那些被實現的介面的符號引用。
fields
提供每個欄位完整描述的常量池索引陣列。
methods
指向constant_pool的索引陣列,用於表示每個方法簽名的完整描述。如果這個方法不是抽象方法也不是 native 方法,那麼就會顯示這個函式的位元組碼。
attributes
不同值的陣列,表示這個類的附加資訊,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 註解。

3.8. 執行時常量池(Run-Time Constant Pool)

執行時常量池是方法區的一部分。Class檔案中有類的版本,欄位,方法,介面等描述資訊和用於存放編譯期生成的各種字面量和符號引用。這部分內容將在類載入後存放到方法區的執行時常量池中。Java虛擬機器規範對Class的細節有著嚴苛的要求而對執行時常量池的實現不做要求。一般來說除了翻譯的Class,翻譯出來的直接引用也會存在執行時常量池中。
執行時常量池具備動態性,即執行時也可將新的常量放入池中。比如String類的intern()方法。
常量池無法申請到足夠的記憶體分配時也會丟擲OutOfMemoryError。

3.9. 直接記憶體(Direct Memory)

直接記憶體並不在Java虛擬機器規範中,不是Java的一部分,但是也被頻繁使用並可能導致OutOfMemoryError。Native函式庫可以直接分配堆外記憶體,透過儲存在Java堆裡的DirectDataBuffer物件作為這塊記憶體的引用進行操作。這樣做在一些場景中可以顯著提高效能。
直接記憶體是堆外記憶體,自然不受Java堆大小的限制,但是可能受實體機記憶體大小的限制。如果記憶體各部分總和大於實體機的記憶體時,也會報出OutOfMemoryError。

4. Java垃圾回收

將記憶體中不再被使用的物件進行回收,GC中用於回收的方法稱為收集器,由於GC需要消耗一些資源和時間,Java在對物件的生命週期特徵進行分析後,按照新生代、舊生代的方式來對物件進行收集,以儘可能的縮短GC對應用造成的暫停。
不同的物件引用型別, GC會採用不同的方法進行回收,JVM物件的引用分為了四種型別:
  • 強引用:預設情況下,物件採用的均為強引用(這個物件的例項沒有其他物件引用,GC時才會被回收)。
  • 軟引用:軟引用是Java中提供的一種比較適合於快取場景的應用(只有在記憶體不夠用的情況下才會被GC)。
  • 弱引用:在GC時一定會被GC回收。
  • 虛引用:由於虛引用只是用來得知物件是否被GC。

5. JVM執行緒與原生執行緒的關係

JVM允許一個程式使用多個併發執行緒,Hotspot JVM中Java的執行緒與原生作業系統的執行緒是直接對映關係。即當執行緒本地儲存、緩衝區分配、同步物件、棧、程式計數器等準備好以後,就會建立一個作業系統原生執行緒。Java 執行緒結束,原生執行緒隨之被回收。作業系統負責排程所有執行緒,並把它們分配到任何可用的 CPU 上。當原生執行緒初始化完畢,就會呼叫 Java 執行緒的 run() 方法。run() 返回時,被處理未捕獲異常,原生執行緒將確認由於它的結束是否要終止 JVM 程式(比如這個執行緒是最後一個非守護執行緒)。當執行緒結束時,會釋放原生執行緒和 Java 執行緒的所有資源。

版權宣告:本文為部落格園博主CieloSun的原創文章, 原文連結: Java基礎:Java虛擬機器(JVM) - CieloSun - 部落格園


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2839143/,如需轉載,請註明出處,否則將追究法律責任。

相關文章