一、JVM 運行流程
JVM 是 Java 程序的運行基礎和運行環(huán)境,同時也是 Java 實現(xiàn) "一次編譯,到處運行"
的關鍵所在。因此,深入了解 JVM 對于學習和理解 Java 編程語言是至關重要的,那么JVM 到底是如何運行的呢?
下面這張圖片展示了 JVM 的基本運行過程:
JVM的執(zhí)行過程涉及以下主要組成部分::
-
Java 代碼轉換為字節(jié)碼(
.class
文件):編寫完成后的 Java 源代碼需要通過 Java 編譯器編譯為字節(jié)碼,生成.class
文件,這些文件包含了 Java 程序的中間代碼。 -
類加載器(ClassLoader):JVM 的類加載器負責將
.class
字節(jié)碼文件加載到內存中的運行時數(shù)據區(qū)
。類加載器根據類的全限定名(Fully Qualified Name)查找并加載對應的字節(jié)碼文件,并根據文件內容創(chuàng)建 Class 對象來代表這個類,以供后續(xù)的執(zhí)行引擎調用。 -
運行內存管理:JVM負責管理程序運行時的內存區(qū)域,主要包括以下幾個區(qū)域:
- 虛擬機棧(Java Virtual Machine Stacks):用于存儲方法調用的棧幀,包含局部變量、操作數(shù)棧等。
- 本地方法棧(Native Method Stacks):用于執(zhí)行本地方法的棧。
- 方法區(qū)(Method Area):存儲類的結構信息、常量池、靜態(tài)變量等。
- 堆(Heap):存儲對象實例和數(shù)組的內存區(qū)域。
- 程序計數(shù)器(Program Counter):記錄當前線程執(zhí)行的字節(jié)碼指令的地址或索引。
-
執(zhí)行引擎(Execution Engine):執(zhí)行引擎是 JVM 的核心組件之一,負責執(zhí)行加載到內存中的字節(jié)碼文件。執(zhí)行引擎有兩種方式執(zhí)行字節(jié)碼:
- 解釋執(zhí)行:逐條解釋字節(jié)碼指令并執(zhí)行相應的操作。解釋執(zhí)行效率較低,但跨平臺性好,適用于剛開始執(zhí)行的代碼段或是執(zhí)行次數(shù)較少的代碼段。
- 編譯執(zhí)行:將字節(jié)碼編譯成特定平臺的本地代碼,然后交由CPU執(zhí)行。編譯執(zhí)行效率高,但需要額外的編譯時間,適用于執(zhí)行次數(shù)頻繁的代碼段。
-
本地方法庫(Native Libraries):JVM 中的 Java 代碼無法直接訪問底層操作系統(tǒng),因為字節(jié)碼只是一套跨平臺的指令集規(guī)范。當 Java 代碼需要執(zhí)行底層操作系統(tǒng)或與本地代碼(如C、C++)進行交互的時候。就需要通過本地方法庫來實現(xiàn)這些功能。本地方法庫允許 Java 程序調用與操作系統(tǒng)相關的本地代碼,從而實現(xiàn)整個程序的功能。
以上就是 JVM 的主要運行過程以及運行時涉及的組成部分,下文是對其中的運行時數(shù)據區(qū)的詳細介紹。
二、虛擬機棧(線程私有)
虛擬機棧是 Java 線程私有的內存區(qū)域,用于存儲線程的方法調用和局部變量等信息。虛擬機棧的生命周期和線程相同,虛擬機棧描述的是 Java 方法執(zhí)行的內存模型:
- 每個方法在執(zhí)行的時候都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
- 當方法結束的時候,響應的棧幀也會因為出棧而銷毀。
虛擬機棧主要包含四個部分:
- 局部變量表:用于存儲方法的局部變量。在Java方法執(zhí)行時,它會分配一塊內存區(qū)域,用于存儲方法參數(shù)和方法內部定義的局部變量。局部變量表的大小在編譯期間就被確定,存儲的數(shù)據類型包括基本數(shù)據類型和對象引用。
- 操作棧:用于執(zhí)行方法時的計算操作。Java虛擬機的字節(jié)碼指令通常都是基于操作數(shù)棧進行運算的。當一個方法被調用時,會將參數(shù)值和返回值等壓入操作數(shù)棧,在方法執(zhí)行時,字節(jié)碼指令會從操作數(shù)棧中取值進行計算。
- 動態(tài)鏈接:用于指向運行時常量池中該棧幀所屬方法的引用。在Java虛擬機的運行時常量池中,存放著每個類的方法的符號引用,動態(tài)鏈接將這些符號引用與實際內存地址進行關聯(lián)。
- 方法返回地址:用于指示方法的返回地址。當方法執(zhí)行完成后,需要知道從哪里繼續(xù)執(zhí)行,方法返回地址就是用來記錄返回的目標地址。
什么是線程私有:
- 線程私有是指在多線程環(huán)境下,每個線程都擁有自己獨立的內存區(qū)域,其他線程無法直接訪問或修改該區(qū)域。線程私有內存中的數(shù)據對于每個線程都是獨立的,互不影響。這種設計使得多線程程序可以同時執(zhí)行,每個線程都能夠獨立地運行和維護自己的數(shù)據。
- 在JVM中,
虛擬機棧
、本地方法棧
和程序計數(shù)器
是線程私有的內存區(qū)域。每個線程都有自己的虛擬機棧和本地方法棧,用于支持方法調用和執(zhí)行,并且這些棧的數(shù)據對其他線程是不可見的。程序計數(shù)器也是線程私有的,每個線程都有自己的程序計數(shù)器,用于指示當前線程執(zhí)行的字節(jié)碼指令的地址或索引。線程的切換時,會保存和恢復程序計數(shù)器的值,以保證線程切換后能正確繼續(xù)執(zhí)行。- 其他共享內存區(qū)域如
堆
和方法區(qū)(元空間)
是線程共享的。堆用于存儲Java對象實例和數(shù)組,方法區(qū)用于存儲類的結構信息、常量池、靜態(tài)變量等。多個線程可以共同訪問堆和方法區(qū)的數(shù)據,因此在多線程環(huán)境下,需要通過同步機制來保護共享數(shù)據的一致性和正確性。- 內存區(qū)域按線程私有和共享的劃分:
![]()
三、本地方法棧 (線程私有)
本地方法棧與虛擬機棧類似,也是 Java 線程私有的內存區(qū)域。它用于支持 Java 程序調用和執(zhí)行本地方法(Native Method)。本地方法是使用其他語言(如C、C++)編寫的方法,通過本地方法接口(JNI,Java Native Interface)與 Java 代碼進行交互。本地方法棧和虛擬機棧在功能上也是類似的,但它們分別用于 Java 方法和本地方法的調用。
四、方法區(qū)(元數(shù)據區(qū))
方法區(qū)是 Java 線程共享的內存區(qū)域,用于存儲類的結構信息、常量池、靜態(tài)變量、即時編譯器編譯后的代碼等。在 JDK 8 及以前版本,方法區(qū)是永久代(Permanent Generation)
;而在 JDK 8 及以后版本,方法區(qū)被替換為元空間(Metaspace)
。方法區(qū)的大小可以通過啟動JVM 時的參數(shù)來設置。
在《Java虛擬機規(guī)范中》把此區(qū)域稱之為 “方法區(qū)”,而在 HotSpot 虛擬機的實現(xiàn)中,在 JDK 7 時此區(qū)域叫做永久代(Permanent Generation),JDK 8 中叫做元空間(Metaspace)。
JDK 1.8 元空間的變化:
- 對于 HotSpot 來說,JDK 8 元空間的內存屬于本地內存,這樣元空間的大小就不在受 JVM 最大內存的參數(shù)影響了,而是與本地內存的大小有關。
- JDK 8 中將字符串常量池移動到了堆中。
運行時常量池:
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分,用于存放在編譯期間生成的各種字面量和符號引用。它是在類加載過程中,由虛擬機根據字節(jié)碼文件中的常量池表構建而成。
在運行時常量池中,主要包含兩種類型的數(shù)據:
-
字面量:
- 字符串字面量:即Java程序中直接寫的字符串值,例如:“Hello, Java”。在JDK 8中,字符串字面量被移動到堆中,這樣它們也可以被垃圾收集器回收,避免了一些內存問題。
-
final
常量:在編譯期間可以確定的final
常量,例如:final int MAX_VALUE = 100;
。 - 基本數(shù)據類型的值:例如:整數(shù)、浮點數(shù)、字符等基本數(shù)據類型的字面量。
-
符號引用:
符號引用是一種在編譯期間產生的、但在類加載階段需要用到的一種數(shù)據結構,它用于描述被引用的目標。符號引用包括:- 類和接口的完全限定名:例如:
java.lang.String
。 - 字段的名稱和描述符:用于描述字段的名稱和數(shù)據類型,例如:
int count
。 - 方法的名稱和描述符:用于描述方法的名稱和參數(shù)列表以及返回值類型,例如:
void print(String message)
。
- 類和接口的完全限定名:例如:
運行時常量池在類加載后,會存放在方法區(qū)中,并且在整個類的生命周期中存在,與類本身一起被回收。它在程序運行時提供了常量池解析、動態(tài)鏈接、方法調用等功能,為 Java 程序的執(zhí)行提供了必要的支持。
五、堆(線程共享)
堆(Heap)是 Java 虛擬機中的一個運行時數(shù)據區(qū)域,用于存儲Java對象實例和數(shù)組。堆是JVM中最大的一塊內存區(qū)域,也是唯一被所有線程共享的內存區(qū)域。
在Java程序運行過程中,當使用new
關鍵字創(chuàng)建對象時,對象實例會被分配在堆中。同時,數(shù)組也是對象,因此數(shù)組的元素也會存儲在堆中。堆的大小可以在啟動 JVM 時通過參數(shù)來指定,也可以動態(tài)調整(如果未指定大小,則JVM會根據系統(tǒng)內存自動設置初始大?。?。
堆的主要特點包括:
-
線程共享:堆是所有線程共享的內存區(qū)域。所有線程都可以訪問堆中的對象實例,這使得多個線程可以共同操作和共享對象。
-
動態(tài)分配和回收:堆的大小在程序運行時是可以動態(tài)調整的。當創(chuàng)建對象時,JVM會自動分配堆中的內存空間。當對象不再被引用時,垃圾收集器會自動回收堆中不再使用的對象的內存。
-
垃圾回收:堆中的內存由垃圾收集器負責管理。垃圾收集器會周期性地檢查堆中的對象,將不再被引用的對象標記為垃圾,然后回收這些垃圾對象的內存,釋放給堆供其他對象使用。
-
自動內存管理:Java 中的堆內存由 JVM 自動進行內存管理,程序員不需要手動釋放內存。JVM 會自動進行垃圾回收,釋放不再使用的內存,避免了內存泄漏等問題。
另外,在Java虛擬機的堆內存中,通常被劃分為兩個主要區(qū)域:新生代(Young Generation)
和老生代(Old Generation)
。
-
新生代(Young Generation):
新生代是存放新創(chuàng)建的對象的區(qū)域。在新生代中,通常會將堆內存劃分為一個Eden空間和兩個Survivor空間(通常稱為S0和S1)。新創(chuàng)建的對象首先會被分配到Eden空間。當Eden空間滿時,會觸發(fā)Minor GC(Young GC),垃圾回收器會將Eden空間和其中還存活的對象復制到一個未使用的Survivor空間中。然后,垃圾回收器會清除Eden空間和正在使用的Survivor空間,將其中的垃圾對象回收。幸存下來的對象會被晉升到老生代。 -
老生代(Old Generation):
老生代是存放長時間存活的對象的區(qū)域。老生代中存放的對象通常是在新生代經過一定次數(shù)的Minor GC后仍然存活的對象,或者是大對象等。當老生代空間滿時,會觸發(fā)Major GC(Full GC),垃圾回收器會對整個堆進行回收,包括新生代和老生代的所有對象。
通過將堆內存分為新生代和老生代,并采用不同的垃圾回收策略,可以提高垃圾回收的效率。新生代中的Minor GC頻繁進行,回收生命周期短的對象,盡可能快速地釋放內存;而老生代中的Major GC則相對較少進行,回收生命周期長的對象,保證了老生代的穩(wěn)定性和可靠性。文章來源:http://www.zghlxwxcb.cn/news/detail-622227.html
六、程序計數(shù)器(線程私有)
程序計數(shù)器也是 Java 線程私有的內存區(qū)域。它是一種指示器,用于指示當前線程執(zhí)行的字節(jié)碼指令的地址或索引
。在Java線程切換時,程序計數(shù)器的值會被保存和恢復,以保證線程切換后能正確繼續(xù)執(zhí)行。文章來源地址http://www.zghlxwxcb.cn/news/detail-622227.html
到了這里,關于【JVM】(一)深入理解JVM運行時數(shù)據區(qū)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!