Java 虛擬機簡介
本文是閱讀《深刻理解 Java 虛擬機:JVM 高級特性與最佳實踐》的筆記java
推薦學習資料:linux
- 《The Java Virtual Machine Specification, Java SE 7 Edition》
http:// hllvm. group. iteye. com/
,高級語言虛擬機圈子
概述
Java 技術體系 4 平臺
- Java card
- Java ME(Micro Edition),手機、PDA 等,對 Java API 有所精簡
- Java SE(Standard Edition),桌面級應用
- Java EE(Enterprise Edition),除桌面功能外提供企業級功能,如 ERP、CRM等,Java EE 之前稱爲 J2EE
從 JDK 1.5 之後,官方就再也不使用這種命名方式,而是使用 JDK 五、JDK 6 這種形式編程
Java 虛擬機實現
- Sun HotSpot VM
- Hot 指的是熱點代碼探測,例如一段被頻繁調用的函數將會觸發編譯器優化以提升這段代碼的執行速度
- Sun Mobile-Embedded VM/Meta-Circular VM,用於嵌入式平臺
BEA JRockit/IBM J9 VM
、Azul VM/BEA Liquid VM
、Microsoft JVM
(因侵權而放棄),等等
混合語言
Clojure、Jruby、Groovy 等語言都基於 Java 虛擬機,且各有特色,分別適用於不一樣的場景windows
其餘 Java 特性
- Fork/Join 多線程模型
- 函數式編程,Java 8,lambda
- OpenJDK 是 sun 2006 年把 Java 開源而造成的項目。OpenJDK 對 windows 的支持並不友好
JVM 內存
graph TB A[JVM memory] A --> B[線程間共享<br/><執行引擎>] style B fill:#f9f A --> C[線程私有<br/><本地庫接口>] style C fill:#f90 B --> D[方法區<br/>常量池<br><小字符串等>] B --> E[堆] C --> F[虛擬機棧] C --> G[本地方法棧] C --> H[程序計數器] A --> I[直接內存區<br/>非JVM管理] style I fill:#690 J[部分 JVM 實現不區分] F --> J G --> J E --> K[指針碰撞<br/>空閒列表<br/>TLAB] E --> L[堆溢出<br/>棧溢出] D --> M[溢出<br/>CGLib&SJP]
- 線程私有
- 程序計數器
- 與 CPU 的程序計數器相似,不過在 Java 中這個計數器可能指向的是字節碼位置(不一樣實現可能不一樣)
- 不一樣線程都有本身的程序計數器
- 不會拋出 OutOfMemoryError 異常
- Java 虛擬機棧
- 與 OS 中進程的棧概念類似,每一個 Java 方法都有本身的棧幀用於保存局部變量、動態連接等信息,例如基本數據類型(boolean、byte、char等)、對象引用和 returnAddress
- 編譯時已知的各類基本類型數據保存在棧中的局部變量表中,局部變量表的大小在編譯時肯定
- StackOverflowError or OutOfMemoryError,Java 容許固定長度的虛擬機棧
- 本地方法棧
- 與 Java 虛擬機棧的功能相似,不過這個棧由 Java 中所調用的 Native 方法使用
- 有些 JVM 將本地方法棧和虛擬機棧合併,Java 方法和 Native 方法都使用相同的棧空間
- A native method is a Java method whose implementation is provided by non-java code
- 線程共享
- Java 堆(GC 堆)
- 虛擬機啓動時建立,保存絕大部分對象實例與數組。隨着 JIT 和逃逸分析等技術的發展對象未必必定在堆上建立
- Java 堆是垃圾蒐集器管理的主要區域
- Java 虛擬機規範中規定堆能夠位於物理上不連續的內存空間,不過主流的實現都按照可擴展來實現
- 方法區
- 保存已經被虛擬機加載的類信息、常量、靜態變量、JIT 編譯後的代碼等書籍
- 方法區的內存回收是必要的,雖然不多使用,有些 BUG (如內存泄漏)是由於方法區沒有回收內存
- HotSpot 虛擬機稱之爲永久代,由於此虛擬機將 GC 擴展到了方法區,其餘虛擬機不存在這個概念
- 運行時常量池
- 存儲編譯時生成的字面值和符號引用,Java 中常量不必定非要是編譯期才能產生,因此這個區域是動態的
- Java String 中的 intern (native 方法)方法會嘗試將字符串放到常量池中並返回對常量池的引用,JDK 1.6 和 JDK 1.7 之間的實現有區別
- 直接內存
- 爲了不拷貝數據,Java 提供直接操做非虛擬機管理的內存區域,這些內存被稱爲直接內存
JVM 內存 & OS 內存
- 32位 OS 給每一個進程可用的內存空間是有限的, windows/linux 爲 2GB(1.9GB)
- JVM 提供了設定 Java 堆(-Xms/-Xmx)、方法區、每一個線程棧幀(-Xss)內存大小的機制
對象的建立(HotSpot 爲例)
graph LR A(new obj) --> B[方法區檢查] B -->|YES| C[calloc] B -->|NO| D[load] D --> C C --> E[構造句柄] E --> F[構造對象]
虛擬機內存概念
指針碰撞
假設 Java 堆中內存是絕對規整的,全部用過的內存都放在一邊, 空閒的內存放在另外一邊, 中間放着一個指針做爲分界點的指示器, 那所分配內存就僅僅是把那個指針向空閒空間那邊挪動 一段與對象大小相等的距離,這種分配方式稱爲「 指針碰撞」( Bump the Pointer)數組
空閒列表
若是 Java 堆中的內存並非規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞 了,虛擬機就必須 維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的錄, 這種分配方式稱爲「 空閒列表」( Free List)ruby
TLAB
用於多線程對象的建立,多線程同時在堆上建立對象時不管使用指針碰撞仍是空閒列表,都會涉及同步問題多線程
每一個線程在 Java 堆中預先分配一小塊內存,稱爲本地線程分配緩衝( Thread Local Allocation Buffer, TLAB)。哪一個線程要分配內存, 就在哪一個線程的 TLAB 上分配, 只有 TLAB 用完並分配新的 TLAB 時,才須要同步鎖定框架
可使用 -XX:+/-UseTLAB
來指定虛擬機是否使用TLABjvm
對象存儲
對象頭
HotSpot 對象頭由兩部分組成:jsp
- 運行時數據,例如哈希碼( HashCode)、 GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等,這些數據通常保存在固定的 32bits 或者 64bits 中
- 對象類型指針,對應於 C++ 中的 vptr
實例數據 & 對齊填充
實例數據就是 Java 對象的有效信息,例如類中的成員變量等
HotSpot 規定對象的起始地址必須是 8 字節的整數倍,這個和 C/C++ 中內存對齊的概念是類似的
對象訪問
對象訪問分兩類,使用句柄和直接指針,以下圖所示
使用句柄的好處是便於垃圾回收與內存整理,只要修改句柄中對象的指針就能夠移動對象
使用直接指針的好處是速度快,其比句柄方法少了一次指針定位的開銷
graph LR A[<b>stack</b><br>obj handle refer] A --> B[<b>handle in heap</b><br/># pointer to obj data<br/># pointer to obj type infos] B --> C[<b>heap</b><br/>obj data] B --> D[<b>methods area</b><br/>obj type infos] E[<b>stack</b><br>obj refer] E --> F[<b>heap</b><br/>obj data<br/>pointer to type infos] F --> G[<b>methods area</b><br/>obj type infos]
溢出實例
堆溢出
import java.util.*;
// args: Args:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError
public class test {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
使用工具(Eclipse memory analyzer 分析 dump 文件)判斷是內存泄漏仍是內存溢出,也可使用工具查看對象到 GC Roots 的引用鏈,從而定位對象的來源
棧溢出
兩種異常
- StackOverflowError,棧溢出
- OutOfMemoryError,JVM 在擴展棧時沒法申請到足夠的內存空間
// VM Args:-Xss128k
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++; // 遞歸時記錄溢出時 棧幀個數
stackLeak(); // 每一個函數調用都有本身的棧幀,用於保存返回地址等信息
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println(" stack length:" + oom.stackLength);
System.out.println(e);
System.exit(0);
}
}
}
String.intern() 簡介
String.intern() 是一個 Native 方法,它的做用是:若是字符串常量池中已經包含一個等於此 String 對象的字符串,則返回表明池中這個字符串的 String 對象;不然,將此 String 對象包含的字符串添加到常量池中,而且返回此 String 對象的引用
JDK 6 和 JDK 7 中 intern 的實現有必定區別。JDK 6 將字符串對象保存在常量池中;JDK 7 將字符串對象保存在堆中而將字符串引用保存在常量池中
方法區溢出 & CGlib & JSP
可修改字節碼以生成新的類,Spring、Hibernate 等框架都基於此類字節碼技術
運行時使用 CGLIB 能夠建立大量的類從而出現方法區溢出的異常
方法區溢出的另外一種場景就是包含大量 JSP 文件的場景,JSP 須要被編譯爲 Java 類才能運行