JVM啟動流程
JVM基本結(jié)構(gòu)
1. PC寄存器 or 程序計數(shù)器(Program Counter Register)
- 是一塊較小的內(nèi)存空間,指向下一條指令的地址:分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基本功能都需要依賴它來完成
- 執(zhí)行本地方法時,PC的值為空(undefined)
- 每個線程擁有一個PC寄存器:在任何一個確定的時刻,一個處理器(內(nèi)核)都只會執(zhí)行一條線程中的指令
- 在線程創(chuàng)建時創(chuàng)建
2. Java虛擬機棧(Java Virtual Machine Stacks)
- 線程私有的(和PC寄存器一樣)
- 每個Java方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame:方法運行時的基本數(shù)據(jù)結(jié)構(gòu)):存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法返回地址等信息
- 每一個方法從調(diào)用直至執(zhí)行完成:對應(yīng)著一個棧幀在虛擬機中入棧到出棧的過程
- 局部變量表(Local Variable Table):存放了編譯期可知的各種基本數(shù)據(jù)類型、對象引用和returnAddress類型(指向了一條字節(jié)碼指令的地址),即,方法參數(shù)和方法內(nèi)部定義的局部變量
- 64位長度的long和double數(shù)據(jù)會占用2個局部變量空間(slot),其余的數(shù)據(jù)類型只占用1個
- 局部變量表所需的內(nèi)存空間在編譯期間完成分配:一個方法需要在幀中分配多大的局部變量空間是完全確定的,運行期間不會改變局部變量表的大小
- 實例方法(非static的方法)的局部變量表中第0位索引的Slot默認(rèn)是對此方法所屬對象實例的引用,即,"this",余下的Slot則與static方法相同
- 操作數(shù)棧(Operand Stack):Java沒有寄存器,所有參數(shù)傳遞使用操作數(shù)棧
- 后入先出
- 32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2
- 棧上分配
- 小對象(一般幾十個bytes),在沒有逃逸的情況下,可以直接分配在棧上
- 直接分配在棧上,可以自動回收,減輕GC壓力
- 大對象或者逃逸對象無法棧上分配
3. Java堆(Java Heap)
- 被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建:唯一目的就是存放對象實例
- 是GC管理的主要區(qū)域
- GC基本都采用分代收集算法:Java堆可以細(xì)分為新生代和老年代;再細(xì)致一點的有Eden空間、From Survivor空間、To Survivor空間等
- 可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可:主流的虛擬機都是按照大小可擴展來實現(xiàn)的
4. 方法區(qū)(Method Area)
- 各個線程共享的內(nèi)存區(qū)域(與Java堆一樣):存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)
- 在HotSpot虛擬機上,很多人把方法區(qū)稱為“永久代”:兩者并不等價,只是因為使用了永久代來實現(xiàn)方法區(qū)--這樣更容易遇到內(nèi)存溢出問題,放棄中,JDK 1.7已經(jīng)把原本放在永久代的字符串常量池移出到堆中
5. 棧、堆、方法區(qū)交互
public class AppMain
//運行時, jvm把appmain的信息都放入方法區(qū)
{
public static void main(String[] args)
//main 方法本身放入方法區(qū)。
{
Sample test1 = new Sample("測試1");
//test1是引用,所以放到棧區(qū)里,Sample是自定義對象應(yīng)該放到堆里面
Sample test2 = new Sample("測試2");
test1.printName();
test2.printName();
}
public class Sample
//運行時, jvm把Sample的信息都放入方法區(qū)
{
private name;
//new Sample實例后,name引用放入棧區(qū)里,name 對象放入堆里
public Sample(String name)
{
this.name =name;
}
//print方法本身放入方法區(qū)里。
public void printName()
{
System.out.println(name);
}
}
}
內(nèi)存模型
Java內(nèi)存模型的主要目標(biāo):定義程序中各個變量的訪問規(guī)則,即,在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)
- 主內(nèi)存(Main Memory):存儲所有的變量
- 每條線程還有自己的工作內(nèi)存(Working Memory):保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線程對變量的所有操作<讀取、賦值等>都必須在工作內(nèi)存中進(jìn)行
- 數(shù)據(jù)從主內(nèi)存復(fù)制到工作內(nèi)存:必須按順序執(zhí)行read和load
- 由主內(nèi)存執(zhí)行的讀(read)操作;
- 由工作內(nèi)存執(zhí)行的相應(yīng)的加載(load)操作
- 數(shù)據(jù)從工作內(nèi)存拷貝到主內(nèi)存
- 由工作內(nèi)存執(zhí)行的存儲(store)操作;
- 由主內(nèi)存執(zhí)行的相應(yīng)的寫(write)操作
- 每種操作都是原子的、不可再分的:lock unlock read load use assign store write
- 執(zhí)行上述8種基本操作時必須滿足如下規(guī)則:
- 不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受;或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受
- 變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存
- 沒有發(fā)生過assign操作時不允許把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存
- 對一個變量實施use、store操作之前,必須先執(zhí)行過了assign和load操作
- 一個變量在同一個時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行湘通次數(shù)的unlock操作,變量才會被解鎖
- 如果對一個變量執(zhí)行l(wèi)ock操作,那將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
- 如果一個變量事先沒有被lock鎖定,那就不允許對它執(zhí)行unlock,也不允許去unlock一個被其它線程鎖定的變量
- 對一個變量執(zhí)行unlock之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)
- volatile關(guān)鍵字:Java虛擬機提供的最輕量級的同步機制
- 非正式但通俗易懂的作用介紹:當(dāng)一個變量定義為volatile后,它將具備兩種特性
-
保證此變量對所有線程的可見性(當(dāng)一條線程修改了這個變量的值,新值對于其它線程來說是可以立即得知的):volatile變量在各個線程的工作內(nèi)存中不存在一致性問題,但是Java里面的運算并非原子操作,導(dǎo)致volatile變量的運算在并發(fā)下一樣是不安全的,只有在以下運算場景中才適合使用volatile變量,否則仍然要通過鎖(synchronized或java.util.concurrent中的原子類)來保證原子性
- 運算結(jié)果并不依賴變量的當(dāng)前值
- 變量不需要與其他的狀態(tài)變量共同參與不變約束
- 禁止指令重排
-
保證此變量對所有線程的可見性(當(dāng)一條線程修改了這個變量的值,新值對于其它線程來說是可以立即得知的):volatile變量在各個線程的工作內(nèi)存中不存在一致性問題,但是Java里面的運算并非原子操作,導(dǎo)致volatile變量的運算在并發(fā)下一樣是不安全的,只有在以下運算場景中才適合使用volatile變量,否則仍然要通過鎖(synchronized或java.util.concurrent中的原子類)來保證原子性
- 選用volatile的意義--它能讓我們的代碼比使用其他的同步工具更快嗎?
- 大多數(shù)場景下volatile的總開銷比鎖要低
- 在volatile與鎖之中選擇的唯一依據(jù)是volatile的語義能否滿足使用場景的要求
- 非正式但通俗易懂的作用介紹:當(dāng)一個變量定義為volatile后,它將具備兩種特性
- 原子性、可見性與有序性:Java內(nèi)存模型是圍繞著在并發(fā)過程中如何處理原子性、可見性和有序性這3個特征來建立的
-
原子性(Atomicity)
- 基本數(shù)據(jù)類型的訪問讀寫是具備原子性的
- 在synchronized塊之間的操作也具備原子性
-
可見性(Visibility):當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改
- volatile
- synchronized:對一個變量執(zhí)行unlock前,必須先把此變量同步回主內(nèi)存中
- final:一旦初始化完成,其他線程就可見
-
有序性(Ordering):在本線程內(nèi),操作都是有序的;在線程外觀察,操作都是無序的:“指令重排”或“工作內(nèi)存與主內(nèi)存同步延遲”)
- volatile
- synchronized:一個變量在同一個時刻只允許一條線程對其進(jìn)行l(wèi)ock--持有同一個鎖的兩個同步塊只能串行的進(jìn)入
-
原子性(Atomicity)
- 先行發(fā)生原則(happens-before):判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù)
- 程序順序規(guī)則(Program Order Rule):一個線程內(nèi)保證語義的串行性
- 鎖規(guī)則(Monitor Lock Rule):解鎖(unlock)必然發(fā)生在隨后的對同一個鎖的加鎖(lock)前
- volatile規(guī)則(Volatile Variable Rule):對一個volatile變量的寫,先發(fā)生于讀
- 線程啟動規(guī)則(Thread Start Rule):線程的start方法先于它的每一個動作
- 線程終止規(guī)則(Thread Termination Rule):線程的所有操作先于線程的終結(jié)(Thread.join())
- 線程中斷規(guī)則(Thread Interruption Rule):線程的中斷(interrupt())先于被中斷線程的代碼檢測到中斷事件的發(fā)生
- 對象終結(jié)規(guī)則(Finalizer Rule):對象的構(gòu)造函數(shù)執(zhí)行結(jié)束先于finalize()方法
- 傳遞性(Transitivity):A先于B,B先于C 那么A必然先于C
- 結(jié)論:一個操作“時間上的先發(fā)生”不代表這個操作會是“先行發(fā)生”;一個操作“先行發(fā)生”不能推導(dǎo)出這個操作必定是“時間上的先發(fā)生”
編譯和解釋運行的概念
- 解釋運行
- 解釋執(zhí)行以解釋方式運行字節(jié)碼
- 解釋執(zhí)行的意思是:讀一句執(zhí)行一句
- 編譯運行(JIT)
- 將字節(jié)碼編譯成機器碼
- 直接執(zhí)行機器碼
- 運行時編譯
- 編譯后性能有數(shù)量級的提升
- 解釋器與編譯器兩者各有優(yōu)勢:
- 當(dāng)程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行
- 當(dāng)程序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲取更高的執(zhí)行效率