JVM 知識體系漫談
背景介紹
jvm已經是Java開發的必備技能了,jvm相當於Java的作業系統。JVM,java virtual machine, 即Java虛擬機器,是執行Java Class檔案的程式。
Java程式碼經過Java編譯器編譯,會編譯成class檔案,一種平臺無關的程式碼格式,class檔案按照jvm規範,包括了java程式碼執行所需的後設資料和程式碼等內容。jvm載入class檔案後,就可以執行java程式碼了。
JVM有不同的實現,有我們熟悉的Hotspot虛擬機器,JRockit等。在各個作業系統上,又回有各自的虛擬機器實現,從而形成了Java程式碼 > class檔案 > JVM規範 > JVM實現的層次。再加上其他語言如scala、groovy也能夠生成class檔案,這樣不僅實現了平臺無關性,也實現了語言無關性。
JVM體系,分為JVM記憶體結構,Class檔案結構,Java ByteCode,垃圾收集演算法和實現,調優和監控工具,以及Java記憶體模型(JMM)。
JVM記憶體結構
通常,認為大概分為執行緒共享的區域和執行緒私有的區域。共享區域在JVM啟動時建立,私有區域伴隨這執行緒的啟動和結束。
私有區域
一個執行緒擁有的結構有
程式計數器(Program Counter, PC)
Java天生支援多執行緒,多執行緒會有執行緒切換的問題,當一個執行緒從可執行狀態得到CPU排程進入執行狀態,CPU需要知道從哪裡開始執行,並且Java是一種基於棧的執行架構(區別於基於暫存器的架構)。當執行一個Java方法時,PC會指向下一條指令的位置。執行native方法時,PC是未定義。操作指令可能會有0個或多個運算元。JVM的執行流程大概可以描述為:
while(true) { opcode = code[pc]; oprand = getOperan(opcode); pc = code[pc + len(oprand)]; execute(opcode, oprand); }
Java虛擬機器棧(Java Virtual Machine Stack)
Java虛擬機器棧,或者叫方法棧,會伴隨這方法的呼叫和返回進行相應的入棧和出棧。棧的元素是棧幀(Stack Frame), 棧幀中的內容包括: 運算元棧,本地變數表,動態連結等資訊。當執行緒呼叫一個方法的時候,會組裝對應的棧幀入棧。
本地變數表(Local Variable Table)
本地變數表儲存方法的引數、方法內部建立的區域性變數。本地變數表的大小在編譯時就確定了。本地變數表會根據變數的作用範圍選擇重用一個位置。本地變數表會存放int,char,byte,float,double,long,address(例項引用)。其中除了double和long其他變數佔用一個slot,一個slot指一個抽象的位置,在32位虛擬機器中是32bit大小,double和long佔用兩個slot。
值得注意的是,如果一個方法是例項方法,Java編譯器會將this作為第一個引數傳入本地變數表。另外Java中物件導向,方法呼叫可以這樣理解
例項方法
obj.method(var1, var2, var3) => method invoke obj var1 var2 var3
運算元棧
運算元棧用於方法內執行儲存中間結果,Java方法中的程式碼邏輯就是通過運算元棧來實現的。和本地方法表一樣,運算元棧也是在編譯時就確定最大大小了,即最大深度。運算元棧可以和本地變數表互動,進行資料的存放和讀取。下面用一個簡單的例子展示一下。
int add(int a, int b) {
return a + b;
}
這個例項方法經過Java編譯器編譯後生成的位元組碼
本地變數表
slot0 this
slot1 a
slot2 b
方法位元組碼
iload_1 #讀位置是1的本地變數(本地變數表從0開始,位置0是this引用)
此時運算元棧是 a
iload_2 #讀位置是2的本地變數,即b
此時運算元棧是 a b
iadd #進行int型別的add操作,會取出棧頭的兩個元素取出進行相加並將結果入棧。
此時運算元棧是 c (相加的結果)
ireturn #ireturn指令會將棧頭元素返回給呼叫方法的棧幀
執行緒共享區域
堆(Heap區)
建立的物件(包括普通例項和陣列)都分配在Heap區(不考慮一些虛擬機器的棧上分配優化技術)。在細分的話,一般還分成年輕代和老年代。這是基於這樣一個類似28原理的統計,90%多的物件都是很快成為垃圾的物件。所以化為成兩個區域,分別使用不同的收集演算法,年輕代的收集頻率更高,所需空間也相對較小。記憶體分配時,多個執行緒會有併發問題,主要通過兩種方式解決:1.CAS加上失敗重試分配記憶體地址。2. TLAB, 即Thread Local Allocation Buffer, 為每個執行緒分配一塊緩衝區域進行物件分配。年輕代還可以分為兩個大小相等的Survivor和一個Eden區域。物件在幾種情況下會進入老年代:1. 大物件,超過Eden大小或者PretenureSizeThreshold. 2. 在年輕代的年齡(經歷的GC次數)超過設定的值的時候 3. To Survivor存放不下的物件
方法區
方法區存放載入的類資訊和執行時常量池等。
垃圾收集(Garbage Collect)
Java中不需要對記憶體進行手動釋放,JVM中的垃圾回收器幫助我們回收記憶體。
何時進行收集
一般來說,當某個區域記憶體不夠的時候就會進行垃圾收集。如當Eden區域分配不下物件時,就會進行年輕代的收集。還有其他的情況,如使用CMS收集器時配置CMSInitalize
如何判斷一塊記憶體是垃圾
即判斷一個物件不再使用,不再使用可以是沒有有效的引用。
一般來說,主要有兩種判斷方式
引用計數
當有物件引用自身時,就會計數器加1,刪除一個引用就減一,當計數為0時即可判斷為垃圾。python等語言使用引用計數。引用計數存在迴圈引用問題,如兩個落單的A和B互相引用,但是沒有其他物件指向它們這種情況。
可達性分析
通過一些根節點開始,分析引用鏈,沒有被引用的物件都可以被標記為垃圾物件。根節點是方法棧中的引用、常量等。
垃圾收集演算法
標記清除(Mark Sweep)
對非垃圾物件進行標記都,清除其他的物件。這種方式對對記憶體空間造成空隙,即記憶體碎片,最終導致有空餘空間,但沒有連續的足夠大小的空間分配記憶體。
標記整理(Mark Compact)
標記非垃圾物件後,將這些物件整理好,排列到記憶體的開始位置。這樣記憶體就是整齊的了。但是因為會造成物件移動,所以效率會有降低。
標記清除整理(Mark Sweep Compact)
即組合兩種方式,在若干次清除後進行一次整理。
複製(Copy)
劃分成兩個相同大小的區域,收集時,將第一個區域的活物件複製到另一個區域,這樣不會有記憶體碎片問題。但是最多隻能存放一半記憶體。
垃圾收集器
垃圾收集器就是垃圾收集演算法的相應實現。
Serial New
新生代單執行緒的收集器,是Client模式預設的垃圾收集器
Parallel New
Serial New的多執行緒版本。ParNew常和CMS拉配使用。這裡說明一些Parallel和Concurrent即並行和併發在垃圾收集這裡的表示的不同,並行表示有多個執行緒同時進行垃圾收集,併發是指垃圾收集執行緒和應用執行緒可以併發執行。
Parallel Scanvenge
PS收集器是注重吞吐量(ThroughPut)的收集器。
Serial Old。
老年代的單執行緒收集器
Parallel Old
Serial Old的多執行緒版本,由於Parallel Scavenge不能和CMS搭配使用,所以會是使用PS時的一種選擇。
CMS (Concurrent Mark Sweep)
注重延遲latency的收集器,在互動式應用中,如面向使用者的web應用,需要儘可能減少垃圾收集造成的停頓時間。在總的統計上,吞吐量可能沒有PS收集器高。
細分上,CMS還分為4個階段
- 初始標記,標記GC Root可以直達的物件。STW
- 併發標記,從第一步標記的物件開始,進行可達性分析遍歷,和應用執行緒併發執行。
- 重新標記,SWT,修正上一階段併發執行造成的引用變化。
- 併發清除,併發的清除垃圾
CMS使用標記清除演算法,所以有記憶體碎片問題,可能設定引數在進行若干次不帶整理的收集後進行一次帶整理(compact)的收集。另外,因為垃圾收集是和應用執行緒併發執行的,在收集的同時可能還會有垃圾不斷產生,即產生了浮動垃圾。另外還需要預留出一定空間,到達這個值後進行收集,但是還會有收集速度趕不上生產的速度,這時就會出現Concurrent Mode Failure,CMS會退化成Serial Old進行GC。G1 (Garbage First)
具有大記憶體收集和目標效率時間等控制能力,目標是代替CMS。G1通過將記憶體劃分成不同的區域(Region),並對不同區域計算分數,分析那個Region最具有收集價值。
一些JVM的GC引數
常用的引數設定有
- -Xms=4g -Xmx=4g 設定Java堆的初始大小和最大大小均為4g,即避免了堆大小調整
- -Xmn=1g 設定年輕代的總大小為1g
- -SurvivorRatio=8, 設定Eden和一個Survivor的比例為8:1
- -XX:+PringGCDetails
堆外記憶體(Non Heap)
Nio中的DirectByteBuffer就是堆外記憶體的一部分,這部分記憶體只能通過Full Gc進行清理。一些框架會通過System.gc呼叫手動觸發gc,但是在啟動引數中可能設定了禁止呼叫System.gc()。另外當設定堆過大時可能會造成堆外記憶體不夠導致OOM。
監控工具
監控工具幫助我們在執行時或問題發生後分析現場,分析記憶體分佈狀態,哪裡導致記憶體洩漏等(本該被釋放的物件仍然被引用)。
命令列工具
HotspotJVM的bin目錄下有很多可用的工具。
jps
jps jps -l jps -lv
即java版的ps,可以檢視當前使用者啟動了哪些java程式。
jstat
pid指jps命令檢視的java程式號
jstat -gcutil pid 1000 10
jstat是一個多種用途的工具,更多需要man jstat或直接輸入jstat檢視提示。
jmap
jmap可以檢視記憶體狀況
jmap -histo:live pid
jmap -dump:file=dump.bin,format=b,live
jmap -dump:file=dump.bin,format=b
dump下來的記憶體檔案可以通過MAT進行分析,通過分析引用鏈等分析記憶體洩漏位置
jstack
檢視Java執行緒狀況
jstack pid
jstack -F pid
可以檢視執行緒的狀態、名稱、程式碼位置
javap (Java Printer)
javap 可以用可讀的方法檢視class檔案內容,在遇到線上class檔案問題,如NoSucheMethodError發生時,可以快速進行判斷分析。如分析一個A.class檔案,檢視它的私有方法和欄位。
javap -p -c -v A.class
視覺化工具
JVisualVM
$JAVA_HOME/bin/jvisualvm
JMC
$JAVA_HOME/bin/jmc
JConsole
$JAVA_HOME/bin/jconsole
Class檔案結構
Java編譯器將Java程式碼編譯成class檔案格式。
其中步驟包括了我們熟悉的詞法分析將原始檔轉換成token流。語法分析將token流轉換成抽象語法樹(AST)。語義分析分析語義是否正確。原始碼優化。目的碼生成和目的碼優化等步驟。最終得到了class檔案。之後在虛擬機器中,class檔案可以通過直譯器解釋執行和通過即時編譯器(JIT-just in time)編譯成native程式碼執行兩種方式執行。
class檔案是有嚴格定義的。符合定義的class檔案才能夠被JVM載入、驗證、初始化、執行。
我們通過javap可以檢視一個class檔案的內容。
Class檔案可以分為以下幾個部分
- Magic Number (0xCAFEBABY)
- minor version, major version 如 0×0033 代表 00,51, 是java8版本
- constant pool 常量池,常量池中包括了欄位、方法、類的名稱的符號引用,符號引用會在執行時經過連結轉換為直接引用。
- access flags 類的private、public等修飾詞
- this class 表明當前類的名稱
- super class 父類
- interfaces 實現的介面列表
- fields class中定義的欄位,每個field又是一個結構體
- methods 方法,包括MaxLocal, Max Stack,方法名,signature,access flags等。 程式碼儲存在方法的名稱為Code的屬性中。
- attributes
下面以一個簡單的類
public class Inc {
public static void main() {
}
private int count;
public void inc() {
count++;
}
}
看一下它的class檔案,通過vim開啟,在Normal模式下,按: 輸入%!xxd,即可轉換成16進製表示。然後可以通過%!xxd -r轉換回來
0000000: cafe babe 0000 0034 0013 0a00 0400 0f09 .......4........
0000010: 0003 0010 0700 1107 0012 0100 0563 6f75 .............cou
0000020: 6e74 0100 0149 0100 063c 696e 6974 3e01 nt...I...<init>.
0000030: 0003 2829 5601 0004 436f 6465 0100 0f4c ..()V...Code...L
0000040: 696e 654e 756d 6265 7254 6162 6c65 0100 ineNumberTable..
0000050: 046d 6169 6e01 0003 696e 6301 000a 536f .main...inc...So
0000060: 7572 6365 4669 6c65 0100 0849 6e63 2e6a urceFile...Inc.j
0000070: 6176 610c 0007 0008 0c00 0500 0601 0003 ava.............
0000080: 496e 6301 0010 6a61 7661 2f6c 616e 672f Inc...java/lang/
0000090: 4f62 6a65 6374 0021 0003 0004 0000 0001 Object.!........
00000a0: 0002 0005 0006 0000 0003 0001 0007 0008 ................
00000b0: 0001 0009 0000 001d 0001 0001 0000 0005 ................
00000c0: 2ab7 0001 b100 0000 0100 0a00 0000 0600 *...............
00000d0: 0100 0000 0100 0900 0b00 0800 0100 0900 ................
00000e0: 0000 1900 0000 0000 0000 01b1 0000 0001 ................
00000f0: 000a 0000 0006 0001 0000 0003 0001 000c ................
0000100: 0008 0001 0009 0000 0027 0003 0001 0000 .........'......
0000110: 000b 2a59 b400 0204 60b5 0002 b100 0000 ..*Y....`.......
0000120: 0100 0a00 0000 0a00 0200 0000 0700 0a00 ................
0000130: 0800 0100 0d00 0000 0200 0e0a ............
通過javap來看一下它的結構
javap -v -p -c -s -l Inc
Classfile /Users/liuzhengyang/study/java/Inc.class
Last modified Oct 6, 2016; size 315 bytes
MD5 checksum 770dcaa972162765744184ffc14bc3c6
Compiled from "Inc.java"
public class Inc
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // Inc.count:I
#3 = Class #17 // Inc
#4 = Class #18 // java/lang/Object
#5 = Utf8 count
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 inc
#13 = Utf8 SourceFile
#14 = Utf8 Inc.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // count:I
#17 = Utf8 Inc
#18 = Utf8 java/lang/Object
{
private int count;
descriptor: I
flags: ACC_PRIVATE
public Inc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 3: 0
public void inc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return
LineNumberTable:
line 7: 0
line 8: 10
}
SourceFile: "Inc.java"
位元組碼指令集
bytecode儲存在class檔案的方法的Code屬性中。用一個byte表示操作指令,所以最多有256個指令。一個指令可能會有多個運算元。
操作指令可以分為以下幾類:
- 數學運算,如iadd,i2c,imul,idiv
- 條件分支, 如ifeq,if_icompeq, if_icmplt
- 運算元棧和本地變數表的操作,如iload_0, iconst_0, ldc i bipush 100, astore_1,
iinc, dup, swap, dup_x1,put_field, get_field, get_static, put_static等。 - class操作,如new, checkcast, instanceof
- 方法呼叫:1.invokespecial:呼叫構造器、私有方法和父類方法;2.invokestatic:呼叫靜態方法;3.invokevirtual:呼叫虛方法,一般的例項方法都是invokevirtual呼叫;4.invokeinterface:呼叫介面類的方法;5.invokedynamic,java中對動態語言的支援。
invokevirtual和invokeinterface通過第一個引數查詢方法,動態分派,從而實現多型。
JMM (Java記憶體模型)
現代計算機的一本基本思想是分層模型,例如網路上的分層。在儲存上,為解決CPU和記憶體磁碟的速度有指數級差別的問題加入了很多快取,利用區域性性原理加快速度,從CPU暫存器到L1Cache、L2Cache、記憶體、磁碟,各個層的速度依次降低、空間增大、單位bit造價降低。最近CPU的處理能力的垂直增加似乎遇到瓶頸,轉而向多核方向發展,多個cpu核可能各自快取自己的內容,又出現了快取一致性問題。CPU有一些快取一致性協議,MESI等。CPU還可能會對機器指令進行亂序執行。JVM為了遮蔽底層的這些差異,提出了Java記憶體模型,即JMM(Java Memory Model),來保證Write One Run Anywhere。開發者面向JMM程式設計,通過JMM提供的一致性保證和工具,就能保證一致性問題。
JMM模型中,每個執行緒會有一個私有的記憶體區域用於快取讀和寫,各個執行緒共享一個主記憶體。
一個重要的概念是happen-before原則。
happen-before用來描述兩個操作的偏序關係,如果Ahappen-beforeB,那個A的操作的結果、產生的影響能夠被B看到。
最後總結
以上知識是通過閱讀書籍、官方文件、規範得來的,會有過時、不準確的情況。
還需通過檢視原始碼、自身探索,真像就在那程式碼中。每個知識點又能夠引出一篇筆記分析。
參考
- JLS
- JVMS
相關文章
- babel知識體系漫談Babel
- Babel知識體系淺談Babel
- 淺談如何搭建知識體系
- 一網打盡JVM垃圾回收知識體系JVM
- -----理論+實戰 構建完整JVM知識體系----新-----JVM
- Java-100天知識進階-JVM記憶體-知識鋪(三)JavaJVM記憶體
- [MongoDB知識體系] 一文全面總結MongoDB知識體系MongoDB
- [Redis知識體系] 一文全面總結Redis知識體系Redis
- JVM學習之JVM基礎知識JVM
- Android知識體系大全!Android
- JVM 面試知識整理JVM面試
- Python知識體系-Python2基礎知識Python
- 磁碟知識體系結構
- 構建自己知識體系
- web前端知識體系圖Web前端
- JVM面試知識點梳理JVM面試
- 【整理】JVM知識點大梳理JVM
- web開發知識體系中必要的知識點Web
- jvm、gc、作業系統等基礎知識總結JVMGC作業系統
- 架構知識體系總結架構
- 大資料的知識體系大資料
- 構建自己的知識體系
- Web前端知識體系精簡Web前端
- 如何將「知識」體系化管理
- 前端---梳理 http 知識體系 2前端HTTP
- 前端---梳理 http 知識體系 1前端HTTP
- 如何搭建自己的知識體系?
- JVM知識點掃盲系列(2)JVM
- JVM相關知識點總結JVM
- 016 | 漫談區塊鏈共識機制區塊鏈
- 談談面試知識點準備面試
- 如何構建自己的知識體系
- VUE知識體系、VUE面試題Vue面試題
- 大廠前端校招 - 知識體系前端
- 效能測試基礎知識體系
- Redis知識體系總結(2021版)Redis
- Java知識體系總結(2021版)Java
- 談談網路協議 – 基礎知識協議
- JVM相關知識整理和學習JVM