天天看點

JVM體系結構和工作方式

JVM的全名是Java Virtual Machine(Java虛拟機)。它是通過模拟一個計算機的形式來實作到計算機所具有的計算功能。讓我們先來看看一台真實的計算機具備計算功能的條件:

指令集

這個計算機所能識别的機器語言的指令集合。

計算單元

即能夠識别并且控制指令執行的功能子產品

尋址方式

位址的位數、最小位址和最大位址範圍,以及位址的運作規則

寄存器定義

包括操作數寄存器、變址寄存器、控制寄存器等的定義、數量和使用方法

存儲單元

能夠存儲操作數和儲存操作結構的單元,如核心級緩存、記憶體和磁盤等

下面再進行具體分析:

        指令集:所謂指令集就是在CPU中用來計算和控制計算機系統的一套指令的集合,每種CPU在設計的時候就已經規定了一套與硬體電路想配合的指令系統。指令集的先進與否是展現CPU性能 的一個重要名額。

        指令集和彙編語言有什麼關系?指令集是可以直接被機器識别的機器碼,也就是說它必須以二進制格式存在于計算機中。而彙編語言則是能夠被人識别的指令,彙編語言在順序和邏輯上是與機器指令一一對應的。換句話說,就是彙編語言是為了讓人能夠更容易記住機器指令而使用的助記符。

       從主流的體系結構上分為精簡指令集(Reduced Istruction Set Computing,RISC)和複雜指令集(Complex Instruction Set Computing, CISC)。目前我們普遍使用的桌面作業系統中基本上使用的都是CISC,如x86架構的CPU都使用複雜指令集。除了這兩種指令集之外Intel和AMD公司還在它們的基礎上開發出了很多擴充指令集,如MMX(MultiMediaeXtension,多媒體擴充指令)使得在處理多媒體資料時性能更強,還有AMD公司為提高3D處理性能開發的3DNow!指令集等。

結構基本上由四部分組成:

類加載器

在JVM啟動時或者是在類運作時将需要的class檔案加載到JVM中

執行引擎

負責執行class檔案中包含的位元組碼指令,相當于實際機器上的CPU

記憶體區

将記憶體劃分為若幹個區以模拟實際機器上的存儲、記錄和排程功能子產品

本地方法調用

調用C或C++實作的本地方法的代碼傳回結果

類加載器:

每個被JVM裝載的類型都有一個對應的java.lang.Class類的執行個體來表示該類型,該類型可以唯一的表示被JVM裝載的class類,要求這個執行個體和其他類的執行個體一樣都存放在Java的堆中。 

執行引擎:

執行引擎的作用就是解析JVM位元組碼指令,得到執行結果。在《Java虛拟機規範》中詳細地定義了執行引擎遇到每條位元組碼指令時應該處理什麼,并且應該得到什麼結果。但是沒有規定執行引擎應該如何過采取什麼方式處理而得到這個結果。不同的廠商有自己不同的實作方式。執行引擎也就是執行一條條代碼的一個流程,而代碼都是包含在方法體内的,是以執行引擎本質上就是執行一個個方法所串起來的流程。對應到作業系統中一個執行流程就是一個Java線程。因為一個Java程序可以有多個同時執行的執行流程。是以每個Java線程就是一個執行引擎的執行個體。

Java記憶體管理:

執行引擎在執行一段程式時需要存儲一些東西(如操作數、執行結果等)。class類的位元組碼還有類的對象等資訊都需要在執行引擎執行之前就準備好。JVM執行個體會有一個方法區、Java堆、Java棧、PC寄存器和本地方法區。其中方法區和Java堆是所有線程共享的,也就是說可以被所有的執行引擎執行個體通路。每個新的執行引擎執行個體被建立時會為這個執行引擎建立一個Java棧和一個PC寄存器,如果目前正在執行一個Java方法,那麼目前的這個Java棧中儲存的是該線程中方法調用的狀态,包括方法的參數、方法的局部變量、方法的傳回值以及運算的中間結果等。而PC寄存器會指向即将執行的下一條指令。

JVM執行位元組碼指令是基于棧的架構,也就是所有的作業系統必須先入棧,然後根據指令中的操作碼選擇從棧頂彈出若幹個元素進行計算後再将結果壓入棧。在JVM中操作數可以存放在每一個棧幀中的一個本地變量集中,即在每個方法調用時就會給這個方法配置設定一個本地變量集,這個本地變量集在編譯時就已經确定,所有操作數入棧可以直接是常量入棧或者從本地變量集中取一個變量壓入棧中。這和一般的基于寄存器的操作有所不同,一個操作需要頻繁地入棧和出棧,如進行一個加法運算,如果兩個操作數都在本地變量中,那麼一個加法操作就要有5次棧操作,分别是将兩個操作數從本地變量入棧(2次入棧操作),再将兩個操作數出棧用于加法運算(2次出棧),再将加法結果壓入棧頂(1次入棧)。如果是基于寄存器的話,一般隻需要将兩個操作數存入寄存器進行加法運算後

再将結果存入其中一個寄存器即可,不需要這麼多的資料移動的操作。

那麼為什麼JVM還要基于棧來設計呢?

第一個理由是JVM要設計成與平台無關的,而平台無關性就是要保證在沒有或者有很少的寄存器的機器上也要同樣能正确地執行Java代碼。例如,在80x86的機器上寄存器就是沒有規律的,很難針對某一款機器設計通用的基于寄存器的指令,是以基于寄存器的架構很難做到通用。在手機作業系統方面,Google的Android平台上的DalvikVM就是基于特定晶片(ARM)設計的基于寄存器的架構,這樣在特定晶片上實作基于寄存器的架構可能更多考慮性能,但是也犧牲了跨平台的移植性,當然在目前的手機上這個需求還不是最迫切的。

第二個理由是為了指令的緊湊性,因為Java的位元組碼可能在網絡上傳輸,是以class檔案的大小也是設計JVM位元組碼指令的一個重要因素,如在class檔案中位元組碼除了處理兩個表跳轉的指令外,其他都是位元組對齊的,操作碼可以隻占一個位元組大小,這都是為了盡量讓編譯後的class檔案更加緊湊。為了提高位元組碼在網絡上的傳輸效率,Sim設計了一個Jar包的壓縮工具Pack200,它可以将多個class檔案中的重複的常量池的資訊進行合并,如一般在每個class檔案中都含有“Ljava/lang/String;”,那麼多個class檔案中的常量就可以共用,進而起到減少資料量的作用。

每當建立一個新的線程時,JVM會為這個線程建立一個Java棧,同時會為這個線程配置設定一個PC寄存器,并且這個PC寄存器會指向這個線程的第一行可執行代碼。每當調用一個新方法時會在這個棧上建立一個新的棧幀資料結構,這個棧幀會保留這個方法的一些元資訊,如在這個方法中定義的局部變量、一些用來支援常量池的解析、正常方法傳回及異常處理機制等。JVM在調用某些指令時可能需要使用到常量池中的一些常量,或者是擷取常量代表的資料或者這個資料指向的執行個體化的對象,而這些資訊都存儲在所有線程共享的方法區和Java堆中。

JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式

       在開始執行方法之前,PC寄存器存儲的指針是第1條指令的位址,局部變量區和操作棧都沒有資料。從第1條到第4條指令分别将a、b兩個本地變量指派,對應到局部變量區就是1和2分别存儲常數1和2,前4條指令執行完後,PC寄存器目前指向的是下一條指令位址,也就是第5條指令,這時局部變量區已經儲存了兩個局部變量(也就是變量a和b的值),而操作棧裡仍然沒有值,因為兩次常數入棧後又分别出棧了(store會讓變量出棧)。第5條和第6條指令分别是将兩個局部變量入棧,然後相加,1先入棧2後入棧,棧頂元素是2,第7條指令是将棧頂的兩個元素彈出後相加,将結果再入棧。可以看出,變量a和b相加的結果3存在目前棧的棧頂中,接下來是第8條指令将10入棧,目前PC寄存器執行的位址是9,下一個操作是将目前棧的兩個操作數彈出進行相乘并把結果壓入棧中,第10條指令是将目前的棧頂元素存入局部變量3中,第10條指令執行完後棧中元素出棧,出棧的元素存儲在局部變量區3中,對應的是變量c的值。最後一條指令是return,這條指令執行完後目前的這個方法對應的這些部件會被JVM回收,局部變量區的所有值将全部釋放,PC寄存器會被銷毀,在Java棧中與這個方法對應的棧幀将消失。

JVM的方法調用分為兩種:一種是Java方法調用,另一種是本地方法調用。本地方法調用由于各個虛拟機的實作不太相同,是以這裡主要介紹Java的方法調用情況。

JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式
JVM體系結構和工作方式

當JVM執行main方法時,首先将兩個常數1和2分别存鍺到局部變量區1和2中,然後調用靜态math方法。從math的位元組碼指令可以看出,math的兩個參數也存儲在其對應的方法棧幀中的局部變量區0和1中,先将這兩個局部變量分别入棧,然後進行相加操作再和常數10相乘,最後将結果傳回。

當執行invokestatic指令時JVM會為math方法建立一個新的棧幀,并且将兩個參數存在math方法對應的棧幀的前兩個局部變量區中,這時PC寄存器會清零,并且會指向math方法對應棧幀地第一條指令位址,執行invokestatic指令時,建立了一個新的棧幀,這時棧幀中的局部變量區中已經有兩個變量了,這兩個變量是從mam方法的棧幀中的操作棧中傳過來的。當執行math方法時,math方法對應的棧幀成為目前的活動棧幀,PC寄存器儲存的是目前這個棧幀中的下一條指令位址,是以是0。math方法先将a、b兩個變量相加,再乘以10,最後傳回這個結果執行到第5條指令的狀态,math的操作棧中的棧頂元素相乘的結果是30,最後一條指令是ireturn,這條指令是将目前棧幀中的棧頂元素傳回到調用這個方法的棧中,而這個棧幀也将撤銷,PC寄存器的值恢複調用桟的下一條指令位址,   main方法将math方法傳回的結果再除以10存放在局部變量區3中,當執行return指令時main方法對應的棧幀也将撤銷,如果目前線程對應的Java棧中沒有棧幀,這個Java棧也将被JVM撤銷,整個JVM退出。