Java Virtual Machine 是Java平臺(tái)的基石,包含相應(yīng)的技術(shù)規(guī)范、實(shí)現(xiàn)(JVM的實(shí)現(xiàn)就是JRE)、運(yùn)行實(shí)例
為了實(shí)現(xiàn)一次編寫到處運(yùn)行 (Write Once Run Anywhere)的理念,JVM是和平臺(tái)相關(guān)的
Java HotSpot Client VM (client VM) :主要用于減少啟動(dòng)時(shí)間和內(nèi)存占用,啟動(dòng)應(yīng)用時(shí)使用-client指定
Java HotSpot Server VM (server VM):用于最大化的執(zhí)行速度,啟動(dòng)應(yīng)用時(shí)使用-server指定
JVM主要包括三個(gè)子系統(tǒng)
- 類加載子系統(tǒng) Class Loader
- 運(yùn)行時(shí)數(shù)據(jù)區(qū) Runtime Data Area
- 執(zhí)行引擎 Execution Engine

運(yùn)行時(shí)數(shù)據(jù)區(qū)
JVM定義了不同的運(yùn)行時(shí)數(shù)據(jù)區(qū),一些是在JVM啟動(dòng)時(shí)就創(chuàng)建的,有些數(shù)據(jù)區(qū)的生命周期和線程有關(guān),主要有5種
程序計(jì)數(shù)器 Progarm Counter Registers
JVM支持許多線程同時(shí)運(yùn)行,每個(gè)JVM線程都有自己的pc寄存器
當(dāng)線程運(yùn)行的是Java方法時(shí),存儲(chǔ)當(dāng)前正在執(zhí)行的JVM指令地址
如果是Native方法,未定義
Java虛擬機(jī)棧 Java Virtual Machine Stacks
每個(gè)線程都有一個(gè)私有的Java虛擬機(jī)棧,和線程的生命周期相同。描述了Java方法執(zhí)行的內(nèi)存模型
用來存儲(chǔ)棧幀(Frame)
Frame
每個(gè)方法在調(diào)用的時(shí)候,都會(huì)創(chuàng)建一個(gè)棧幀,包含局部變量(Local Variables)、操作數(shù)棧(Operand Stacks)、動(dòng)態(tài)鏈接(Dynamic Linking)、方法的調(diào)用者信息(Normal/Abrupt Method Invocation Completion)
局部變量
存儲(chǔ)boolen,byte,char,short,int,long,float,double,reference(引用),returnAddress(指向一條操作碼)
局部變量的數(shù)組長度是在編譯時(shí)確定的,之后不再改變
同樣用于方法調(diào)用中的參數(shù)傳遞,0號(hào)索引用于存儲(chǔ)this
操作數(shù)棧
深度由編譯時(shí)確定
加載常量,局部變量或者字段到操作數(shù)棧,出棧計(jì)算出結(jié)果,壓棧
還可以用來準(zhǔn)備傳遞到函數(shù)中的參數(shù),接收方法的返回值
和常見的語言(例如C)的棧類似:存儲(chǔ)局部變量,局部結(jié)果,用于方法調(diào)用和返回等
Exception
StackOverflowError:線程請(qǐng)求的棧深度大于JVM的的允許范圍(函數(shù)調(diào)用層級(jí)過多導(dǎo)致)
OutOfMemoryError:內(nèi)存不足,無法動(dòng)態(tài)擴(kuò)展內(nèi)存時(shí)
-Xss<size>:設(shè)定線程棧的大小 (默認(rèn)單位是byte,支持 K/k M/m G/g 作為單位)
本地方法棧 Native Method Stack
JVM使用到Native方法的時(shí)候使用本地方法棧
每一個(gè)線程,將創(chuàng)建一個(gè)單獨(dú)的本地方法棧。
和Java虛擬機(jī)棧的異常相同
堆Heap
被所有的JVM線程共享,所有的類對(duì)象和數(shù)組都要在堆上分配
堆是虛擬機(jī)所管理的內(nèi)存中最大的一塊,也是Garbage Collector管理的主要區(qū)域
Java堆的內(nèi)部可以不連續(xù),堆空間大小可以是固定的,也可以是可擴(kuò)展收縮的
Exception:
OutOfMemoryError: 請(qǐng)求更多的堆空間無法滿足
-Xms<size>:初始堆大小
-Xmx<size>:最大堆大小
方法區(qū) Method Area
被所有Java線程共享,存儲(chǔ)被虛擬機(jī)加載的類信息,例如運(yùn)行時(shí)常量池,字段和方法數(shù)據(jù),方法的代碼,構(gòu)造函數(shù)等,JVM規(guī)范沒有限定方法區(qū)的位置,可以是固定大小或者可擴(kuò)展,內(nèi)存不要求連續(xù)
Hotspot虛擬機(jī)中的方法區(qū)之前也叫做永久代,PermGen,Java8 后叫做元空間,Matespace,分配在本地內(nèi)存Native Memory中,它的大小可以自動(dòng)的增長,可以通過XX:MaxPermSize,-XX:MaxMetaspaceSize=<size>設(shè)定大小
因?yàn)槭窃诒镜貎?nèi)存(native memory)分配,所以其最大可利用空間是整個(gè)系統(tǒng)內(nèi)存的可用空間,不容易遇到OutOfMemoryError錯(cuò)誤

運(yùn)行時(shí)常量池 Runtime Constant Pool
是方法區(qū)的一部分,類似于常規(guī)語言的符號(hào)表
類或者接口的.class文件中,有一個(gè)常量表屬性(constant_pool table),存放各種字面常量(在編譯時(shí)確認(rèn))和字段引用(在運(yùn)行時(shí)確認(rèn)),加載到虛擬機(jī)時(shí),對(duì)應(yīng)加載到運(yùn)行時(shí)常量池
Exception:
OutOfMemoryError: 方法區(qū)無法滿足內(nèi)存分配需求
堆外內(nèi)存
Native Memory或者Off-Heap內(nèi)存空間,例如NIO中的DirectBuffer就是使用native函數(shù)直接分配的堆外內(nèi)存,可以通過-XX:MaxDirectMemorySize來設(shè)置NIO直接緩沖區(qū)的最大值。
使用堆外內(nèi)存的好處:
- 可以擴(kuò)展更大的內(nèi)存空間
- 能減少GC時(shí)間
- 可以在進(jìn)程間共享數(shù)據(jù)
Java堆中對(duì)象的分配、布局、訪問
對(duì)象的創(chuàng)建
通過new指令創(chuàng)建對(duì)象時(shí),虛擬機(jī)的執(zhí)行流程
- 類加載
- 為對(duì)象分配內(nèi)存(大小是確定的,在加載后便已經(jīng)確定),兩種分配策略
- 指針碰撞(Bump the Pointer)內(nèi)存是規(guī)整的,一邊是用過的,一邊是空閑的,中間一個(gè)指針
-
空閑列表(Free List),記錄哪些內(nèi)存可用,查找到一個(gè)足夠大的可用的內(nèi)存空間劃分給對(duì)象,并更新列表上的記錄
具體的策略取決于GC算法,具有整理功能(Compact)使用碰撞指針,否則使用空閑列表,默認(rèn)在新生代使用-XX:+UseTLAB
并發(fā)分配內(nèi)存時(shí),對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理(基于CAS,Compare and Swap),或者采用本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)策略,每個(gè)線程在Java堆中預(yù)先分配一塊內(nèi)存,在該線程的內(nèi)存塊內(nèi)進(jìn)一步分配,只有TLAB用完并分配新的TLAB時(shí)才需要同步鎖定
- 內(nèi)存空間初始化為零值
- 對(duì)象頭(Object Header)信息設(shè)置
- 執(zhí)行init方法,即構(gòu)造方法
對(duì)象的內(nèi)存布局
對(duì)象頭(Header)
HotSpot虛擬機(jī)的對(duì)象頭(Object Header)包括兩部分信息
- 存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù), 如哈希碼(identity_hashcode,對(duì)象的標(biāo)識(shí))、GC分代年齡、偏向鎖標(biāo)志、鎖狀態(tài)標(biāo)志、偏向線程ID、偏向時(shí)間戳、指向棧中鎖記錄的指針、指向互斥量(重量級(jí)鎖)的指針等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)中分別為32個(gè)和64個(gè)Bits,官方稱它為“Mark Word”,hotspot內(nèi)部對(duì)應(yīng)markOop.hpp,其中oop全拼是
ordinary object pointer,表示指向一個(gè)對(duì)象的指針,這里的mark不是一個(gè)指針,而是一個(gè)word - 類型指針(指向方法區(qū)的對(duì)象類型數(shù)據(jù)),用于判定該對(duì)象是哪個(gè)類的實(shí)例
如果是數(shù)組類型,還會(huì)有4bytes(32bits)用于記錄數(shù)組的長度
64位JVM,未進(jìn)行指針壓縮的對(duì)象頭結(jié)構(gòu):
|------------------------------------------------------------------------------------------------------------|--------------------|
| Object Header (128 bits) | State |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| Mark Word (64 bits) | Klass Word (64 bits) | |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | Lightweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | Heavyweight Locked |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
| | lock:2 | OOP to metadata object | Marked for GC |
|------------------------------------------------------------------------------|-----------------------------|--------------------|
32位
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
實(shí)例數(shù)據(jù)(Instance Data)
存儲(chǔ)父類子類的數(shù)據(jù),存儲(chǔ)順序與虛擬機(jī)字段分配策略和源碼中的定義順序有關(guān),相同寬度的字段會(huì)被分配到一起,HotSpot默認(rèn)策略是從長到短排列,引用排最后: long/double --> int/float --> short/char --> byte/boolean --> Reference
對(duì)齊填充(Padding)
占位符,默認(rèn)情況下HotSpot VM對(duì)象的起始地址是8字節(jié)的整數(shù)倍
查看內(nèi)存布局
可以使用openJDK中的JOL (Java Object Layout) 查看內(nèi)存布局
$ java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
$ java -jar jol-cli-0.9-full.jar internals java.lang.Object
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
Instantiated the sample instance via default constructor.
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到,mark work為前兩行,占8字節(jié),kclass pointer占4字節(jié)(這里為64位虛擬機(jī),使用了指針壓縮),對(duì)齊消耗4字節(jié)
指針壓縮
對(duì)應(yīng)的JVM選項(xiàng)是-XX:+UseCompressedOops,默認(rèn)開啟,使用壓縮指針時(shí),對(duì)象引用實(shí)際表示的是32位偏移而不是64位指針,可以提高性能(通常64位JVM消耗的內(nèi)存會(huì)比32位的大1.5倍)
壓縮oops表示指向64位Java堆基地址的32位對(duì)象偏移量。因?yàn)樗鼈兪菍?duì)象偏移而不是字節(jié)偏移,所以它們可以用于處理4G個(gè)對(duì)象(不是字節(jié)),HotSpot VM默認(rèn)是進(jìn)行8字節(jié)對(duì)齊,地址低3位始終為0,此時(shí)可以處理32GB大小的堆, 使用過程中,JVM將它們放大8倍并加上Java堆基址以查找它們引用的對(duì)象。
當(dāng)Java堆大小大于32G時(shí),也可以使用壓縮指針,通過-XX:ObjectAlignmentInBytes=alignment選項(xiàng)設(shè)定對(duì)齊字節(jié)數(shù)目,必須是2的冪次,范圍[8,256],默認(rèn)為8,可以管理的堆空間大小為:
4GB * ObjectAlignmentInBytes
開啟指針壓縮時(shí),堆中的以下oop會(huì)被壓縮:
- 每個(gè)對(duì)象的klass字段
- 每個(gè)oop實(shí)例字段
- oop數(shù)組的每個(gè)元素(objArray)
對(duì)象的訪問定位
通過棧上的reference數(shù)據(jù)指向堆上的具體數(shù)據(jù),有兩種實(shí)現(xiàn)方式:
- 直接指針訪問:reference中存儲(chǔ)的直接就是對(duì)象地址,訪問速度更快,HotSpot使用該方式,它需要虛擬機(jī)明確知道某個(gè)位置是什么類型的數(shù)據(jù),這樣才能在對(duì)象移動(dòng)后改變Reference類型的內(nèi)容,HotSpot的JIT編譯器會(huì)生成OopMap符號(hào)信息來記錄棧上和寄存器上的引用對(duì)象位置,用來在安全點(diǎn)進(jìn)行GC
- 句柄訪問:reference中存儲(chǔ)的是穩(wěn)定的句柄地址,對(duì)象被移動(dòng)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,reference本身不需要修改
鎖狀態(tài)
根據(jù)Java對(duì)象頭,鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài),輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí),但不能降級(jí)


偏向鎖
大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,所以當(dāng)一個(gè)線程訪問一個(gè)同步塊并獲取鎖時(shí),會(huì)通過CAS操作在對(duì)象頭里存儲(chǔ)偏向的線程ID,后續(xù)使用時(shí)只需要測(cè)試對(duì)象頭的threadID,當(dāng)其他線程試圖獲得偏向鎖CAS失敗時(shí),持有偏向鎖的線程會(huì)先暫停,恢復(fù)為無鎖狀態(tài),或者轉(zhuǎn)換為輕量級(jí)鎖
這里threadID 會(huì)覆蓋MarkWord中原有的identity hashcode,如果一個(gè)已經(jīng)偏向的對(duì)象調(diào)用object.identityHashCode()將會(huì)觸發(fā)偏向鎖的撤銷
epoch字段占2位,起到時(shí)間戳的作用
輕量級(jí)鎖
輕量級(jí)是相對(duì)使用操作系統(tǒng)互斥量來實(shí)現(xiàn)的重量級(jí)鎖而言的,在執(zhí)行同步代碼塊之前,JVM會(huì)先在當(dāng)前線程的棧幀中創(chuàng)建一個(gè)鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的MarkWord拷貝,該拷貝叫做Displaced Mark Word
然后使用CAS操作將對(duì)象頭中的MarkWord替換為指向鎖記錄的指針,如果失敗了,表示其他線程已經(jīng)獲得了鎖,當(dāng)前線程通過自旋等待,如果自旋失敗,鎖膨脹為重量級(jí)鎖,修改Mark Word,當(dāng)前線程阻塞,等待持有鎖的線程釋放鎖并喚醒該線程
重量級(jí)鎖
鎖的最終形態(tài),標(biāo)識(shí)位為10,其中指針指向的是monitor對(duì)象(也稱為管程或監(jiān)視器鎖)的起始地址。每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián),對(duì)象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如monitor可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成,當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。在JVM中,monitor是由ObjectMonitor實(shí)現(xiàn)的,其主要功能如下

優(yōu)化
- 自旋鎖:當(dāng)線程申請(qǐng)鎖時(shí),鎖被占用,則讓當(dāng)前線程執(zhí)行一個(gè)忙循環(huán)(自旋),看看持有鎖的線程是否會(huì)很快釋放鎖。如果自旋后還沒獲得鎖,才進(jìn)入同步阻塞狀態(tài);
- 自適應(yīng)自旋:時(shí)間根據(jù)之前的情況進(jìn)行調(diào)整
- 鎖消除:根據(jù)逃逸分析,消除鎖
- 鎖膨脹: 如果虛擬機(jī)探測(cè)到一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(膨脹)到整個(gè)操作序列的外部