輕鬆學JVM

輕鬆學JVM(一)——基本原理

 

前言html

  JVM一直是java知識裏面進階階段的重要部分,若是但願在java領域研究的更深刻,則JVM則是如論如何也避開不了的話題,本系列試圖經過簡潔易讀的方式,講解JVM必要的知識點。java

運行流程數組

  咱們都知道java一直宣傳的口號是:一次編譯,處處運行。那麼它如何實現的呢?咱們看下圖:多線程

image

  java程序通過一次編譯以後,將java代碼編譯爲字節碼也就是class文件,而後在不一樣的操做系統上依靠不一樣的java虛擬機進行解釋,最後再轉換爲不一樣平臺的機器碼,最終獲得執行。這樣咱們是否是能夠推演,若是要在mac系統上運行,是否是隻須要安裝mac java虛擬機就好了。那麼瞭解了這個基本原理後,咱們嘗試去作更深的研究,一個普通的java程序它的執行流程究竟是怎樣的呢?例如咱們寫了一段這樣的代碼:併發

public class HelloWorld {
	public static void main(String[] args) {
		System.out.print("Hello world");
	}
}

這段程序從編譯到運行,最終打印出「Hello world」中間通過了哪些步驟呢?咱們直接上圖:jvm

image

  java代碼經過編譯以後生成字節碼文件(class文件),經過:java HelloWorld執行,此時java根據系統版本找到jvm.cfg,各位能夠搜索一下本身電腦上的jvm.cfg文件在哪,它會根據你的系統版本放在不一樣的位置,好比個人這個文件就在:C:\Program Files\Java\jdk1.8.0_101\jre\lib\amd64\jvm.cfg,打開看一下:post

S{6G7J8WOT3$)72FL1DW5U7

  這是我電腦上的文件,其中-server KNOWN就表示名稱爲server的jvm可用。若是這時你搜索一下你電腦上jvm.dll,你就會發現它必定在你的某個server目錄下,好比個人:C:\Program Files\Java\jdk1.8.0_101\jre\bin\server\jvm.dll。簡而言之就是經過jvm.cfg文件找到對應的jvm.dll,jvm.dll則是java虛擬機的主要實現。接下來會初始化JVM,而且獲取JNI接口,什麼是JNI接口,就是java本地接口,你想啊java被編譯成了class文件,JVM怎麼從硬盤上找到這個文件並裝載到JVM裏呢,就是經過JNI接口(它還經常使用於java與操做系統、硬件交互),找到class文件後並裝載進JVM,而後找到main方法,最後執行。學習

JVM基本結構url

  可能經過上面的描述,你們對JVM運行流程有了一個粗略的認識,那麼JVM內部究竟是怎麼執行一個class文件的呢,也就是上圖中最後一步第6步的內部細節是怎樣的呢?要了解這個問題,咱們首先得看一下JVM的內部結構:spa

image

  從這個結構不難看出,class文件被jvm裝載之後,通過jvm的內存空間調配,最終是由執行引擎完成class文件的執行。固然這個過程還有其餘角色模塊的協助,這些模塊協同配合才能讓一個java程序成功的運行,下面就詳細介紹這些模板,它們也是後面學習jvm最重要的部分。

內存空間:

JVM內存空間包含:方法區、java堆、java棧、本地方法棧。

方法區是各個線程共享的區域,存放類信息、常量、靜態變量。

java堆也是線程共享的區域,咱們的類的實例就放在這個區域,能夠想象你的一個系統會產生不少實例,所以java堆的空間也是最大的。若是java堆空間不足了,程序會拋出OutOfMemoryError異常。

java棧是每一個線程私有的區域,它的生命週期與線程相同,一個線程對應一個java棧,每執行一個方法就會往棧中壓入一個元素,這個元素叫「棧幀」,而棧幀中包括了方法中的局部變量、用於存放中間狀態值的操做棧,這裏面有不少細節,咱們之後再講。若是java棧空間不足了,程序會拋出StackOverflowError異常,想想什麼狀況下會容易產生這個錯誤,對,遞歸,遞歸若是深度很深,就會執行大量的方法,方法越多java棧的佔用空間越大。

本地方法棧角色和java棧相似,只不過它是用來表示執行本地方法的,本地方法棧存放的方法調用本地方法接口,最終調用本地方法庫,實現與操做系統、硬件交互的目的。

PC寄存器,說到這裏咱們的類已經加載了,實例對象、方法、靜態變量都去了本身改去的地方,那麼問題來了,程序該怎麼執行,哪一個方法先執行,哪一個方法後執行,這些指令執行的順序就是PC寄存器在管,它的做用就是控制程序指令的執行順序。

執行引擎固然就是根據PC寄存器調配的指令順序,依次執行程序指令。

結語

  本文主要介紹了java虛擬機運行的基本流程,以及java虛擬機內部結構。下一篇咱們將學習java內存模型以及探索java變量的可見性、有序性、指令重排等問題.

 

 

 

 

輕鬆學JVM(二)——內存模型、可見性、指令重排序

 

    上一篇咱們介紹了JVM的基本運行流程以及內存結構,對JVM有了初步的認識,這篇文章咱們將根據JVM的內存模型探索java當中變量的可見性以及不一樣的java指令在併發時可能發生的指令重排序的狀況。

內存模型

    首先咱們思考一下一個java線程要向另一個線程進行通訊,應該怎麼作,咱們再把需求明確一點,一個java線程對一個變量的更新怎麼通知到另一個線程呢?咱們知道java當中的實例對象、數組元素都放在java堆中,java堆是線程共享的。(咱們這裏把java堆稱爲主內存),而每個線程都是本身私有的內存空間(稱爲工做內存),若是線程1要向線程2通訊,必定會通過相似的流程:

clip_image002

一、 線程1將本身工做內存中的X更新爲1並刷新到主內存中;

二、 線程2從主內存讀取變量X=1,更新到本身的工做內存中,從而線程2讀取的X就是線程1更新後的值。

從上面的流程看出線程之間的通訊都須要通過主內存,而主內存與工做內存的交互,則須要Java內存模型(JMM)來管理器。下圖演示了JMM如何管理主內存和工做內存:

clip_image004

當線程1須要將一個更新後的變量值刷新到主內存中時,須要通過兩個步驟:

一、 工做內存執行store操做;

二、 主內存執行write操做;

完成這兩步便可將工做內存中的變量值刷新到主內存,即線程1工做內存和主內存的變量值保持一致;

當線程2須要從主內存中讀取變量的最新值時,一樣須要通過兩個步驟:

一、主內存執行read操做,將變量值從主內存中讀取出來;

二、工做內存執行load操做,將讀取出來的變量值更新到本地內存的副本;

完成這兩步,線程2的變量和主內存的變量值就保持一致了。

可見性

    Java中有一個關鍵字volatile,它有什麼用呢?這個答案其實就在上述java線程間通訊機制中,咱們想象一下,因爲工做內存這個中間層的出現,線程1和線程2必然存在延遲的問題,例如線程1在工做內存中更新了變量,但還沒刷新到主內存,而此時線程2獲取到的變量值就是未更新的變量值,又或者線程1成功將變量更新到主內存,但線程2依然使用本身工做內存中的變量值,一樣會出問題。無論出現哪一種狀況均可能致使線程間的通訊不能達到預期的目的。例如如下例子:

//線程1 boolean stop = false; while(!stop){ doSomething(); } //線程2

stop 
= true;

這個經典的例子表示線程2經過修改stop的值,控制線程1中斷,但在真實環境中可能會出現意想不到的結果,線程2在執行以後,線程1並無馬上中斷甚至一直不會中斷。出現這種現象的緣由就是線程2對線程1的變量更新沒法第一時間獲取到。

但這一切等到Volatile出現後,不再是問題,Volatile保證兩件事:

一、 線程1工做內存中的變量更新會強制當即寫入到主內存;

二、 線程2工做內存中的變量會強制當即失效,這使得線程2必須去主內存中獲取最新的變量值。

因此這就理解了Volatile保證了變量的可見性,由於線程1對變量的修改能第一時間讓線程2可見。

指令重排序

關於指令排序咱們先看一段代碼:

int a = 0;
boolean flag = false;

//線程1

public void writer() {

a = 1;

flag = true;

}

//線程2

public void reader() {

if (flag) {

int i= a+1;

...... }

}

線程1依次執行a=1,flag=true;線程2判斷到flag==true後,設置i=a+1,根據代碼語義,咱們可能會推斷此時i的值等於2,由於線程2在判斷flag==true時,線程1已經執行了a=1;因此i的值等於a+1=1+1=2;但真實狀況卻不必定如此,引發這個問題的緣由是線程1內部的兩條語句a=1;flag=true;可能被從新排序執行,如圖:

clip_image006

這就是指令重排序的簡單演示,兩個賦值語句儘管他們的代碼順序是一前一後,但真正執行時卻不必定按照代碼順序執行。你可能會說,有這個指令重排序那不是亂套了嗎?我寫的程序都不按個人代碼流程走,這怎麼玩?這個你能夠放心,你的程序不會亂套,由於java和CPU、內存之間都有一套嚴格的指令重排序規則,哪些能夠重排,哪些不能重排都有規矩的。下列流程演示了一個java程序從編譯到執行會經歷哪些重排序:

clip_image008

在這個流程中第一步屬於編譯器重排查,編譯器重排序會按JMM的規範嚴格進行,換言之編譯器重排序通常不會對程序的正確邏輯形成影響。第2、三步屬於處理器重排序,處理器重排序JMM就很差管了,怎麼辦呢?它會要求java編譯器在生成指令時加入內存屏障,內存屏障是什麼?你能夠理解爲一個不透風的保護罩,把不能重排序的java指令保護起來,那麼處理器在遇到內存屏障保護的指令時就不會對它進行重排序了。關於在哪些地方該加入內存屏障,內存屏障有哪些種類,各有什麼做用,這些知識點這裏就再也不闡述了。能夠參考JVM規範相關資料。

下面介紹一下在同一個線程中,不會被重排序的邏輯:

clip_image010

這三種狀況中,任意改變一個代碼的順序,結果都會大不相同,對於這樣的邏輯代碼,是不會被重排序的。注意這是指單線程中不會被重排序,若是在多線程環境下,仍是會產生邏輯問題,例如咱們一開始舉的例子。

結語

本文簡單介紹了java在實現線程間通訊時的簡單原理,並介紹了volatile關鍵字的做用,最後介紹了java當中可能會出現指令重排序的狀況。下一篇將介紹JVM中的參數設置對java程序的影響。

 

參考資料:

《實戰Java虛擬機》 葛一鳴

《深刻理解Java虛擬機(第2版)》 周志明

《深刻理解Java內存模型》 程曉明

相關文章
相關標籤/搜索