偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

誰(shuí)說(shuō)深入淺出虛擬機(jī)難?現(xiàn)在我讓他通俗易懂(JVM)

云計(jì)算 虛擬化
JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫(xiě),JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),是通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的。

 [[328100]]

 1:什么是JVM

大家可以想想,JVM 是什么?JVM是用來(lái)干什么的?在這里我列出了三個(gè)概念,第一個(gè)是JVM,第二個(gè)是JDK,第三個(gè)是JRE。相信大家對(duì)這三個(gè)不會(huì)很陌生,相信你們都用過(guò),但是,你們對(duì)這三個(gè)概念有清晰的知道么?我不知道你們會(huì)不會(huì),知不知道。接下來(lái)你們看看我對(duì)JVM的理解。

(1):JVM

JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫(xiě),JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),是通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的。

引入Java語(yǔ)言虛擬機(jī)后,Java語(yǔ)言在不同平臺(tái)上運(yùn)行時(shí)不需要重新編譯。

Java語(yǔ)言使用Java虛擬機(jī)屏蔽了與具體平臺(tái)相關(guān)的信息,

使得Java語(yǔ)言編譯程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼),

就可以在多種平臺(tái)上不加修改地運(yùn)行。

Java虛擬機(jī)在執(zhí)行字節(jié)碼時(shí),把字節(jié)碼解釋成具體平臺(tái)上的機(jī)器指令執(zhí)行。

這就是Java的能夠“一次編譯,到處運(yùn)行”的原因。

(2):JDK

JDK(Java Development Kit) 是 Java 語(yǔ)言的軟件開(kāi)發(fā)工具包(SDK)。

JDK包含的基本組件包括:

  1. javac – 編譯器,將源程序轉(zhuǎn)成字節(jié)碼
  2. jar – 打包工具,將相關(guān)的類(lèi)文件打包成一個(gè)文件
  3. javadoc – 文檔生成器,從源碼注釋中提取文檔
  4. jdb – debugger,查錯(cuò)工具
  5. java – 運(yùn)行編譯后的java程序(.class后綴的)
  6. appletviewer:小程序?yàn)g覽器,一種執(zhí)行HTML文件上的Java小程序的Java瀏覽器。
  7. Javah:產(chǎn)生可以調(diào)用Java過(guò)程的C過(guò)程,或建立能被Java程序調(diào)用的C過(guò)程的頭文件。
  8. Javap:Java反匯編器,顯示編譯類(lèi)文件中的可訪問(wèn)功能和數(shù)據(jù),同時(shí)顯示字節(jié)代碼含義。
  9. Jconsole: Java進(jìn)行系統(tǒng)調(diào)試和監(jiān)控的工具

(3):JRE

JRE(Java Runtime Environment,Java運(yùn)行環(huán)境),運(yùn)行JAVA程序所必須的環(huán)境的集合,包含JVM標(biāo)準(zhǔn)實(shí)現(xiàn)及Java核心類(lèi)庫(kù)。

包括兩部分:

Java Runtime Environment:

  • 是可以在其上運(yùn)行、測(cè)試和傳輸應(yīng)用程序的Java平臺(tái)。
  • 它包括Java虛擬機(jī)(jvm)、Java核心類(lèi)庫(kù)和支持文件。
  • 它不包含開(kāi)發(fā)工具(JDK)--編譯器、調(diào)試器和其它工具。
  • JRE需要輔助軟件--Java Plug-in--以便在瀏覽器中運(yùn)行applet。

Java Plug-in。

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

而不是在使用缺省的Java運(yùn)行環(huán)境的瀏覽器中運(yùn)行。

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

2:JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)

Java虛擬機(jī)在執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途,已經(jīng)創(chuàng)建和銷(xiāo)毀時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而創(chuàng)建,有些區(qū)域則依賴(lài)用戶(hù)線程的啟動(dòng)和結(jié)束而創(chuàng)建和銷(xiāo)毀。根據(jù)《Java虛擬機(jī)規(guī)范(Java SE 7)》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域,如下圖所示:

 

2.1、程序計(jì)數(shù)器

程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在虛擬機(jī)的概念模型里(僅是概念模型,各種虛擬機(jī)可能會(huì)通過(guò)一些更高效的方式去實(shí)現(xiàn)),字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令、分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴(lài)這個(gè)計(jì)數(shù)器來(lái)完成。

由于Java虛擬機(jī)的多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的。在任何一個(gè)確定的時(shí)刻,一個(gè)處理器都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各個(gè)線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。

如果線程正在執(zhí)行的是一個(gè)Java方法,那這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,這個(gè)計(jì)數(shù)器值則為空(undefined)。

此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。

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

2.2、Java虛擬機(jī)棧

虛擬機(jī)棧(Java Virtual Machine Stack)描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從被調(diào)用直至執(zhí)行完成的過(guò)程就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。

在Java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常情況:

如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;

如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可以擴(kuò)展),如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常。

與程序寄存器一樣,java虛擬機(jī)棧也是線程私有的,它的生命周期與線程相同。

2.3、本地方法棧

本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常類(lèi)似,它們之間的區(qū)別在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù),而本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)。在虛擬機(jī)規(guī)范中對(duì)本地方法棧中方法使用的語(yǔ)言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒(méi)有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由的實(shí)現(xiàn)它。

與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemoryError異常。

與虛擬機(jī)棧一樣,本地方法棧也是線程私有的。

2.4、Java 堆(Java Heap)

對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),Java 堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)的是創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例以及數(shù)組都要在這里分配內(nèi)存。

Java堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱(chēng)為“GC堆”(Garbage Collected Heap)。從內(nèi)存回收的角度來(lái)看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆還可以細(xì)分為:新生代和老年代;新生代又可以分為:Eden 空間、From Survivor空間、To Survivor空間。

根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,就像我們的磁盤(pán)空間一樣。在實(shí)現(xiàn)時(shí),既可以實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過(guò)當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來(lái)實(shí)現(xiàn)的(通過(guò)-Xms和-Xmx控制)。如果在堆中沒(méi)有內(nèi)存完成實(shí)例的分配,并且堆也無(wú)法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError異常。

2.5、方法區(qū)(Method Area)

方法區(qū)(Method Area)和Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存放已被虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)變量、JIT編譯后的代碼等數(shù)據(jù)。方法區(qū)在虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建。

Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的限制非常寬松,除了和堆一樣不需要不連續(xù)的內(nèi)存空間和可以固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾收集。

根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,如果方法區(qū)的內(nèi)存空間不能滿(mǎn)足內(nèi)存分配需要時(shí),將拋出OutOfMemoryError異常。

2.6、運(yùn)行時(shí)常量池

運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類(lèi)的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。

2.7、直接內(nèi)存

直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。

在JDK 1.4 中新加入了NIO(New Input/Output)類(lèi),引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方法,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來(lái)回復(fù)制數(shù)據(jù)。

到這里我們大致知道了Java虛擬機(jī)的運(yùn)行時(shí)區(qū)的概況,接下來(lái)會(huì)繼續(xù)介紹更多JVM相關(guān)信息。

 

3:JVM內(nèi)存模型

 

Java內(nèi)存模型即Java Memory Model,簡(jiǎn)稱(chēng)JMM。JMM定義了Java 虛擬機(jī)(JVM)在計(jì)算機(jī)內(nèi)存(RAM)中的工作方式。JVM是整個(gè)計(jì)算機(jī)虛擬模型,所以JMM是隸屬于JVM的。

如果我們要想深入了解Java并發(fā)編程,就要先理解好Java內(nèi)存模型。Java內(nèi)存模型定義了多線程之間共享變量的可見(jiàn)性以及如何在需要的時(shí)候?qū)蚕碜兞窟M(jìn)行同步。原始的Java內(nèi)存模型效率并不是很理想,因此Java1.5版本對(duì)其進(jìn)行了重構(gòu),現(xiàn)在的Java8仍沿用了Java1.5的版本。

關(guān)于并發(fā)編程

在并發(fā)編程領(lǐng)域,有兩個(gè)關(guān)鍵問(wèn)題:線程之間的通信和同步。

線程之間的通信

線程的通信是指線程之間以何種機(jī)制來(lái)交換信息。在命令式編程中,線程之間的通信機(jī)制有兩種共享內(nèi)存和消息傳遞。

在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),線程之間通過(guò)寫(xiě)-讀內(nèi)存中的公共狀態(tài)來(lái)隱式進(jìn)行通信,典型的共享內(nèi)存通信方式就是通過(guò)共享對(duì)象進(jìn)行通信。

在消息傳遞的并發(fā)模型里,線程之間沒(méi)有公共狀態(tài),線程之間必須通過(guò)明確的發(fā)送消息來(lái)顯式進(jìn)行通信,在java中典型的消息傳遞方式就是wait()和notify()。

關(guān)于Java線程之間的通信,可以參考線程之間的通信(thread signal)。

線程之間的同步

同步是指程序用于控制不同線程之間操作發(fā)生相對(duì)順序的機(jī)制。

在共享內(nèi)存并發(fā)模型里,同步是顯式進(jìn)行的。程序員必須顯式指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行。

在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進(jìn)行的。

Java的并發(fā)采用的是共享內(nèi)存模型

Java線程之間的通信總是隱式進(jìn)行,整個(gè)通信過(guò)程對(duì)程序員完全透明。如果編寫(xiě)多線程程序的Java程序員不理解隱式進(jìn)行的線程之間通信的工作機(jī)制,很可能會(huì)遇到各種奇怪的內(nèi)存可見(jiàn)性問(wèn)題。

Java內(nèi)存模型

上面講到了Java線程之間的通信采用的是過(guò)共享內(nèi)存模型,這里提到的共享內(nèi)存模型指的就是Java內(nèi)存模型(簡(jiǎn)稱(chēng)JMM),JMM決定一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫(xiě)共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫(xiě)緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。

 

從上圖來(lái)看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個(gè)步驟:

1. 首先,線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去。

2. 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量。

下面通過(guò)示意圖來(lái)說(shuō)明這兩個(gè)步驟:

 

如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時(shí),這三個(gè)內(nèi)存中的x值都為0。線程A在執(zhí)行時(shí),把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?。

從整體來(lái)看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過(guò)程必須要經(jīng)過(guò)主內(nèi)存。JMM通過(guò)控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來(lái)為java程序員提供內(nèi)存可見(jiàn)性保證。

上面也說(shuō)到了,Java內(nèi)存模型只是一個(gè)抽象概念,那么它在Java中具體是怎么工作的呢?為了更好的理解上Java內(nèi)存模型工作方式,下面就JVM對(duì)Java內(nèi)存模型的實(shí)現(xiàn)、硬件內(nèi)存模型及它們之間的橋接做詳細(xì)介紹。

JVM對(duì)Java內(nèi)存模型的實(shí)現(xiàn)

在JVM內(nèi)部,Java內(nèi)存模型把內(nèi)存分成了兩部分:線程棧區(qū)和堆區(qū),下圖展示了Java內(nèi)存模型在JVM中的邏輯視圖:

 

JVM中運(yùn)行的每個(gè)線程都擁有自己的線程棧,線程棧包含了當(dāng)前線程執(zhí)行的方法調(diào)用相關(guān)信息,我們也把它稱(chēng)作調(diào)用棧。隨著代碼的不斷執(zhí)行,調(diào)用棧會(huì)不斷變化。

線程棧還包含了當(dāng)前方法的所有本地變量信息。一個(gè)線程只能讀取自己的線程棧,也就是說(shuō),線程中的本地變量對(duì)其它線程是不可見(jiàn)的。即使兩個(gè)線程執(zhí)行的是同一段代碼,它們也會(huì)各自在自己的線程棧中創(chuàng)建本地變量,因此,每個(gè)線程中的本地變量都會(huì)有自己的版本。

所有原始類(lèi)型(boolean,byte,short,char,int,long,float,double)的本地變量都直接保存在線程棧當(dāng)中,對(duì)于它們的值各個(gè)線程之間都是獨(dú)立的。對(duì)于原始類(lèi)型的本地變量,一個(gè)線程可以傳遞一個(gè)副本給另一個(gè)線程,當(dāng)它們之間是無(wú)法共享的。

堆區(qū)包含了Java應(yīng)用創(chuàng)建的所有對(duì)象信息,不管對(duì)象是哪個(gè)線程創(chuàng)建的,其中的對(duì)象包括原始類(lèi)型的封裝類(lèi)(如Byte、Integer、Long等等)。不管對(duì)象是屬于一個(gè)成員變量還是方法中的本地變量,它都會(huì)被存儲(chǔ)在堆區(qū)。

下圖展示了調(diào)用棧和本地變量都存儲(chǔ)在棧區(qū),對(duì)象都存儲(chǔ)在堆區(qū):

 

一個(gè)本地變量如果是原始類(lèi)型,那么它會(huì)被完全存儲(chǔ)到棧區(qū)。

一個(gè)本地變量也有可能是一個(gè)對(duì)象的引用,這種情況下,這個(gè)本地引用會(huì)被存儲(chǔ)到棧中,但是對(duì)象本身仍然存儲(chǔ)在堆區(qū)。

對(duì)于一個(gè)對(duì)象的成員方法,這些方法中包含本地變量,仍需要存儲(chǔ)在棧區(qū),即使它們所屬的對(duì)象在堆區(qū)。

對(duì)于一個(gè)對(duì)象的成員變量,不管它是原始類(lèi)型還是包裝類(lèi)型,都會(huì)被存儲(chǔ)到堆區(qū)。

Static類(lèi)型的變量以及類(lèi)本身相關(guān)信息都會(huì)隨著類(lèi)本身存儲(chǔ)在堆區(qū)。

堆中的對(duì)象可以被多線程共享。如果一個(gè)線程獲得一個(gè)對(duì)象的應(yīng)用,它便可訪問(wèn)這個(gè)對(duì)象的成員變量。如果兩個(gè)線程同時(shí)調(diào)用了同一個(gè)對(duì)象的同一個(gè)方法,那么這兩個(gè)線程便可同時(shí)訪問(wèn)這個(gè)對(duì)象的成員變量,但是對(duì)于本地變量,每個(gè)線程都會(huì)拷貝一份到自己的線程棧中。

下圖展示了上面描述的過(guò)程:

 

硬件內(nèi)存架構(gòu)

不管是什么內(nèi)存模型,最終還是運(yùn)行在計(jì)算機(jī)硬件上的,所以我們有必要了解計(jì)算機(jī)硬件內(nèi)存架構(gòu),下圖就簡(jiǎn)單描述了當(dāng)代計(jì)算機(jī)硬件內(nèi)存架構(gòu):

 

現(xiàn)代計(jì)算機(jī)一般都有2個(gè)以上CPU,而且每個(gè)CPU還有可能包含多個(gè)核心。因此,如果我們的應(yīng)用是多線程的話,這些線程可能會(huì)在各個(gè)CPU核心中并行運(yùn)行。

在CPU內(nèi)部有一組CPU寄存器,也就是CPU的儲(chǔ)存器。CPU操作寄存器的速度要比操作計(jì)算機(jī)主存快的多。在主存和CPU寄存器之間還存在一個(gè)CPU緩存,CPU操作CPU緩存的速度快于主存但慢于CPU寄存器。某些CPU可能有多個(gè)緩存層(一級(jí)緩存和二級(jí)緩存)。計(jì)算機(jī)的主存也稱(chēng)作RAM,所有的CPU都能夠訪問(wèn)主存,而且主存比上面提到的緩存和寄存器大很多。

當(dāng)一個(gè)CPU需要訪問(wèn)主存時(shí),會(huì)先讀取一部分主存數(shù)據(jù)到CPU緩存,進(jìn)而在讀取CPU緩存到寄存器。當(dāng)CPU需要寫(xiě)數(shù)據(jù)到主存時(shí),同樣會(huì)先f(wàn)lush寄存器到CPU緩存,然后再在某些節(jié)點(diǎn)把緩存數(shù)據(jù)flush到主存。

Java內(nèi)存模型和硬件架構(gòu)之間的橋接

正如上面講到的,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)并不一致。硬件內(nèi)存架構(gòu)中并沒(méi)有區(qū)分棧和堆,從硬件上看,不管是棧還是堆,大部分?jǐn)?shù)據(jù)都會(huì)存到主存中,當(dāng)然一部分棧和堆的數(shù)據(jù)也有可能會(huì)存到CPU寄存器中,如下圖所示,Java內(nèi)存模型和計(jì)算機(jī)硬件內(nèi)存架構(gòu)是一個(gè)交叉關(guān)系:

 

當(dāng)對(duì)象和變量存儲(chǔ)到計(jì)算機(jī)的各個(gè)內(nèi)存區(qū)域時(shí),必然會(huì)面臨一些問(wèn)題,其中最主要的兩個(gè)問(wèn)題是:

1. 共享對(duì)象對(duì)各個(gè)線程的可見(jiàn)性

2. 共享對(duì)象的競(jìng)爭(zhēng)現(xiàn)象123

共享對(duì)象的可見(jiàn)性

當(dāng)多個(gè)線程同時(shí)操作同一個(gè)共享對(duì)象時(shí),如果沒(méi)有合理的使用volatile和synchronization關(guān)鍵字,一個(gè)線程對(duì)共享對(duì)象的更新有可能導(dǎo)致其它線程不可見(jiàn)。

想象一下我們的共享對(duì)象存儲(chǔ)在主存,一個(gè)CPU中的線程讀取主存數(shù)據(jù)到CPU緩存,然后對(duì)共享對(duì)象做了更改,但CPU緩存中的更改后的對(duì)象還沒(méi)有flush到主存,此時(shí)線程對(duì)共享對(duì)象的更改對(duì)其它CPU中的線程是不可見(jiàn)的。最終就是每個(gè)線程最終都會(huì)拷貝共享對(duì)象,而且拷貝的對(duì)象位于不同的CPU緩存中。

下圖展示了上面描述的過(guò)程。左邊CPU中運(yùn)行的線程從主存中拷貝共享對(duì)象obj到它的CPU緩存,把對(duì)象obj的count變量改為2。但這個(gè)變更對(duì)運(yùn)行在右邊CPU中的線程不可見(jiàn),因?yàn)檫@個(gè)更改還沒(méi)有flush到主存中:

 

要解決共享對(duì)象可見(jiàn)性這個(gè)問(wèn)題,我們可以使用java volatile關(guān)鍵字。 Java’s volatile keyword. volatile 關(guān)鍵字可以保證變量會(huì)直接從主存讀取,而對(duì)變量的更新也會(huì)直接寫(xiě)到主存。volatile原理是基于CPU內(nèi)存屏障指令實(shí)現(xiàn)的,后面會(huì)講到。

競(jìng)爭(zhēng)現(xiàn)象

如果多個(gè)線程共享一個(gè)對(duì)象,如果它們同時(shí)修改這個(gè)共享對(duì)象,這就產(chǎn)生了競(jìng)爭(zhēng)現(xiàn)象。

如下圖所示,線程A和線程B共享一個(gè)對(duì)象obj。假設(shè)線程A從主存讀取Obj.count變量到自己的CPU緩存,同時(shí),線程B也讀取了Obj.count變量到它的CPU緩存,并且這兩個(gè)線程都對(duì)Obj.count做了加1操作。此時(shí),Obj.count加1操作被執(zhí)行了兩次,不過(guò)都在不同的CPU緩存中。

如果這兩個(gè)加1操作是串行執(zhí)行的,那么Obj.count變量便會(huì)在原始值上加2,最終主存中的Obj.count的值會(huì)是3。然而下圖中兩個(gè)加1操作是并行的,不管是線程A還是線程B先f(wàn)lush計(jì)算結(jié)果到主存,最終主存中的Obj.count只會(huì)增加1次變成2,盡管一共有兩次加1操作。

 

要解決上面的問(wèn)題我們可以使用java synchronized代碼塊。synchronized代碼塊可以保證同一個(gè)時(shí)刻只能有一個(gè)線程進(jìn)入代碼競(jìng)爭(zhēng)區(qū),synchronized代碼塊也能保證代碼塊中所有變量都將會(huì)從主存中讀,當(dāng)線程退出代碼塊時(shí),對(duì)所有變量的更新將會(huì)flush到主存,不管這些變量是不是volatile類(lèi)型的。

volatile和 synchronized區(qū)別

  1. volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取; synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問(wèn)該變量,其他線程被阻塞住。
  2. volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法、和類(lèi)級(jí)別的
  3. volatile僅能實(shí)現(xiàn)變量的修改可見(jiàn)性,不能保證原子性;而synchronized則可以保證變量的修改可見(jiàn)性和原子性
  4. volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
  5. volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化

支撐Java內(nèi)存模型的基礎(chǔ)原理

指令重排序

在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器會(huì)對(duì)指令做重排序。但是,JMM確保在不同的編譯器和不同的處理器平臺(tái)之上,通過(guò)插入特定類(lèi)型的Memory Barrier來(lái)禁止特定類(lèi)型的編譯器重排序和處理器重排序,為上層提供一致的內(nèi)存可見(jiàn)性保證。

  1. 編譯器優(yōu)化重排序:編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
  2. 指令級(jí)并行的重排序:如果不存l在數(shù)據(jù)依賴(lài)性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
  3. 內(nèi)存系統(tǒng)的重排序:處理器使用緩存和讀寫(xiě)緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。

數(shù)據(jù)依賴(lài)性

如果兩個(gè)操作訪問(wèn)同一個(gè)變量,其中一個(gè)為寫(xiě)操作,此時(shí)這兩個(gè)操作之間存在數(shù)據(jù)依賴(lài)性。

編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴(lài)性關(guān)系的兩個(gè)操作的執(zhí)行順序,即不會(huì)重排序。

as-if-serial

不管怎么重排序,單線程下的執(zhí)行結(jié)果不能被改變,編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。

內(nèi)存屏障(Memory Barrier )

上面講到了,通過(guò)內(nèi)存屏障可以禁止特定類(lèi)型處理器的重排序,從而讓程序按我們預(yù)想的流程去執(zhí)行。內(nèi)存屏障,又稱(chēng)內(nèi)存柵欄,是一個(gè)CPU指令,基本上它是一條這樣的指令:

  1. 保證特定操作的執(zhí)行順序。
  2. 影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見(jiàn)性。

編譯器和CPU能夠重排序指令,保證最終相同的結(jié)果,嘗試優(yōu)化性能。插入一條Memory Barrier會(huì)告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。

Memory Barrier所做的另外一件事是強(qiáng)制刷出各種CPU cache,如一個(gè)Write-Barrier(寫(xiě)入屏障)將刷出所有在Barrier之前寫(xiě)入 cache 的數(shù)據(jù),因此,任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。

這和java有什么關(guān)系?上面java內(nèi)存模型中講到的volatile是基于Memory Barrier實(shí)現(xiàn)的。

如果一個(gè)變量是volatile修飾的,JMM會(huì)在寫(xiě)入這個(gè)字段之后插進(jìn)一個(gè)Write-Barrier指令,并在讀這個(gè)字段之前插入一個(gè)Read-Barrier指令。這意味著,如果寫(xiě)入一個(gè)volatile變量,就可以保證:

  1. 一個(gè)線程寫(xiě)入變量a后,任何線程訪問(wèn)該變量都會(huì)拿到最新值。
  2. 在寫(xiě)入變量a之前的寫(xiě)入操作,其更新的數(shù)據(jù)對(duì)于其他線程也是可見(jiàn)的。因?yàn)镸emory Barrier會(huì)刷出cache中的所有先前的寫(xiě)入。

happens-before

從jdk5開(kāi)始,java使用新的JSR-133內(nèi)存模型,基于happens-before的概念來(lái)闡述操作之間的內(nèi)存可見(jiàn)性。

在JMM中,如果一個(gè)操作的執(zhí)行結(jié)果需要對(duì)另一個(gè)操作可見(jiàn),那么這兩個(gè)操作之間必須要存在happens-before關(guān)系,這個(gè)的兩個(gè)操作既可以在同一個(gè)線程,也可以在不同的兩個(gè)線程中。

與程序員密切相關(guān)的happens-before規(guī)則如下:

  1. 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中任意的后續(xù)操作。
  2. 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖操作,happens-before于隨后對(duì)這個(gè)鎖的加鎖操作。
  3. volatile域規(guī)則:對(duì)一個(gè)volatile域的寫(xiě)操作,happens-before于任意線程后續(xù)對(duì)這個(gè)volatile域的讀。
  4. 傳遞性規(guī)則:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:兩個(gè)操作之間具有happens-before關(guān)系,并不意味前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行!僅僅要求前一個(gè)操作的執(zhí)行結(jié)果,對(duì)于后一個(gè)操作是可見(jiàn)的,且前一個(gè)操作按順序排在后一個(gè)操作之前。

 

責(zé)任編輯:武曉燕 來(lái)源: 今日頭條
相關(guān)推薦

2021-10-05 20:29:55

JVM垃圾回收器

2019-02-13 16:22:53

網(wǎng)絡(luò)虛擬化大二層

2023-05-05 18:33:15

2021-03-16 08:54:35

AQSAbstractQueJava

2011-07-04 10:39:57

Web

2019-10-10 16:25:02

JVM數(shù)據(jù)多線程

2019-01-07 15:29:07

HadoopYarn架構(gòu)調(diào)度器

2021-07-20 15:20:02

FlatBuffers阿里云Java

2012-05-21 10:06:26

FrameworkCocoa

2017-07-02 18:04:53

塊加密算法AES算法

2022-09-26 09:01:15

語(yǔ)言數(shù)據(jù)JavaScript

2022-05-06 07:19:11

DOMDiff算法

2022-01-11 07:52:22

CSS 技巧代碼重構(gòu)

2025-03-27 09:38:35

2021-04-27 08:54:43

ConcurrentH數(shù)據(jù)結(jié)構(gòu)JDK8

2019-11-11 14:51:19

Java數(shù)據(jù)結(jié)構(gòu)Properties

2022-12-02 09:13:28

SeataAT模式

2012-02-21 13:55:45

JavaScript

2022-10-31 09:00:24

Promise數(shù)組參數(shù)

2018-11-09 16:24:25

物聯(lián)網(wǎng)云計(jì)算云系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)