深入理解HotSpot JVM 基本原理

關於JAVA

Java®編程語言是一種通用的、併發的、面向對象的語言。它的語法類似於C和C++,但它省略了許多使C和C++複雜、混亂和不安全的特性。

Java 是幾乎所有類型的網絡應用程序的基礎,也是開發和提供嵌入式和移動應用程序、遊戲、基於 Web 的內容和企業軟件的全球標準。.

從筆記本電腦到數據中心,從遊戲控制檯到科學超級計算機,從手機到互聯網,Java 無處不在!

Java的技術體系主要有各種硬件平臺上的JVM虛擬機、提供各開發領域接口支持的Java API、Java編程語言、三方Java框架(Spring等)構成。

Java程序設計語言、Java虛擬機、Java API類庫這三部分統稱爲JDK(Java Development Kit),JDK是用於支持Java程序開發的最小環境。

可以把Java API類庫中的Java SE API子集和Java虛擬機這兩部分統稱爲JRE(Java Runtime Environment),JRE是支持Java程序運行的標準環境。

下圖展示了Java技術體系所包含的內容,以及JDK和JRE所涵蓋的範圍。

關於JVM

Java虛擬機是Java平臺的基石。負責其硬件和操作系統的獨立性,爲Java字節碼的執行提供運行時環境。

JVM虛擬機在Java 虛擬機規範中沒有規定具體實現,而是有各大廠商自己實現。

Implementation details that are not part of the Java Virtual Machine's specification would unnecessarily constrain the creativity of implementors. For example, the memory layout of run-time data areas, the garbage-collection algorithm used, and any internal optimization of the Java Virtual Machine instructions (for example, translating them into machine code) are left to the discretion of the implementor.

Classic VM 是“世界上第一款商用Java虛擬機”,在JDK 1.2之前是Sun JDK中唯一的虛擬機。

在JDK 1.2時,它與HotSpot VM並存,而在JDK 1.3時,HotSpot VM成爲默認虛擬機,直到JDK 1.4的時候,Classic VM才完全退出商用虛擬機的歷史舞臺。

1999年4月27日,HotSpot虛擬機發布,HotSpot最初由一家名爲“Longview Technologies”的小公司開發,因爲HotSpot的優異表現,這家公司在1997年被Sun公司收購了。後來它成爲了JDK 1.3及之後所有版本的Sun JDK的默認虛擬機。

在2008年和2009年,Oracle公司分別收購了BEA公司和Sun公司,這樣Oracle就同時擁有了兩款優秀的Java虛擬機:JRockit VM和HotSpot VM。

關於HostSpot

Java HotSpot虛擬機是Sun用於Java平臺的VM。 它使用許多先進技術爲Java應用程序提供最佳性能,包括最先進的內存模型,垃圾收集器和自適應優化器。

在SUN/Orace JDK中包括兩種風格的VM

  • client mode
  • server mode

默認以client mode啓動。

啓動命令加- server,以server mode啓動。

查看當前JVM mode:

兩種mode的區別:

client mode

  • 短時間內啓動,運行時,佔用更少內存
  • C1輕量級編譯器,優化較少
  • 適合輕量級程序和桌面程序

server mode

  • 啓動慢,運行時,佔用更大的內存
  • C2重量級編譯器,更徹底的優化
  • 能提供更好的性能,適合生產部署

HotSpot JVM Architecture

HotSpot JVM 主要包括3個組件:

  • Class Loader Subsystem
  • Runtime Data Areas
  • Execution Engine

Class Loader Subsystem

Class Loader Subsystem是JVM必不可少的核心,用於讀取/加載.class文件,並把字節碼保存在JVM方法區。

加載過程

Java虛擬機中類加載的全過程:加載,驗證,準備,解析,初始化。

Loading

Loading階段,虛擬機完成三件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流;
  2. 將這個字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構;
  3. 在內存中生成一個代表這個類的java.lang.class對象,作爲方法區這個類的各種數據的訪問入口。

加載的第一步是在Java虛擬機外部去實現的,有‘類加載器’來完成加載動作。

絕大部分Java程序都會用到以下3種系統提供的類加載器:

  1. 啓動類加載器(Bootstrap ClassLoader)
    • 使用C++語言實現,是虛擬機自身的一部分。
    • 加載<JAVA_HOME>/lib中虛擬機識別的jar,如rt.jar。
    • 無法被Java程序直接引用
  2. 擴展類加載器(Extension ClassLoader)
    • 由sun.misc.Launcher$ExtClassLoader實現
    • 加載<JAVA_HOME>/lib/ext中類庫
    • Java程序可以直接使用
  3. 應用程序類加載器(Application ClassLoader)
    • 由sun.misc.Launcher$AppClassLoader實現
    • 也稱系統類加載器,加載應用程序classpath下的jar
    • 可以直接使用,程序中默認的類加載器

Linking

執行類或接口的鏈接。

  • Verification,驗證。確保Class文件的字節流中包含的信息符合當前虛擬機的要求,且不會危害虛擬機的安全。
  • Preparation,準備。在方法區中爲類變量(被static修飾的變量)分配內存並設置初始值。
  • Resolution,解析。虛擬機將常量池的符號引用替換爲直接引用。

Initialization

執行類加載的最後階段,爲所有靜態變量都分配了原始值,靜態塊從父類執行到子類。

加載原則

  1. Delegation,委派
  2. Visibility,可見性
  3. Uniqueness,唯一性

Delegation

雙親委派模型

雙親委派模型在JDK1.2中被引入,要求除了啓動類加載器,其他類加載器都要有父類加載器。

加載過程:當一個類加載器收到加載請求的時候,先委派給父類加載器去完成,每個層次的類加載器都是如此,最終加載請求傳到頂層啓動類加載器。如果父類無法完成加載請求,子類纔會去嘗試加載。

破壞雙親委派模型

雙親委派解決了各類加載器的基礎類的統一問題。

如果基礎類需要反調用戶代碼,怎麼辦?

線程上下問類加載器(Thread Context ClassLoader),TCCL。解決基礎類反調用戶代碼。例如JDK中實現SPI機制的JDBC、JNDI等。

Visibility

子類加載器能看到父類加載器加載的類。父類加載器看不到子類加載器中的類。

Uniqueness

父類加載器加載過的類不會被子類加載器加載。

Runtime Data Areas

Runtime Data Areas大致可以分爲兩部分:

  • 線程私有區,線程創建時分配內存。線程啓動時初始化,並在線程完成後銷燬,
  • 線程共享區,所有線程都可以訪問。JVM啓動時初始化,在關閉時銷燬。

程序計數器

保存當前正在執行的JVM指令地址。每個線程都有自己的PC。

Java虛擬機棧

每個Java虛擬機線程都有一個私有Java虛擬機堆棧,與線程同時創建。

本地方法棧

供用非Java語言實現的本地方法的堆棧。換句話說,它是用來調用通過JNI(Java Native Interface Java本地接口)調用的C/C++代碼。根據具體的語言,一個C堆棧或者C++堆棧會被創建。

Java堆

用來保存實例或者對象的空間,而且它是垃圾回收的主要目標。當討論類似於JVM性能之類的問題時,它經常會被提及。JVM提供者可以決定怎麼來配置堆空間,以及不對它進行垃圾回收。

方法區

方法區是所有線程共享的,它是在JVM啓動的時候創建的。它保存所有被JVM加載的類和接口的運行時常量池,成員變量以及方法的信息,靜態變量以及方法的字節碼。JVM的提供者可以通過不同的方式來實現方法區。在Oracle 的HotSpot JVM裏,方法區被稱爲永久區或者永久代(PermGen)。是否對方法區進行垃圾回收對JVM的實現是可選的。

JDK1.8以後,PermGen被永久移除,有Metaspace(元空間)來實現方法區。

與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。

因此,默認情況下,元空間的大小僅受本地內存限制

運行時常量池

這個區域和class文件裏的constant_pool是相對應的。這個區域是包含在方法區裏的,不過,對於JVM的操作而言,它是一個核心的角色。因此在JVM規範裏特別提到了它的重要性。除了包含每個類和接口的常量,它也包含了所有方法和變量的引用。簡而言之,當一個方法或者變量被引用時,JVM通過運行時常量區來查找方法或者變量在內存裏的實際地址。

Execution Engine

Interpreter

讀取字節碼指令並以順序方式執行。

JIT

JIT (Just In Time) Compiler。

抵消了Interpreter執行速度慢的缺點並提高了性能。 JIT編譯器同時編譯字節碼的類似部分,從而減少了編譯所需的總時間。

Garbage Collection

通過收集和刪除未引用的對象來釋放內存。

對象是否存活

在堆裏面存放着Java世界幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事就是要判斷對象是否存活。

引用計數算法

引用計數算法(Reference Counting)實現簡單,判定效率也很高。給對象中添加一個引用計數器,每當一個地方引用它時,計數器值就加1;引用失效時,計數器值就減1;計數器爲0的對象就是不可能再被使用的。

Python語言、遊戲腳本領域被廣泛應用的Squire中都使用了引用計數算法進行內存管理。但是,java虛擬機裏沒有用,主要原因是很難解決對象之間相互循環引用的問題

可達性分析

可達性分析(Reachability Analysis),基本思路是通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始往下搜索,搜索所走過的路程稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象不可用。

Java語言中,可作爲GC Roots的對象包括以下幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象;
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(Native方法)引用的對象

垃圾收集算法

常見的垃圾收集算法。

標記-清除算法

算法分爲‘標記’、‘清除’兩個階段。

首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。

複製算法

複製(Copying),將可用內存按容量分爲大小相等的兩塊,每次只使用其中的一塊。 當這一塊的內存用完了,將還存活的對象複製到另外一塊上去。把已使用過的內存空間一次清理掉。

實現簡單,運行高效,內存使用率不高,用於回收新生代。

IBM研究表明,新生代中98%的對象都是在第一次GC時被回收掉,不需要按照1:1分配空間。

HotSpot虛擬機默認Eden和Survivor比例是8:1,只有10%的內存會被浪費。

Survivor空間不夠時,需要依賴老年代進行分配擔保,新生代收集下來的存活對象直接進入老年代。

標記-整理算法

標記-整理(Mark-Compact)算法,標記所有存活對象,向一端移動,然後直接清理掉端邊界意外的內存。

分代收集算法

根據對象存活週期不同,將Java堆分爲新生代和老年代。

新生代,採用複製算法。

老年代採用‘標記-清理’或者‘標記-整理’算法。

垃圾收集器

如果說,收集算法是內存回收的方法論,那麼,垃圾收集器就是內存回收的具體體現。

Java虛擬機規範沒有規範如何實現垃圾收集器。不同的廠商、版本的虛擬機實現可能會有很大差別。

下面,基於JDK1.7 Update14之後的HotSpot虛擬機討論收集器。

Serial 收集器

"Serial" is a stop-the-world, copying collector which uses a single GC thread.

ParNew 收集器

"ParNew" is a stop-the-world, copying collector which uses multiple GC threads.

Parallel Scavenge 收集器

"Parallel Scavenge" is a stop-the-world, copying collector which uses multiple GC threads.

Serial Old 收集器

"Serial Old" is a stop-the-world, mark-sweep-compact collector that uses a single GC thread.

Parallel Old 收集器

"Parallel Old" is a compacting collector that uses multiple GC threads. Using the -XX flags for our collectors for jdk6.

CMS收集器

"CMS"(Concurrent Mark Sweep) is a mostly concurrent, low-pause collector.

G1收集器

G1(Garbage First) straddles the young generation - tenured generation boundary because it is a generational collector only in the logical sense. G1 divides the heap into regions and during a GC can collect a subset of the regions.

JMM

Java內存模型(Java Memory Model),JMM,用來屏蔽掉各種硬件和操作系統之間內存訪問差異,以實現在各種平臺下一致的內存訪問效果。

  1. JMM主要目標是定義程序中各個變量的訪問規則。即在虛擬機中將變量存儲到內存和從內存中取出變量。

  2. 所有變量都存儲在主內存(Main Memory),每個線程還有自己的工作內存(Working Memory)。

  3. 線程對變量的讀取、賦值等操作都必須在工作內存中進行。

  4. 線程間變量值的傳遞必須通過主內存來完成。

基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是也爲計算機系統帶來更高的複雜度,因爲它引入了一個新的問題:緩存一致性(CacheCoherence)。

除了增加高速緩存之外,爲了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程序中各個語句計算的先後順序與輸入代碼中的順序一致

Java內存模型規定了所有的變量都存儲在主內存(Main Memory)中(此處的主內存與介紹物理硬件時的主內存名字一樣,兩者也可以互相類比,但此處僅是虛擬機內存的一部分)。

每條線程還有自己的工作內存(Working Memory,可與前面講的處理器高速緩存類比),線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。

不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成

線程、主內存、工作內存三者的交互關係如圖12-2所示。

這裏所講的主內存、工作內存與Java內存區域中的Java堆、棧、方法區等並不是同一個層次的內存劃分,這兩者基本上是沒有關係的。

如果兩者一定要勉強對應起來,那從變量、主內存、工作內存的定義來看,主內存主要對應於Java堆中的對象實例數據部分,而工作內存則對應於虛擬機棧中的部分區域。

從更低層次上說,主內存就直接對應於物理硬件的內存,而爲了獲取更好的運行速度,虛擬機(甚至是硬件系統本身的優化措施)可能會讓工作內存優先存儲於寄存器和高速緩存中,因爲程序運行時主要訪問讀寫的是工作內存。

volatile

關鍵字volatile可以說是Java虛擬機提供的最輕量級的同步機制

當一個變量定義爲volatile之後,它將具備兩種特性:

  • 第一是保證此變量對所有線程的可見性
  • 第二個語義是禁止指令重排序優化

這裏的“可見性”是指當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得知的。

禁止指令重排序優化,普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。

參考

Our Collectors

瞭解 Java 技術

Java Virtual Machine from Sun

Java Garbage Collection Introduction

The Java® Virtual Machine Specification

Java發展史

The Java HotSpot Performance Engine Architecture

JVM Server vs Client Mode

JSR-133: JavaTM Memory Model and Thread Specification

JVM Architecture: JVM Class loader and Runtime Data Areas

JVM Architecture: Execution Engine in JVM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章