圖解JVM記憶體模型及JAVA程式執行原理

陶然陶然發表於2023-05-15

   一、JAVA語言的特點

  在進入正題之前,先問一個老生常談的問題,相較於C,JAVA語言的優勢是什麼?相信學過JAVA的人都知道,無論是大學時的第一堂課還是JAVA相關書籍的第一章也都會講到:一次編寫、到處執行;真正意義上的實現了跨平臺。

  那再問一個問題,為什麼Java可以跨平臺?

  大多數人都知道Java可以跨平臺得益於 JVM(java虛擬機器)。

  在這之前,我瞭解到的java跨平臺得益於不同版本的JVM,那麼它的底層原理是什麼呢?

  “一次編譯,到處執行” 是Java的跨平臺特性。像 C 、C++ 這樣的程式語言沒有它。

  透過下面的介紹,相信你會有一個近一步的瞭解。

  Java是一種可以跨平臺的程式語言。首先,我們需要知道什麼是平臺。我們把CPU處理器與作業系統的整體叫平臺。

  CPU相當於計算機的大腦,指令集是CPU中用來計算和控制計算機系統的一套指令的集合。

  指令集分為精簡指令集(RISC)和複雜指令集(CISC)。每個CPU都有自己的特定指令集。

  要開發一個程式,我們必須首先知道程式執行在什麼CPU上,也就是說,我們必須知道CPU使用的指令集。

  作業系統是使用者與計算機之間的介面軟體。不同的作業系統支援不同的CPU。嚴格來說,不同的作業系統支援不同的CPU指令集。但問題是,原來的Mac作業系統只支援PowerPC,不能安裝在英特爾上。我們該怎麼辦?因此,蘋果必須重寫其Mac作業系統來支援這一變化。最後,我們應該知道不同的作業系統支援不同的CPU指令集。現在windows、Linux、MAC和Solaris都支援Intel和AMD CPU指令集。

  如果你想開發一個程式,首先應該確定:

  CPU型別,即指令集型別;

  作業系統;我們稱之為軟硬體平臺的結合。也可以說“平臺=CPU+OS”。而且由於主流作業系統支援主流CPU,有時作業系統也被稱為平臺。

   二、如何實現跨平臺

  通常,我們編寫的Java原始碼在編譯後會生成一個Class檔案,稱為位元組碼檔案。Java虛擬機器負責將位元組碼檔案翻譯成特定平臺下的機器程式碼,然後執行。簡言之,java的跨平臺就是因為不同版本的 JVM。換句話說,只要在不同的平臺上安裝相應的JVM,就可以執行位元組碼檔案(.class)並執行我們編寫的Java程式。在這個過程中,我們編寫的Java程式沒有做任何改動,只是透過JVM的“中間層”,就可以在不同的平臺上執行,真正實現了“一次編譯,到處執行”的目的。JVM是跨平臺的橋樑和中介軟體,是實現跨平臺的關鍵。首先將Java程式碼編譯成位元組碼檔案,然後透過JVM將其翻譯成機器語言,從而達到執行Java程式的目的。因此,執行Java程式必須有JVM的支援,因為編譯的結果不是機器程式碼,必須在執行前由JVM再次翻譯。即使您將Java程式打包成可執行檔案(例如。Exe),仍然需要JVM的支援。

  注意:編譯的結果不是生成機器程式碼,而是生成位元組碼。位元組碼不能直接執行,必須由JVM轉換成機器碼。編譯生成的位元組碼在不同的平臺上是相同的,但是JVM翻譯的機器碼是不同的。

   三、JVM簡介

  JVM------Java Virtual Machine.JVM是Java平臺的基礎,與實際機器一樣,他有自己的指令集(類似CPU透過指令操作程式執行),並在執行時操作不同的記憶體區域(JVM記憶體體系)。Java虛擬機器位於作業系統之上(如下圖所示),將透過JAVAC命令編譯後的位元組碼載入到其記憶體區域,透過直譯器將位元組碼翻譯成CPU能識別的機器碼行。每一條Java指令,Java虛擬機器規範中都有詳細定義,如怎麼取運算元,怎麼處理運算元,處理結果放在哪裡。  

  JVM是執行在作業系統之上的,它與硬體沒有直接互動。

   四、JVM的記憶體結構

  JAVA原始碼檔案透過編譯後變成虛擬機器可以識別的位元組碼,JAVA程式在執行時,會透過類載入器把位元組碼載入到虛擬機器的記憶體中(虛擬機器的記憶體是一個邏輯概念,相當於是對主記憶體的一個抽象,實際上真實的資料還是存放在主存中),詳見下圖。  

  Java 虛擬機器在執行 Java 程式的過程中會把它管理的記憶體劃分為若干個不同的資料區域。每個區域都有各自的作用。

  分析 JVM 記憶體結構,主要就是分析JVM 執行時資料儲存區域。JVM 的執行時資料區主要包括:堆、棧、方法區、程式計數器等。而 JVM 的最佳化問題主要線上程共享的資料區中:堆、方法區。  

  4.1、方法區

  又稱非堆(non-heap),方法區用於儲存已被虛擬機器載入的類資訊,常量、靜態變數,即時編譯後的程式碼等資料。方法區中最著名的就是CLASS物件,CLASS物件中存放了類的後設資料資訊,包括:類的名稱、類的載入器、類的方法、類的註解等。

  當我們new一個新物件或者引用靜態成員變數時,Java虛擬機器(JVM)中的類載入器子系統會將對應Class物件載入到JVM中,然後JVM再根據這個型別資訊相關的Class物件建立我們需要例項物件或者提供靜態變數的引用值。注意,我們定義的一個類,無論建立多少個例項物件,在JVM中都只有一個Class物件與其對應,即:在記憶體中每個類有且只有一個相對應的Class物件,如圖:  

  實際上所有的類都是在對其第一次使用時動態載入到JVM中的,當程式建立第一個對類的靜態成員引用時,就會載入這個被使用的類(實際上載入的就是這個類的位元組碼檔案)。注:使用new建立類的新例項物件也會被當作對類的靜態成員的引用(建構函式也是類的靜態方法)

  由此看來Java程式在它們開始執行之前並非被完全載入到記憶體的,其各個部分是按需載入,所以在使用該類時,類載入器首先會檢查這個類的Class物件是否已被載入(類的例項物件建立時依據Class物件中型別資訊完成的),如果還沒有載入,預設的類載入器就會先根據類名查詢.class檔案(編譯後Class物件被儲存在同名的.class檔案中),在這個類的位元組碼檔案被載入時,它們必須接受相關驗證,以確保其沒有被破壞並且不包含不良Java程式碼(這是java的安全機制檢測),完全沒有問題後就會被動態載入到記憶體中,此時相當於Class物件也就被載入記憶體了(畢竟.class位元組碼檔案儲存的就是Class物件),同時也就可以根據這個類的Class物件來建立這個類的所有例項物件。

  4.2、堆

  所有建立出來的例項物件還有陣列都是存放在堆記憶體中,堆是Java虛擬機器所管理的記憶體中最大的一塊儲存區域,堆記憶體被所有執行緒共享。

  垃圾收集器就是根據GC演算法,收集堆上物件所佔用的記憶體空間,堆上又分為了新生代和老年代,針對不同的分代又會有物件的垃圾回收器和相應的回收演算法(GC章節中會詳細介紹)。

  4.3、棧

  JVM 中的棧包括 Java 虛擬機器棧和本地方法棧,兩者的區別就是,Java 虛擬機器棧為 JVM 執行 Java 方法服務,本地方法棧則為 JVM 使用到的 Native 方法服務。兩者作用是極其相似的,本文主要介紹 Java 虛擬機器棧,以下簡稱棧。

  棧屬於執行緒私有的資料區域,與執行緒同時建立,總數與執行緒關聯,代表Java方法執行的記憶體模型。每個方法執行時都會建立一個棧幀來儲存方法的的區域性變數表、運算元棧、動態連結方法、方法返回值、返回地址等資訊。每個方法從呼叫值結束就對於一個棧楨在虛擬機器棧中的入棧和出棧過程,棧幀中的區域性變數表可以存放基本型別,也可以存放指向物件的引用,當在某個方法中new Object()時,會在當前方法棧幀中的區域性變數表存放一個指向堆記憶體例項物件的引用,詳見下圖。  

  4.4、程式計數器

  是一塊較小的記憶體空間,用來儲存虛擬機器下一條執行的位元組碼指令地址,和CPU中的程式計數器是一樣的概念。

  五 、JAVA程式在JVM內是如何執行的

  上文已介紹了JVM的記憶體結構,接下來再看一下這個程式在JVM內部是怎麼執行的:

  1.JAVA程式的執行過程簡單來說包括:

  2.JAVA原始碼編譯成位元組碼;

  3.位元組碼校驗並把JAVA程式透過類載入器載入到JVM記憶體中;

  4.在載入到記憶體後針對每個類建立Class物件並放到方法區;

  5.位元組碼指令和資料初始化到記憶體中;

  6.找到main方法,並建立棧幀;

  7.初始化程式計數器內部的值為main方法的記憶體地址;

  8.程式計數器不斷遞增,逐條執行JAVA位元組碼指令,把指令執行過程的資料存放到運算元棧中(入棧),執行完成後從運算元棧取出後放到區域性變數表中,遇到建立物件,則在堆記憶體中分配一段連續的空間儲存物件,棧記憶體中的區域性變數表存放指向堆記憶體的引用;遇到方法呼叫則再建立一個棧幀,壓到當前棧幀的上面。

  下面以一段實際的程式碼舉例,來看一下,程式在JVM內部的執行過程。  

  我們先透過JAVAP命令,展示上述程式碼對應的位元組碼,下圖是JVM把類載入到記憶體以後在方法區的常量池中初始化好的Class物件和各種方法引用,這裡面需要重點關注一下前面的#1,#2,#5這些符號,這些數字儲存的是和Class物件以及方法的引用關係,後面的位元組碼中會用到。  

  隨後執行引擎中的直譯器會率先啟動,對ClassFile位元組碼採用逐行解釋的方式載入機器碼,並配合執行時資料區的程式計數器與運算元棧來支援。

  下圖是main方法的位元組碼指令,我們結合JVM記憶體情況對程式碼做逐一分析。  

  上圖中stack=3,local=2:stack=3代表棧的深度為3,local=2代表區域性變數表中的變數數量。

  程式樣例執行詳解

  下圖是main方法中的位元組碼執行到detail.Sum方法前的JVM記憶體結構。  

  具體執行流程如下:

  首先會在JAVA棧中壓入main方法的棧幀,然後程式計數器中的值更新成位元組碼new所在的記憶體地址,樣例中為了方便起見就直接以0表示,程式計數器逐條解析位元組碼,其中new(new後面的#5中有講到,對應的是JvmDetailClass的Class物件),dup,invokespecial三個位元組碼指令分別代表建立物件、賦值引用、呼叫構造方法,astore_1代表是把運算元(引用)放入運算元棧,aload_1代表是把運算元(引用)出棧,並放到區域性變數表中。

  Iconst_3,iconst_5分別代表把運算元3,5入棧放到運算元棧中。

  接下來我們再看一下方法呼叫時JVM的記憶體結構是怎麼做的,上面的程式碼中涉及到2塊程式碼呼叫,一個是detail.Sum,一個是detail.getSum,這裡我們detail.getSum是個帶有返回值的方法,比較典型,我們直接以detail.getSum的呼叫為樣例,看一下JVM內部是怎麼執行的。

  當直譯器執行到方法呼叫時,會修改程式計數器中的值為呼叫的方法內部第一行指令,同時在棧中壓入getSum方法的棧幀,壓入棧幀後會在區域性變數表中初始化一個當前方法所屬的物件的引用this,如果呼叫方法涉及到傳參的情況下,則會在區域性變數表中存入傳遞的引數。

  當getSum方法執行完成後,會做兩步動作:

  程式計數器又會修改為main方法呼叫getSum處的下一行指令的地址。

  方法返回值寫入main方法棧幀中的運算元棧中。  

來自 “ 阿里開發者 ”, 原文作者:金峰(項良);原文連結:http://server.it168.com/a2023/0515/6803/000006803643.shtml,如有侵權,請聯絡管理員刪除。

相關文章