譯者語
為加深對jvm的了解和日後查閱時更友善,于是對原文進行翻譯。内容是建立在我對jvm的認識的基礎上翻譯的,加上本人的英語水準有限,若有纰漏請大家指正,謝謝。
一、前言
本文将介紹jvm内部架構。下圖展示符合java7規範的jvm内部主要元件。

下面我們将上述元件分為線程相關和線程獨立兩種類型來介紹。
二、目錄
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a1">thread</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a3">per thread</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a31">program counter (pc)</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a32">stack</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a33">native stack</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a34" target="_blank">frame</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a341">local variables array</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a342">operand stack</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a343">dynamic linking</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a2">shared between threads</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a4">heap</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a5">memory management</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a6">non-heap memory</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a7">just in time (jit) compilation</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a8">method area</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a9">class file structure</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a10">classloader</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a11">faster class loading</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a12" target="_blank">where is the method area</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a13">classloader reference</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a15">run time constant pool</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a15"> exception table</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a16">symbol table</a>
<a href="http://www.cnblogs.com/fsjohnhuang/p/4260417.html#a17">interned strings (string table)</a>
jvm允許程序包含多個并發的線程。hotspot
jvm中的java線程與os線程是一一對應的。當線程工作存儲區(thread-local storage)、配置緩存(allocation
buffers)、同步對象(synchronized objects)、棧和本地棧(stacks)和程式計數器(pragram
counter)等java線程相關的狀态均準備好後,就會啟動os線程并有os線程執行run函數。os負責線程的排程。當以正常方式或異常抛出的方式
退出run函數,os線程均會判斷目前java線程的終止是否會導緻程序的終止(程序的工作線程是否都終止了?),若要終止程序的化,則釋放java線程
和os線程所占的資源,否則就釋放java線程的資源,并回收os線程。
jvm system threads
若你用過jconsole或其他調試工具,你會發現除了主線程外還存在數個有jvm建立的系統線程。hotspot jvm的系統線程有這5個:
1. vm thread(虛拟機線程)
vm thread 用于為一些需要防止堆變化操作提供執行環境,當要執行防止堆變化的操作時,就是要求jvm啟動安全點(safe-point),此時将會暫停gc、線程棧操作、線程恢 複和偏向鎖解除。
2. periodic task thread(周期性任務線程)
periodic task thread負責定時事件(如interrupts),用于周期性執行計劃任務
3. gc threads(垃圾回收線程)
gc threads 負責不同類型垃圾回收活動。
4. compiler threads(編譯器線程)
compiler threads用于在運作時将位元組碼編譯為cpu本地代碼。
5. signal dispatcher thread(信号量分發線程)
singal dispatcher thread用于接收發送給jvm的信号量,并将其分發到合适的jvm方法來處理。
每個線程的執行環境均有以下的元件。
用于存放目前指令(或操作碼)的位址,若該指令為本地指令那麼pc為undefined。當執行完目前指令後pc會自增(根據目前指令的定義自
增1或n)進而指向下一個指令的地 址,那麼jvm就可以知道接下來要執行哪個指令了。事實上pc存放的是方法區(method
area)中的記憶體位址。
每個線程有自定獨立的堆棧用于存放在該線程執行的方法。堆棧是一個後進先出(lifo)的資料結構,元素稱為棧幀(frame)。當将要線上程
上執行某方法時,則需要将代表 該方法的棧幀壓棧,當方法執行完畢後(正常退出或抛出未處理的異常)則将棧幀彈棧。棧幀可能配置設定在堆上(heap),而
堆棧并不需要連續的存儲空間。
不是每種jvm都支援本地方法,對于支援本地方法的jvm它門會提供線程本地堆棧。若jvm實作了通過c連結模型(c-linkage
model)來實作jni,那麼本地堆棧實質就是c堆 棧(入參順序和傳回值均與c程式一緻)。本地方法一般都可以調用java方法,此時會在java
的堆棧中壓入一個棧幀并按執行java方法的流程處理。
stack restrictions(堆棧限制):堆棧的容量有動态和固定兩種。當棧幀數量大于堆棧容量時就會抛出stackoverflowerror;當堆中沒有足夠記憶體來配置設定新棧幀時則抛出outofmemoryerror。
局部變量表用于存放方法執行過程中this引用、方法入參和局部變量。對于靜态方法而言方法參數從局部變量表的第一位開始(下标為0),對于執行個體方法而
言方法參數從局部變量表的第二位開始(下标為1,第一位是this引用)。局部變量表内可包含以下類型資料,boolean/byte/char
/long/short/int/float/double/reference/returnaddress。
局部變量表的每個元素占32bit,每32bit稱為1個slot。上述所支援的類型中除了long和double外均占1個slot,而它倆就占2個slot。
在執行方法内部的位元組碼指令時需要使用操作數棧,大多數jvm的位元組碼指令是用于操作操作數棧(壓棧、彈棧、指派棧幀、棧幀互換位置或執行方法操作棧幀),實作資料在操作數棧和局部變量表之間頻繁移動。示例如下:
3. dynamic linking(動态連結)
每個棧幀均包含一個指向運作時常量池(runtime constant pool)的引用。通過這個運作時常量池來實作動态連結。
c/c++的代碼會被編譯成一個一個獨立的對象檔案,并通過靜态連結将對多個對象檔案生成一個執行檔案或dll類庫。在連結階段所有的符号引用會被直接引用取代,而直接引用則為相對于可執行檔案的程序入口位址的相對位址。而java的連結階段是在運作時動态發生的。
當将java類編譯成位元組碼時,所有對變量和方法的引用将被儲存為常量池表中的一條條符号引用表項,這些符号引用為邏輯引用而不是指向實體記憶體位址的引
用。jvm可以選擇不同的時刻将符号引用轉換為直接引用。一種是當class檔案加載并驗證通過後,這種稱為靜态處理(eager or static
resolution);另一種是在使用時才轉換為直接引用,這種稱為懶處理(lazy or late
resolution)。對于字段通過綁定來處理,對于對象或類則通過将符号引用轉換直接引用來識别,動态連結後原有的符号引用将被直接引用替換,是以對
于同一個符号引用,動态連結的操作僅發生一次。假如直接引用的類還未加載,則會加載該類。而直接引用所包含的位址相對于變量和方法在運作時的位址。
堆用于在運作時配置設定對象和數組。由于棧幀的容量是固定的,是以無法将對象和數組等容量可變的資料存放到堆棧中,而是将對象和數組在堆中的位址存放在棧幀中
進而操作對象和數組。由于對象和數組是存放在堆,是以需要通過垃圾回收器來回收它們所占的記憶體空間。垃圾回收機制将堆分成3部分:<br/>
1. 新生代(再細分為初生空間和幸存空間)
2. 老年代
3. 永久代(譯者語:永久代不在堆上)
對象和數組不能被顯式地釋放,必須通過垃圾回收器來自動回收。一般的工作步驟如下:
1. 新建立的對象和數組被存放在新生代;
2. 次垃圾回收将會對新生代作操作,存活下來的将從初生空間移至幸存空間;
3. 主垃圾回收(一般會導緻應用的其他所有線程挂起),會将新生代的對象愛嗯挪動到老年代;
4. 每次回收老年代對象時均會回收永久代的對象。當他們滿的時候就會觸發回收操作。
非堆記憶體包含下列這些:
1. 永久代
1.1. 方法區
1.2. 字元串區
2. 代碼緩存
用于存放被jit編譯器編譯為本地代碼的方法。
java的位元組碼是解析執行的,速度比cpu本地代碼差遠了。為了提高java程式的執行效率,oracle的hotspot虛拟機将需要經常執行的位元組
碼編譯成本地代碼并存放在代碼緩存當中。hotspot虛拟機會自動權衡解析執行位元組碼和将位元組碼編譯成本地代碼再執行之間的效率,然後選擇最優方案。
方法區存放每個類的資訊,具體如下:
1. 類加載器引用
2. 運作時常量池
2.1. 數字常量
2.2. 字段引用
2.3. 方法引用
2.4. 屬性
3. 字段資料,每個字段包含以下資訊
3.1. 名稱
3.2. 類型
3.3. 修飾符
3.4. 屬性
4. 方法資料,每個方法包含以下資訊
4.1. 名稱
4.2. 傳回值類型
4.3. 入參的資料類型(保持入參的次序)
4.4. 修飾符
4.5. 屬性
5. 方法代碼,每個方法包含以下資訊
5.1. 位元組碼
5.2. 操作數棧容量
5.3. 局部變量表容量
5.4. 局部變量表
5.5. 異常表,每個異常表項包含以下資訊
5.5.1. 起始位址
5.5.2. 結束位址
5.5.3. 異常處理代碼的位址
5.5.4. 異常類在常量池的位址
所有線程均通路同一個方法區,是以方法區的資料通路和動态連結操作必須是線程安全才行。假如兩個線程試圖通路某個未加載的類的字段或方法時,則會先挂起這兩個線程,等該類加載完後繼續執行。
magic、minor_version、major_version:用于聲明jdk版本
constant_pool:類似符号表,但包含更多的資訊
access_flags:存放該類的描述符清單
this_class:指向constant_pool中constant_class_info類型常量的索引,該常量存放的是符号引用到目前類(如org/jamesdbloom/foo/bar)
super_class:指向constant_pool中constant_class_info類型常量的索引,該常量存放的是符号引用到超類(如java/lang/object)
interfaces:一組指向constant_pool中constant_class_info類型常量的索引,該類常量存放的是符号引用到接口
fields:字段表,一個表項代表一個字段,表項的子項資訊均有constant_pool提供。
methods:方法表,一個表項代表一個方法,表項的子項資訊均有constant_pool提供。
attributes:屬性表,表項用于類提供額外的資訊。java代碼中通過注解(限制為retentionpolicy.class或retentionpolicy.runtime的annotation)提供
通過`javap`指令我們可以檢視解析後的位元組碼
位元組碼顯示三個主要的區域:常量池、構造函數和sayhello方法。
常量池:提供類似于符号表的資訊。
方法:每個方法均含四個區域
1. 簽名和通路标志;
2. 方法體的位元組碼;
3. 行号表:為調試器提供java代碼與位元組碼的行号映射關系資訊。
4. 局部變量表:羅列當且目前方法的所有局部變量名。
(譯者語:由于後續内容為對位元組碼指令的講解,沒什麼必要翻譯了是以..............)
jvm啟動時通過bootstrap classloader加載初始類。在執行 public static void main(string[]) 方法前,這個類需要經過連結、初始化操作。然後在執行這個方法時就會觸發其他類和接口的加載、連結和初始化操作。
**加載**,通過特定的名稱搜尋類或接口檔案,并将其内容加載為位元組數組。(譯者語:這裡加載的工作已經完成了,後面内容是加載+連結的内容)然後位元組數組被解析為符合java版本号的類對象(如object.class),而該類或接口的直接父類和直接父接口也會被加載。
**連結**,由驗證class檔案合法性、準備和可選的解析三個步驟組成。
1. **驗證**,就是要根據java和jvm規範對類或接口位元組碼的格式和語義進行校驗。下面羅列部分校驗項:
1.1. 符号表具有一緻和合法的格式;
1.2. 不可更改的方法和類沒有被重寫;
1.3. 方法含有效的通路控制關鍵字;
1.4. 方法含有效的入參類型和數目;
1.5. 位元組碼沒有對操作數棧進行非法操作;
1.6. 變量先初始化後使用;
1.7. 變量值與變量類型比對。
在類加載階段進行驗證雖然會減慢加載速度,但可以減少運作時對同一類或接口進行重複驗證。
2. **準備**,為靜态字段、靜态方法和如方法表等jvm使用的資料配置設定記憶體空間,并對靜态字段進行初始化。但這個時候該類或接口的構造函數、靜态構造函數和方法均沒有被執行。
3. **解析(可選項)**,檢查符号引用并加載所引用的類或接口(加載直接父類和直接接口)。當沒有執行這一步驟時,則在運作時中調用這個類或接口時在執行。
**初始化**,執行類的靜态構造函數 <clinit> 。
jvm中有多個不同類型的類加載器。bootstrap classloader是頂層的類加載器,其他類加載器均繼承自它。
1. **bootstrap classloader**,由于在jvm加載時初始化,是以bootstrap classloader是用c++編寫的。用于加載java的核心api,如rt.jar等位于boot類路徑的高信任度的類,而這些類在連結時需要的校驗步驟比一般類要少不止一點點。
2. **extenson classloader**,用于加載java的擴充apis。
3. **system classloader**,預設的應用類加載器,用于從classpath中加載應用的類。
4. **user defined classloaders**,應用内部按一定的需求将對類分組加載或對類進行重新加載。
從hotspot5.0開始引入了共享類資料(cds)特性。在安裝jvm時則會将如rt.jar中的類加載到一個記憶體映射共享文檔中。然後各jvm執行個體啟動時直接讀取該記憶體中的類,提高jvm的啟動速度。
《java virtual machine specification java se 7
edition》明确聲明:“雖然方法區邏輯上位于堆中,簡單的實作方式應該是被垃圾回收。”沖突的是oracle
jvm的jconsole告知我們方法區和代碼緩存是位于非堆記憶體空間中的。而openjdk則将代碼緩存設定為虛拟機外的objectheap中。
每個類都持有一個指向加載它的類加載器指針,同樣每個類加載都持有一組由它加載的類的指引。
每個類都對應一個運作時常量池(有class檔案中的常量池生成)。運作時常量池與符号表類似但包含更多的資訊。位元組碼指令中需要對資料進行操作,但由于
資料太大無法直接存放在位元組碼指令當中,于是通過将資料存放在常量池,而位元組碼指令存放資料位于常量池的索引值來實作指令對資料的操作。動态連結也是通過
運作時常量池來實作的。
運作時常量池包含以下的類型的資料:
1. 數字字面量;
2. 字元串字面量;
3. 類引用;
4. 字段引用;
5. 方法引用。
舉個栗子:
`new`操作碼後的#2操作數就是常量池第2項的索引,該項為類型引用,内含一個縮略utf8類型的常量來存放類的全限定名(java/lang
/object)。在進行動态符号連結時則通過該名稱來查找類對象`java.lang.object`。而`new`操作碼會建立一個類的執行個體、初始化
執行個體的字段,并将該對象壓入操作數棧。`dup`複制棧頂元素并壓棧,然後`invokespecial`則彈出操作數棧頂的一個元素執行對象的構造函
數。
再舉個栗子:
class的常量池包含以下類型:
integer 一個4bytes的整型常量
long 一個8bytes的長整型常量
float 一個4bytes的浮點型常量
double 一個4bytes的雙精度浮點型常量
string 字元串引用,指向一個縮略utf8常量
utf8 縮略utf8編碼的字元串
class 類型引用,指向一個縮略utf8常量,存放類全限定名(用于動态連結)
nameandtype 存放兩個引用,一個指向用于存放字段或方法名的縮略utf8常量,一個指向存放字段資料類型或方法傳回值類型和入參的縮略utf8常量
fieldref, 存放兩個引用,一個指向表示所屬類或接口的class常量,一個指向描述字段、方法名稱和描述符的nameandtype常量
methodref,
interfacemethodref
異常表的每一項表示一項異常處理,表項字段如下:起始位置、結束位置、處理代碼的起始位置和指向常量池class常量的位置索引。
隻要java代碼中出現try-catch或try-finally的異常處理時,就會建立異常表,異常表的表項用于存放try語句塊在位元組碼指令集中的
範圍、捕捉的異常類和相應的位元組碼處理指令的起始位置。(譯者注:try-finally所建立的表項的異常類引用為0)<br/>
當發生異常并沒有被捕獲處理,則會從線程棧的目前棧幀抛出并觸發彈棧操作,再棧頂棧幀接收,直到異常被某個棧幀捕獲處理或該線程棧為空并退出線程然後異常有系統異常處理機制捕獲。
finally語句塊的代碼無論是否抛出異常均會執行。
hotspot虛拟機在永久代中增加了符号表。該表為哈希表用于将直接引用與運作時常量池的符号引用作映射。<br/>
另外每個表項還有個引用計數器,用來記錄有多少個符号引用指向同一個直接引用。假如某個類被解除安裝了那麼類中的所有符号引用将無效,則對應的符号表表項的引用計數器減1,當計數器為0時則将該表項移除。
java語言說明中要求字元串字面量必須唯一,一樣的字元串字面量必須為同一個string執行個體。
hotsport虛拟機通過字元串表來實作。字元串表位于永久代中,表項為string執行個體位址與字元串字面量的映射關系資訊。加載類時成功執行連結的準
備階段時,class檔案常量池下的constant_string_info常量的資訊均加載到字元串表中。而執行階段可以通過
string#intern()方法将字元串字面量加入到字元串表中。如:
string#intern(),會先去字元串表查找字面量相同的表項,有則傳回對應的對象引用,沒有則先将新的字元串對象和字面量添加到表中,然後再傳回對象引用。
總結
本文對jvm記憶體模型做了概要的說明,讓初次接觸jvm的朋友對它有一個初步的big photo,在此感謝作者的分享。