誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

1:什麼是JVM

你們能夠想一想,JVM 是什麼?JVM是用來幹什麼的?在這裏我列出了三個概念,第一個是JVM,第二個是JDK,第三個是JRE。相信你們對這三個不會很陌生,相信大家都用過,可是,大家對這三個概念有清晰的知道麼?我不知道大家會不會,知不知道。接下來大家看看我對JVM的理解。
java

(1):JVM
程序員

JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是經過在實際的計算機上仿真模擬各類計算機功能來實現的。算法

引入Java語言虛擬機後,Java語言在不一樣平臺上運行時不須要從新編譯。編程

Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,小程序

使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),數組

就能夠在多種平臺上不加修改地運行。瀏覽器

Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。緩存

這就是Java的可以「一次編譯,處處運行」的緣由。性能優化

(2):JDK
bash

JDK(Java Development Kit) 是 Java 語言的軟件開發工具包(SDK)。

JDK包含的基本組件包括:

  1. javac – 編譯器,將源程序轉成字節碼

  2. jar – 打包工具,將相關的類文件打包成一個文件

  3. javadoc – 文檔生成器,從源碼註釋中提取文檔

  4. jdb – debugger,查錯工具

  5. java – 運行編譯後的java程序(.class後綴的)

  6. appletviewer:小程序瀏覽器,一種執行HTML文件上的Java小程序的Java瀏覽器。

  7. Javah:產生能夠調用Java過程的C過程,或創建能被Java程序調用的C過程的頭文件。

  8. Javap:Java反彙編器,顯示編譯類文件中的可訪問功能和數據,同時顯示字節代碼含義。

  9. Jconsole: Java進行系統調試和監控的工具

(3):JRE

JRE(Java Runtime Environment,Java運行環境),運行JAVA程序所必須的環境的集合,包含JVM標準實現及Java核心類庫。

包括兩部分:

Java Runtime Environment:

  • 是能夠在其上運行、測試和傳輸應用程序的Java平臺。

  • 它包括Java虛擬機(jvm)、Java核心類庫和支持文件。

  • 它不包含開發工具(JDK)--編譯器、調試器和其它工具。

  • JRE須要輔助軟件--Java Plug-in--以便在瀏覽器中運行applet。

Java Plug-in。

容許Java Applet和JavaBean組件在使用Sun的Java Runtime Environment(JRE)的瀏覽器中運行,

而不是在使用缺省的Java運行環境的瀏覽器中運行。

Java Plug-in可用於Netscape Navigator和Microsoft Internet Explorer。

2:JVM運行時數據區

Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域都有各自的用途,已經建立和銷燬時間,有的區域隨着虛擬機進程的啓動而建立,有些區域則依賴用戶線程的啓動和結束而建立和銷燬。根據《Java虛擬機規範(Java SE 7)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域,以下圖所示:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

2.一、程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令、分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。

因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的。在任何一個肯定的時刻,一個處理器都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各個線程之間計數器互不影響,獨立存儲。

若是線程正在執行的是一個Java方法,那這個計數器記錄的是正在執行的字節碼指令的地址;若是正在執行的是Native方法,這個計數器值則爲空(undefined)。

此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。

程序計數器是線程私有的,它的生命週期與線程相同(隨線程而生,隨線程而滅)。

2.二、Java虛擬機棧

虛擬機棧(Java Virtual Machine Stack)描述的是Java方法執行的內存模型:每一個方法被執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從被調用直至執行完成的過程就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

在Java虛擬機規範中,對這個區域規定了兩種異常狀況:

若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;

若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可以擴展),若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。

與程序寄存器同樣,java虛擬機棧也是線程私有的,它的生命週期與線程相同。

2.三、本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是相似,它們之間的區別在於虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由的實現它。

與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

與虛擬機棧同樣,本地方法棧也是線程私有的。

2.四、Java 堆(Java Heap)

對於大多數應用來講,Java 堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動的是建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數組都要在這裏分配內存。

Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱爲「GC堆」(Garbage Collected Heap)。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法,因此Java堆還能夠細分爲:新生代和老年代;新生代又能夠分爲:Eden 空間、From Survivor空間、To Survivor空間。

根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(經過-Xms和-Xmx控制)。若是在堆中沒有內存完成實例的分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。

2.五、方法區(Method Area)

方法區(Method Area)和Java堆同樣,是各個線程共享的內存區域,它用於存放已被虛擬機加載的類信息、常量、靜態變量、JIT編譯後的代碼等數據。方法區在虛擬機啓動的時候建立。

Java虛擬機規範對方法區的限制很是寬鬆,除了和堆同樣不須要不連續的內存空間和能夠固定大小或者可擴展外,還能夠選擇不實現垃圾收集。

根據Java虛擬機規範的規定,若是方法區的內存空間不能知足內存分配須要時,將拋出OutOfMemoryError異常。

2.六、運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。

2.七、直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。可是這部份內存也被頻繁使用,並且也可能致使OutOfMemoryError異常出現。

在JDK 1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方法,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。

到這裏咱們大體知道了Java虛擬機的運行時區的概況,接下來會繼續介紹更多JVM相關信息。

在此我向你們推薦一個交流學習羣:575745314 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

3:JVM內存模型

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

Java內存模型即Java Memory Model,簡稱JMM。JMM定義了Java 虛擬機(JVM)在計算機內存(RAM)中的工做方式。JVM是整個計算機虛擬模型,因此JMM是隸屬於JVM的。

若是咱們要想深刻了解Java併發編程,就要先理解好Java內存模型。Java內存模型定義了多線程之間共享變量的可見性以及如何在須要的時候對共享變量進行同步。原始的Java內存模型效率並非很理想,所以Java1.5版本對其進行了重構,如今的Java8仍沿用了Java1.5的版本。

關於併發編程

在併發編程領域,有兩個關鍵問題:線程之間的通訊和同步。

線程之間的通訊

線程的通訊是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種共享內存和消息傳遞。

在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊,典型的共享內存通訊方式就是經過共享對象進行通訊。

在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊,在java中典型的消息傳遞方式就是wait()和notify()。

關於Java線程之間的通訊,能夠參考線程之間的通訊(thread signal)。

線程之間的同步

同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。

在共享內存併發模型裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。

在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。

Java的併發採用的是共享內存模型

Java線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。若是編寫多線程程序的Java程序員不理解隱式進行的線程之間通訊的工做機制,極可能會遇到各類奇怪的內存可見性問題。

Java內存模型

上面講到了Java線程之間的通訊採用的是過共享內存模型,這裏提到的共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:

1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。複製代碼

下面經過示意圖來講明這兩個步驟:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1)臨時存放在本身的本地內存A中。當線程A和線程B須要通訊時,線程A首先會把本身本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變爲了1。

從總體來看,這兩個步驟實質上是線程A在向線程B發送消息,並且這個通訊過程必需要通過主內存。JMM經過控制主內存與每一個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。

上面也說到了,Java內存模型只是一個抽象概念,那麼它在Java中具體是怎麼工做的呢?爲了更好的理解上Java內存模型工做方式,下面就JVM對Java內存模型的實現、硬件內存模型及它們之間的橋接作詳細介紹。

JVM對Java內存模型的實現

在JVM內部,Java內存模型把內存分紅了兩部分:線程棧區和堆區,下圖展現了Java內存模型在JVM中的邏輯視圖:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

JVM中運行的每一個線程都擁有本身的線程棧,線程棧包含了當前線程執行的方法調用相關信息,咱們也把它稱做調用棧。隨着代碼的不斷執行,調用棧會不斷變化。

線程棧還包含了當前方法的全部本地變量信息。一個線程只能讀取本身的線程棧,也就是說,線程中的本地變量對其它線程是不可見的。即便兩個線程執行的是同一段代碼,它們也會各自在本身的線程棧中建立本地變量,所以,每一個線程中的本地變量都會有本身的版本。

全部原始類型(boolean,byte,short,char,int,long,float,double)的本地變量都直接保存在線程棧當中,對於它們的值各個線程之間都是獨立的。對於原始類型的本地變量,一個線程能夠傳遞一個副本給另外一個線程,當它們之間是沒法共享的。

堆區包含了Java應用建立的全部對象信息,無論對象是哪一個線程建立的,其中的對象包括原始類型的封裝類(如Byte、Integer、Long等等)。無論對象是屬於一個成員變量仍是方法中的本地變量,它都會被存儲在堆區。

下圖展現了調用棧和本地變量都存儲在棧區,對象都存儲在堆區:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

一個本地變量若是是原始類型,那麼它會被徹底存儲到棧區。

一個本地變量也有多是一個對象的引用,這種狀況下,這個本地引用會被存儲到棧中,可是對象自己仍然存儲在堆區。

對於一個對象的成員方法,這些方法中包含本地變量,仍須要存儲在棧區,即便它們所屬的對象在堆區。

對於一個對象的成員變量,無論它是原始類型仍是包裝類型,都會被存儲到堆區。

Static類型的變量以及類自己相關信息都會隨着類自己存儲在堆區。

堆中的對象能夠被多線程共享。若是一個線程得到一個對象的應用,它即可訪問這個對象的成員變量。若是兩個線程同時調用了同一個對象的同一個方法,那麼這兩個線程即可同時訪問這個對象的成員變量,可是對於本地變量,每一個線程都會拷貝一份到本身的線程棧中。

下圖展現了上面描述的過程:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

硬件內存架構

無論是什麼內存模型,最終仍是運行在計算機硬件上的,因此咱們有必要了解計算機硬件內存架構,下圖就簡單描述了當代計算機硬件內存架構:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

現代計算機通常都有2個以上CPU,並且每一個CPU還有可能包含多個核心。所以,若是咱們的應用是多線程的話,這些線程可能會在各個CPU核心中並行運行。

在CPU內部有一組CPU寄存器,也就是CPU的儲存器。CPU操做寄存器的速度要比操做計算機主存快的多。在主存和CPU寄存器之間還存在一個CPU緩存,CPU操做CPU緩存的速度快於主存但慢於CPU寄存器。某些CPU可能有多個緩存層(一級緩存和二級緩存)。計算機的主存也稱做RAM,全部的CPU都可以訪問主存,並且主存比上面提到的緩存和寄存器大不少。

當一個CPU須要訪問主存時,會先讀取一部分主存數據到CPU緩存,進而在讀取CPU緩存到寄存器。當CPU須要寫數據到主存時,一樣會先flush寄存器到CPU緩存,而後再在某些節點把緩存數據flush到主存。

Java內存模型和硬件架構之間的橋接

正如上面講到的,Java內存模型和硬件內存架構並不一致。硬件內存架構中並無區分棧和堆,從硬件上看,無論是棧仍是堆,大部分數據都會存到主存中,固然一部分棧和堆的數據也有可能會存到CPU寄存器中,以下圖所示,Java內存模型和計算機硬件內存架構是一個交叉關係:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

當對象和變量存儲到計算機的各個內存區域時,必然會面臨一些問題,其中最主要的兩個問題是:

1. 共享對象對各個線程的可見性2. 共享對象的競爭現象123複製代碼

共享對象的可見性

當多個線程同時操做同一個共享對象時,若是沒有合理的使用volatile和synchronization關鍵字,一個線程對共享對象的更新有可能致使其它線程不可見。

想象一下咱們的共享對象存儲在主存,一個CPU中的線程讀取主存數據到CPU緩存,而後對共享對象作了更改,但CPU緩存中的更改後的對象尚未flush到主存,此時線程對共享對象的更改對其它CPU中的線程是不可見的。最終就是每一個線程最終都會拷貝共享對象,並且拷貝的對象位於不一樣的CPU緩存中。

下圖展現了上面描述的過程。左邊CPU中運行的線程從主存中拷貝共享對象obj到它的CPU緩存,把對象obj的count變量改成2。但這個變動對運行在右邊CPU中的線程不可見,由於這個更改尚未flush到主存中:

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

要解決共享對象可見性這個問題,咱們可使用java volatile關鍵字。 Java’s volatile keyword. volatile 關鍵字能夠保證變量會直接從主存讀取,而對變量的更新也會直接寫到主存。volatile原理是基於CPU內存屏障指令實現的,後面會講到。

競爭現象

若是多個線程共享一個對象,若是它們同時修改這個共享對象,這就產生了競爭現象。

以下圖所示,線程A和線程B共享一個對象obj。假設線程A從主存讀取Obj.count變量到本身的CPU緩存,同時,線程B也讀取了Obj.count變量到它的CPU緩存,而且這兩個線程都對Obj.count作了加1操做。此時,Obj.count加1操做被執行了兩次,不過都在不一樣的CPU緩存中。

若是這兩個加1操做是串行執行的,那麼Obj.count變量便會在原始值上加2,最終主存中的Obj.count的值會是3。然而下圖中兩個加1操做是並行的,無論是線程A仍是線程B先flush計算結果到主存,最終主存中的Obj.count只會增長1次變成2,儘管一共有兩次加1操做。

誰說深刻淺出虛擬機難?如今我讓他通俗易懂(JVM)

要解決上面的問題咱們可使用java synchronized代碼塊。synchronized代碼塊能夠保證同一個時刻只能有一個線程進入代碼競爭區,synchronized代碼塊也能保證代碼塊中全部變量都將會從主存中讀,當線程退出代碼塊時,對全部變量的更新將會flush到主存,無論這些變量是否是volatile類型的。

volatile和 synchronized區別

  1. volatile本質是在告訴jvm當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取; synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住。

  2. volatile僅能使用在變量級別;synchronized則可使用在變量、方法、和類級別的

  3. volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則能夠保證變量的修改可見性和原子性

  4. volatile不會形成線程的阻塞;synchronized可能會形成線程的阻塞。

  5. volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化

支撐Java內存模型的基礎原理

指令重排序

在執行程序時,爲了提升性能,編譯器和處理器會對指令作重排序。可是,JMM確保在不一樣的編譯器和不一樣的處理器平臺之上,經過插入特定類型的Memory Barrier來禁止特定類型的編譯器重排序和處理器重排序,爲上層提供一致的內存可見性保證。

  1. 編譯器優化重排序:編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。

  2. 指令級並行的重排序:若是不存l在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。

  3. 內存系統的重排序:處理器使用緩存和讀寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

數據依賴性

若是兩個操做訪問同一個變量,其中一個爲寫操做,此時這兩個操做之間存在數據依賴性。

編譯器和處理器不會改變存在數據依賴性關係的兩個操做的執行順序,即不會重排序。

as-if-serial

無論怎麼重排序,單線程下的執行結果不能被改變,編譯器、runtime和處理器都必須遵照as-if-serial語義。

內存屏障(Memory Barrier )

上面講到了,經過內存屏障能夠禁止特定類型處理器的重排序,從而讓程序按咱們預想的流程去執行。內存屏障,又稱內存柵欄,是一個CPU指令,基本上它是一條這樣的指令:

  1. 保證特定操做的執行順序。

  2. 影響某些數據(或則是某條指令的執行結果)的內存可見性。

編譯器和CPU可以重排序指令,保證最終相同的結果,嘗試優化性能。插入一條Memory Barrier會告訴編譯器和CPU:無論什麼指令都不能和這條Memory Barrier指令重排序。

Memory Barrier所作的另一件事是強制刷出各類CPU cache,如一個Write-Barrier(寫入屏障)將刷出全部在Barrier以前寫入 cache 的數據,所以,任何CPU上的線程都能讀取到這些數據的最新版本。

這和java有什麼關係?上面java內存模型中講到的volatile是基於Memory Barrier實現的。

若是一個變量是volatile修飾的,JMM會在寫入這個字段以後插進一個Write-Barrier指令,並在讀這個字段以前插入一個Read-Barrier指令。這意味着,若是寫入一個volatile變量,就能夠保證:

  1. 一個線程寫入變量a後,任何線程訪問該變量都會拿到最新值。

  2. 在寫入變量a以前的寫入操做,其更新的數據對於其餘線程也是可見的。由於Memory Barrier會刷出cache中的全部先前的寫入。

happens-before

從jdk5開始,java使用新的JSR-133內存模型,基於happens-before的概念來闡述操做之間的內存可見性。

在JMM中,若是一個操做的執行結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在happens-before關係,這個的兩個操做既能夠在同一個線程,也能夠在不一樣的兩個線程中。

與程序員密切相關的happens-before規則以下:

  1. 程序順序規則:一個線程中的每一個操做,happens-before於該線程中任意的後續操做。

  2. 監視器鎖規則:對一個鎖的解鎖操做,happens-before於隨後對這個鎖的加鎖操做。

  3. volatile域規則:對一個volatile域的寫操做,happens-before於任意線程後續對這個volatile域的讀。

  4. 傳遞性規則:若是 A happens-before B,且 B happens-before C,那麼A happens-before C。

注意:兩個操做之間具備happens-before關係,並不意味前一個操做必需要在後一個操做以前執行!僅僅要求前一個操做的執行結果,對於後一個操做是可見的,且前一個操做按順序排在後一個操做以前。

在此我向你們推薦一個交流學習羣:575745314 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。

相關文章
相關標籤/搜索