程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java虛擬機的研究與實現

Java虛擬機的研究與實現

編輯:關於JAVA

摘 要 本文在研究kaffe的基礎上,吸收kaffe虛擬機的主要思想,用C語言作為開發語言,采用了及時編譯器作為執行引擎,實現了一種Windows平台下的Java虛擬機。然後對實現過程中的一些關鍵技術如class文件驗證、及時編譯器、垃圾收集器、線程同步和線程調度等做了分析。

關鍵詞 kaffe; C語言; 及時編譯器;Java虛擬機

引言

Java虛擬機本質是就是一個程序,當它在命令行上啟動的時候,就開始執行保存在某字節碼文件中的指令。Java語言的可移植性正是建立在Java虛擬機的基礎上。任何平台只要裝有針對於該平台的Java虛擬機,字節碼文件(.class)就可以在該平台上運行。這就是“一次編譯,多次運行”。

kaffe虛擬機的簡要分析

kaffe虛擬機采用了模塊化的程序設計思想,它由多個獨立的子系統組成。從功能模塊上來分它主要分為:虛擬機總體驅動模塊,類裝載器模塊,類執行模塊, 數據區管理模塊,內存管理模塊,本地支持模塊等等。kaffe虛擬機簡要的程序流程圖如圖1所示。

圖1 kaffe虛擬機簡要的程序流程圖

Java虛擬機的實現

Java 源程序的執行過程為: Java源程序(.java)經過Java編譯器編譯生成字節碼文件(.class),然後由類裝載器將字節碼文件裝載到方法區中,然後進行連接驗證,由Java虛擬機讀取字節碼,轉換為特定平台的指令,並且在對應的CPU中執行。

本實現中采用的流程框架如下圖所示:

圖2 本實現的主要框架

1、類裝載、連接及初始化

類文件包括:魔數(magic),次、主版本號,常量池,類或接口訪問修飾符,常量池索引(this_class和super_class),接口表,域表,方法表,類或接口的屬性信息。其中最復雜的內容是常量池,它類似於傳統語言編譯過程中用到的符號表。

從原始的class文件到可以被Java虛擬機執行的內部數據格式,需要經過裝載、連接和初始化這3個階段。

裝載是將class文件通過類裝載器裝載到在邏輯上被稱為方法區的內存單元中的過程。

連接又分為三個步驟:驗證,准備和解析。驗證是對字節碼的驗證,可根據具體情況來確定被裝載的類是否符合Java虛擬機規范中規定的class文件格式,並確保它不會破壞Java虛擬機的完整性。包括(1)類裝載過程中的驗證; (2) 檢查class文件內部的連貫性,一旦發現class文件格式存在一處錯誤,則拋出VerifyError異常或ClassFormatError異常。確保每個final類不含有子類,final方法不能被覆蓋,以及常量池中所有的域引用和方法引用有有效的名字和類型描述符號;(3) 對字節碼流使用一個數據流分析器進行驗證。准備步驟的任務是創建域表,並設置域初值。解析步驟是將類中的常量池中的類、接口、字段和方法的符號引用替換成直接引用,以達到更快地訪問數據的目的。

在初始化階段,Java虛擬機設計者需要將類變量賦予正確的初始值。

class文件經過上述三個階段的處理,虛擬機就獲得了該類的所有信息並且表示成能夠容易操作的內部數據格式,從而為方法的運行作好了充分的准備。

2、及時編譯器

任何Java虛擬機實現的核心都是它的執行引擎。在由軟件實現的虛擬機中,執行引擎主要有一次性解釋字節碼、及時編譯器、自適應優化編譯器三種方式。本實現采用了及時編譯的方式,它的特點是第一次被執行的機器碼會被編譯成本地機器碼。及時編譯器將引入的字節碼翻譯成本地機器碼,然後直接執行機器碼指令而不是解釋字節碼。機器碼指令保存在內存中,由於在運行過程中編譯的結果不被保存, 所以程序下一次運行時,字節碼將再一次被翻譯成機器碼。

如果一裝載完字節碼文件中的Java方法後,就對其進行編譯,則有點處理不恰當,因為還不清楚是否需要執行該方法。編譯一個不需要執行的方法,將帶來不必要的空間和時間上的損失。 因此虛擬機設計者需要采用一種優化方案,即只有需要被執行的方法才能被JIT編譯,這個問題可以參照kaffe虛擬機中的trampoline來解決。

JIT實現步驟:(1)對字節碼進行驗證並且劃分基本塊;(2)產生四元式;(3)根據四元式生成本地機器碼;(4)操作數地址回填。

圖3及時編譯器的流程圖

在字節碼指令模擬操作的時候,按其語義動作生成指令屬性四元式序列,指令屬性四元式的結構為: (目的操作數, 源操作數1,源操作數2,語義動作),四元式數據結構如下:

typedef struct Sequence{
  void (*func)(struct Sequence*); //語義動作
  union{
  jvalue value;
  struct _label_ *labconst; //標號類型操作數
  Method *methconst; //方法地址操作數
  struct slotData **smask;
  struct slotData *slot; //槽操作數
  }u[3];
  uint8 type; //Sequence類型
  uint8 refered; //該四元式的引用
  struct Sequence *next; //下一個四元式
}Sequence;

其中目的操作數為Sequence.u[0],源操作數1為Sequence.u[1],源操作數2為Sequence.u[2]。 Sequence.func則代表語義動作,它主要用於生成該Sequence語義的本地機器碼。

指令屬性四元組建立後就進入代碼生成階段,屬性四元組在形式上已經非常接近本地機器指令,只需要遍歷該屬性序列,執行相應的語義動作函數,即可生成機器指令。語義動作函數的功能包括操作數尋址、寄存器分配、建立指令連接以及本地機器碼生成等。

在及時編譯過程中要經常使用到操作數棧,虛擬機把操作數棧作為它的工作區。大多數指令都要從這裡彈出數據,執行運算,然後把結果壓回操作數棧。而操作數棧區,局部變量區和幀數據區被包含在方法幀中。方法幀的數據結構如下:

typedef struct Frame{
  struct Frame *prev; // 上一幀
  struct Frame *next; // 下一幀
  value_t *sp; // 棧槽指針
  uint8 *pc; // 程序計數器
  method_t *method; //指向正在被執行的方法
  class_t *class_ptr; // 指向包含該方法的類
  value_t locals[1]; //方法的局部變量的起始
}Frame;

本實現中的及時編譯器的優點表現在:(1)大大提高了Java應用程序運行的速度;(2)編譯過程只在運行時進行,不會改動Java字節碼,不會影響Java程序的可移植性;(3)對字節碼的編譯,使得許多優化手段的采用成為可能。缺點表現在:(1)如果對所有方法進行編譯,則會占用大量的內存空間;(2)及時編譯的結果在虛擬機終止運行時不被保存,這意味著下一次運行同樣的程序仍需要重復編譯。

3、垃圾收集

垃圾收集器主要的任務是檢測出垃圾對象,然後回收垃圾對象使用的堆空間並還給程序。kaffe采用了增量垃圾收集的算法,而本實現中采用了三色標記並清除算法。

在標記之前先將堆中所有的分配單元置成白色,然後按深度優先算法遍歷每一個單元。當垃圾收集器遍歷一個分支的時候,如果一個分配單元及與之相關聯的單元都被遍歷到,則將其標記成黑色。 如果一個單元被遍歷到,但是與之相關聯的單元尚未被遍歷,則將該單元標記成灰色。這時,垃圾收集器將繼續遍歷與該灰色單元相關聯的單元,直到這些相關聯的單元全部被遍歷到,才能將這個灰色單元標記成黑色。最後當所有被遍歷到的單元都被標記成黑色的時候, 將堆中被標記成白色的分配單元回收。

圖4三色標記並清除算法的中間過程圖

最後是對堆碎塊進行壓縮處理。是通過快速地移動對象來減少堆碎塊。即把當前活動的對象移動到堆的一端,在此過程中,堆的另外一端出現一塊大的連續的內存單元。所有被移動的對象的引用也被更新,指向新的內存單元。

4、線程同步

Java虛擬機中存在著以下兩種線程:虛擬機系統線程和用戶Java線程。虛擬機系統線程是指虛擬機運行過程中執行其特殊功能的線程,比如垃圾收集器線程等。用戶Java線程是指用戶編寫的Java應用程序中明確表示要啟動的線程,並且至少有一個Java線程,即main方法。

而Java語言的一大優勢是支持多線程,這種支持主要表現在同步上。在java應用程序中使用synchronized關鍵字簡單地使方法同步,而在Java虛擬機指令中則使用monitorenter和monitorexit指令顯式地支持方法同步。Java虛擬機為每個對象都關聯一個鎖。當當前線程訪問共享資源的時候,會執行monitorenter指令來彈出該對象引用,從而獲取該對象引用相關聯的鎖。如果該對象已經被另一線程占用則當前線程就需要進入鎖的等待隊列,等待釋放對象上的鎖;已經獲取共享資源的線程在釋放資源的時候,執行monitorexit指令來彈出對象引用,並且釋放與該資源相關聯的鎖,並讓等待隊列中的第一個線程獲取該對象鎖。

當然線程thread也可以根據需要對某對象obj多次上鎖,上鎖的次數放在計數器counter中。只有當counter為0的時候,即thread加在該對象上的鎖被完全釋放,其它線程才有機會使用 object。對象的數據結構為:

typedef struct Obj{
 uint32 size; //堆中對象的大小
 uint16 counter; //對象被上鎖的數量
 uint16 flag; //對象的狀態標志
 uint16 thread_id; //對該對象進行加鎖的
 //線程的ID
} Obj;

而在實際Java編程中, 程序員並不需要動手加鎖,對象鎖只是在Java虛擬機內部使用的。程序員只需要編寫同步語句就可以標志一監視區域,當Java虛擬機運行程序的時候,每次進入一個監視區域,它每次都會給對象上鎖。

5、線程調度

在本實現中,還需要考慮到在上述等待線程隊列中如何選擇下一個線程來執行,即線程調度問題。

哪個線程將獲取notify命令,這一點在很大程度上取決於虛擬機的設計者,既可以通過使用FIFO隊列來調度,也可以根據所有等待線程的優先級來調度,比如喚醒等待隊列中優先級最高的線程獲取剛剛釋放的資源。而Bill Venners則從平台無關和執行效率這兩個角度出發,提倡Java虛擬機的設計者應使用java.lang.Object類中的notifyAll()方法來代替notify()方法去喚醒等待隊列中的線程。

處理好線程調度問題,就可以節省程序的執行時間,這對於提高Java虛擬機的執行性能是很有幫助的。

總結

本文在研究kaffe的基礎上,實現了一虛擬機。並且對Java虛擬機中的關鍵技術及時編譯器、垃圾收集器、線程同步和線程調度等做了分析。本文中所實現的及時編譯器雖然在執行速度上比解釋型的Java虛擬機快得多,但是不如自適應優化編譯器,因為自適應優化編譯器具有程序啟動快,占用內存少的特點。如果要明顯地提高虛擬機的性能,應該更多的從執行引擎著手。另外在Java應用程序的執行過程中許多時間是花費在多線程處理和垃圾收集上,如果在線程同步和線程調度上有所創新,也可以提高虛擬機的執行性能。

研究Java虛擬機的實現過程有重要的意義,程序員可以編寫針對於不同平台下的裁減了的Java虛擬機,這樣它就可以在實時嵌入式系統得到廣泛地應用。相信Java虛擬機將在更多的領域得到不斷的完善和發展。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved