前言
Java內(nèi)存模型(Java Memory Model)簡(jiǎn)稱JMM
闺阱,是java語(yǔ)言的運(yùn)行時(shí)內(nèi)存模型和規(guī)范, 是一種編程語(yǔ)言的規(guī)范炮车;
JVM內(nèi)存模型,是虛擬機(jī)的內(nèi)存管理模型酣溃,是一種虛擬機(jī)工程規(guī)范瘦穆;
JVM不僅僅是java語(yǔ)言的運(yùn)行時(shí)容器,還可以運(yùn)行多種其他語(yǔ)言赊豌,如Groovy
扛或、Scala
等等, JVM的內(nèi)存模型跟Java語(yǔ)言本身是沒(méi)有關(guān)系的;
JVM 內(nèi)存模型
JDK1.7以前的內(nèi)存結(jié)構(gòu)
JVM內(nèi)存結(jié)構(gòu)主要有三大塊:堆內(nèi)存碘饼、方法區(qū)和棧熙兔。
堆內(nèi)存是JVM中最大的一塊悲伶,由年輕代和老年代組成,而年輕代內(nèi)存又被分成三部分黔姜,Eden空間拢切、From Survivor空間、To Survivor空間秆吵,默認(rèn)情況下年輕代的這3種空間年輕代按照8:1:1的比例來(lái)分配;
方法區(qū)存儲(chǔ)類(lèi)信息淮椰、常量、靜態(tài)變量等數(shù)據(jù)纳寂,是線程共享的區(qū)域主穗,為與Java堆區(qū)分,方法區(qū)還有一個(gè)別名Non-Heap(非堆);
棧又分為java虛擬機(jī)棧和本地方法棧主要用于方法的執(zhí)行;
JDK1.8以后的內(nèi)存結(jié)構(gòu)
以前的方法區(qū)(或永久代)毙芜,用來(lái)存放class忽媒,Method等元數(shù)據(jù)信息,但在JDK1.8已經(jīng)沒(méi)有了腋粥,取而代之的是MetaSpace(元空間)晦雨,元空間不在虛擬機(jī)里面,而是直接使用本地內(nèi)存隘冲。
為什么要用元空間代替永久代闹瞧?
(1) 類(lèi)以及方法的信息比較難確定其大小,因此對(duì)于永久代的指定比較困難展辞,太小容易導(dǎo)致永久代溢出奥邮,太大容易導(dǎo)致老年代溢出。
(2) 永久代會(huì)給GC帶來(lái)不需要的復(fù)雜度罗珍,并且回收效率偏低洽腺。
JVM架構(gòu)概覽
1. Java堆(Heap)
對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),Java堆(Java Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊覆旱。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域蘸朋,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例扣唱,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存度液。
Java堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱做“GC堆”画舌。如果從內(nèi)存回收的角度看堕担,由于現(xiàn)在收集器基本都是采用的分代收集算法,所以Java堆中還可以細(xì)分為:新生代和老年代曲聂;再細(xì)致一點(diǎn)的有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ò)-Xmx和-Xms控制)特愿。
如果在堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí)勾缭,將會(huì)拋出OutOfMemoryError異常揍障。
2. 方法區(qū)(Method Area)
方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域俩由,在 Java 虛擬機(jī)規(guī)范中是這樣定義方法區(qū)的:它存儲(chǔ)了每個(gè)類(lèi)的結(jié)構(gòu)信息毒嫡,例如運(yùn)行時(shí)常量池、字段幻梯、方法數(shù)據(jù)兜畸、構(gòu)造函數(shù)和普通方法的字節(jié)碼內(nèi)容,還包括一些在類(lèi)碘梢、實(shí)例咬摇、接口初始化時(shí)用到的特殊方法。
HotSpot虛擬機(jī)上把方法區(qū)稱為“永久代”(Permanent Generation)痘系,本質(zhì)上兩者并不等價(jià)菲嘴,僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū)饿自,或者說(shuō)使用永久代來(lái)實(shí)現(xiàn)方法區(qū)而已汰翠。
Java虛擬機(jī)規(guī)范對(duì)這個(gè)區(qū)域的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外昭雌,還可以選擇不實(shí)現(xiàn)垃圾收集复唤。這個(gè)區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類(lèi)型的卸載。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定烛卧,當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí)佛纫,將拋出OutOfMemoryError異常。
3. 程序計(jì)數(shù)器(Program Counter Register)
程序計(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ǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成。
由于Java虛擬機(jī)的多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的融虽,在任何一個(gè)確定的時(shí)刻享完,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)只會(huì)執(zhí)行一條線程中的指令。因此有额,為了線程切換后能恢復(fù)到正確的執(zhí)行位置般又,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間的計(jì)數(shù)器互不影響谆吴,獨(dú)立存儲(chǔ)倒源,我們稱這類(lèi)內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個(gè)Java方法句狼,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址笋熬;如果正在執(zhí)行的是Natvie方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)腻菇。
此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域胳螟。
4. JVM棧(JVM Stacks)
與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的筹吐,它的生命周期與線程相同糖耸。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧丘薛、動(dòng)態(tài)鏈接嘉竟、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過(guò)程洋侨,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程舍扰。
- 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類(lèi)型(boolean、byte希坚、char边苹、short、int裁僧、float个束、long、double)聊疲、對(duì)象引用(reference類(lèi)型茬底,它不等同于對(duì)象本身,根據(jù)不同的虛擬機(jī)實(shí)現(xiàn)获洲,它可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔樬灞恚部赡苤赶蛞粋€(gè)代表對(duì)象的句柄或者其他與此對(duì)象相關(guān)的位置)和returnAddress類(lèi)型(指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí)捶枢,這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的握截,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
- 操作數(shù)棧烂叔,和局部變量區(qū)一樣谨胞,也被組織成一個(gè)以字長(zhǎng)為單位的數(shù)組,但和前者不同的是蒜鸡,它不是通過(guò)索引來(lái)訪問(wèn)的胯努,而是通過(guò)入棧和出棧來(lái)訪問(wèn)的,可把操作數(shù)棧理解為存儲(chǔ)計(jì)算時(shí)逢防,臨時(shí)數(shù)據(jù)的存儲(chǔ)區(qū)域叶沛。
- 除了局部變量區(qū)和操作數(shù)棧外,java棧幀還需要一些數(shù)據(jù)來(lái)支持常量池解析忘朝、正常方法返回以及異常派發(fā)機(jī)制灰署。這些數(shù)據(jù)都保存在java棧幀的幀數(shù)據(jù)區(qū)中。
在Java虛擬機(jī)規(guī)范中局嘁,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度溉箕,將拋出StackOverflowError異常;如果虛擬機(jī)椩藐牵可以動(dòng)態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動(dòng)態(tài)擴(kuò)展肴茄,只不過(guò)Java虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的虛擬機(jī)棧),當(dāng)擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常但指。
5. 本地方法棧(Native Method Stacks)
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的寡痰,其區(qū)別不過(guò)是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)棋凳。虛擬機(jī)規(guī)范中對(duì)本地方法棧中的方法使用的語(yǔ)言拦坠、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒(méi)有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它贫橙。甚至有的虛擬機(jī)(譬如Sun HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一贪婉。與虛擬機(jī)棧一樣反粥,本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemoryError異常卢肃。
JVM 調(diào)參
jvm常用參數(shù) | 含義 |
---|---|
-XX:+PrintGC/-verbose:gc | 打印GC的簡(jiǎn)要信息 |
-XX:+PrintGCDetails | 打印GC的詳細(xì)信息 |
-XX:+PrintGCDateStamps | 打印GC發(fā)生的時(shí)間 |
-Xloggc:log/gc.log | 指定GC的log位置,以文件輸出 |
-XX:+PrintHeapAtGC | 每一次GC后都打印堆信息 |
-XX:+HeapDumpOnOutOfMemoryError | 當(dāng)JVM發(fā)生OOM時(shí)才顿,自動(dòng)生成DUMP文件 |
-XX:HeapDumpPath=${目錄} | 生成的DUMP文件的存放位置 |
-Xms | 初始堆大小莫湘,默認(rèn)是物理內(nèi)存的1/64 |
-Xmx | 最大堆大小 默認(rèn)是物理內(nèi)存的1/4 |
-Xmn | 年輕代的大小,默認(rèn)整個(gè)堆的3/8 |
-Xss | 設(shè)置每個(gè)線程的堆棧大小 |
-XX:MetaspaceSize | 初始空間大小郑气,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類(lèi)型卸載幅垮,同時(shí)GC會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值尾组;如果釋放了很少的空間忙芒,那么在不超過(guò)MaxMetaspaceSize時(shí)示弓,適當(dāng)提高該值 |
-XX:MaxMetaspaceSize | 空間最大內(nèi)存,默認(rèn)是沒(méi)有限制的 |
-XX:MaxDirectMemorySize | 限制堆外內(nèi)存的使用 |
-XX:+DisableExplicitGC | 禁用 System.gc 顯式FullGC |
-XX:ReservedCodeCacheSize | 調(diào)整JIT編譯代碼緩存 |
-XX:PretenureSizeThreshold | 大于這個(gè)值的對(duì)象直接在老年代分配 |
-XX:+PrintGCApplicationConcurrentTime | 打印每次垃圾回收前呵萨,程序未中斷的執(zhí)行時(shí)間奏属。可與上面混合使用潮峦。輸出形式: Application time: 0.5291524 seconds
|
-XX:+PrintGCApplicationStoppedTime | 打印垃圾回收期間程序暫停的時(shí)間 |
JVM堆外內(nèi)存
堆外內(nèi)存就是把內(nèi)存對(duì)象分配在Java虛擬機(jī)的堆以外的內(nèi)存囱皿,這些內(nèi)存直接受操作系統(tǒng)管理(而不是虛擬機(jī)),這樣做的結(jié)果就是能夠在一定程度上減少垃圾回收對(duì)應(yīng)用程序造成的影響忱嘹。
堆外內(nèi)存主要包含:
- Meta Space 元空間
- Code Cache JIT編譯代碼緩存
- JVM線程棧
- Native方法棧
- 程序計(jì)數(shù)器
- Class Compress Space 類(lèi)壓縮空間
作為JAVA開(kāi)發(fā)者我們經(jīng)常用java.nio.DirectByteBuffer對(duì)象進(jìn)行堆外內(nèi)存的管理和使用嘱腥,它會(huì)在對(duì)象創(chuàng)建的時(shí)候就分配堆外內(nèi)存。
java.nio.DirectByteBuffer對(duì)象在創(chuàng)建過(guò)程中會(huì)先通過(guò)Unsafe接口直接通過(guò)os::malloc來(lái)分配內(nèi)存拘悦,然后將內(nèi)存的起始地址和大小存到j(luò)ava.nio.DirectByteBuffer對(duì)象里齿兔,這樣就可以直接操作這些內(nèi)存。這些內(nèi)存只有在DirectByteBuffer回收掉之后才有機(jī)會(huì)被回收础米,因此如果這些對(duì)象大部分都移到了old愧驱,但是一直沒(méi)有觸發(fā)CMS GC或者Full GC,那么悲劇將會(huì)發(fā)生椭盏,因?yàn)槟愕奈锢韮?nèi)存被他們耗盡了组砚,因此為了避免這種悲劇的發(fā)生,通過(guò)-XX:MaxDirectMemorySize來(lái)指定最大的堆外內(nèi)存大小掏颊,當(dāng)使用達(dá)到了閾值的時(shí)候?qū)⒄{(diào)用System.gc來(lái)做一次full gc糟红,以此來(lái)回收掉沒(méi)有被使用的堆外內(nèi)存
堆外內(nèi)存的好處:
1、減少了垃圾回收乌叶,因?yàn)槎褍?nèi)存垃圾回收會(huì)Stop The World盆偿。
2、加快了復(fù)制的速度准浴,堆內(nèi)數(shù)據(jù)在flush到遠(yuǎn)程時(shí)事扭,會(huì)先復(fù)制到直接內(nèi)存(非堆內(nèi)存),然后在發(fā)送乐横,而堆外內(nèi)存相當(dāng)于省略掉了這個(gè)工作求橄。
堆外內(nèi)存的缺點(diǎn):
1、堆外內(nèi)存的缺點(diǎn)就是內(nèi)存難以控制葡公,使用了堆外內(nèi)存就間接失去了JVM管理內(nèi)存的可行性罐农,改由自己來(lái)管理,當(dāng)發(fā)生內(nèi)存溢出時(shí)排查起來(lái)非常困難催什。
有許多單機(jī)緩存框架是用的堆外內(nèi)存涵亏,比如EHCache。
Java內(nèi)存模型(JMM)
Java的共享內(nèi)存
由上述對(duì)JVM內(nèi)存結(jié)構(gòu)的描述中,我們知道了堆和方法區(qū)是線程共享的气筋。方法調(diào)用的局部變量就不會(huì)在線程之間共享拆内,它們不會(huì)有內(nèi)存可見(jiàn)性問(wèn)題,也不受內(nèi)存模型的影響宠默。
CPU的處理速度和主存的讀寫(xiě)速度不是一個(gè)量級(jí)的矛纹,為了平衡這種巨大的差距,每個(gè)CPU都會(huì)有緩存光稼。共享變量會(huì)先放在主存中或南,每個(gè)線程都有屬于自己的工作內(nèi)存,并且會(huì)把位于主存中的共享變量拷貝到自己的工作內(nèi)存艾君,之后的讀寫(xiě)操作均使用位于工作內(nèi)存的變量副本采够,并在某個(gè)時(shí)刻將工作內(nèi)存的變量副本寫(xiě)回到主存中去。
Java線程之間的通信由Java內(nèi)存模型控制冰垄,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è)抽象概念蝴罪,它涵蓋了各種CPU緩存董济、寄存器以及其他的硬件和編譯器優(yōu)化。
Java的重排序
Class Reordering {
int x = 0, y = 0;
public void writer() {
x = 1;
y = 2;
}
public void reader() {
int r1 = y;
int r2 = x;
}
}
在執(zhí)行程序時(shí)為了提高性能要门,編譯器和處理器常常會(huì)對(duì)指令做重排序执虹。
這里說(shuō)的重排序可以發(fā)生在好幾個(gè)地方:編譯器港庄、運(yùn)行時(shí)梯浪、JIT等抖剿,比如編譯器會(huì)覺(jué)得把一個(gè)變量的寫(xiě)操作放在最后會(huì)更有效率,編譯后炒瘟,這個(gè)指令就在最后了(前提是只要不改變程序的語(yǔ)義吹埠,編譯器、執(zhí)行器就可以這樣自由的隨意優(yōu)化)疮装,一旦編譯器對(duì)某個(gè)變量的寫(xiě)操作進(jìn)行優(yōu)化(放到最后)缘琅,那么在執(zhí)行之前,另一個(gè)線程將不會(huì)看到這個(gè)執(zhí)行結(jié)果斩个。
JMM 語(yǔ)義
由于Java共享內(nèi)存以及Java重排序的存在胯杭,會(huì)導(dǎo)致多線程環(huán)境下存在操作可見(jiàn)性的問(wèn)題驯杜。
為了方便程序員進(jìn)行并發(fā)編程受啥,Java定義了一些規(guī)則,這規(guī)則稱為happens-before規(guī)則,從JDK 5 開(kāi)始滚局,JMM就使用happens-before的概念來(lái)闡述多線程編程時(shí)的操作可見(jiàn)性居暖。
happends-before含義:cpu在某個(gè)時(shí)間片執(zhí)行一個(gè)操作后,cpu按時(shí)間輪片到后續(xù)任意線程執(zhí)行時(shí)藤肢,都能觀察到這個(gè)操作的效果太闺;
在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn)嘁圈,那么這兩個(gè)操作之間必須存在happens-before關(guān)系省骂。
happens-before原則定義如下:
1、程序次序規(guī)則:一個(gè)線程內(nèi)最住,按照代碼順序钞澳,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作;
2涨缚、鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作轧粟;
3、volatile變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作脓魏;
4兰吟、傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C茂翔,則可以得出操作A先行發(fā)生于操作C混蔼;
5、線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作珊燎;
6拄丰、線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生;
7俐末、線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè)料按,我們可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行卓箫;
8载矿、對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開(kāi)始;
JMM的底層實(shí)現(xiàn)
從java編程的角度來(lái)看烹卒,JMM是通過(guò)synchronize
闷盔、volatile
以及并發(fā)包里的鎖來(lái)實(shí)現(xiàn)JMM語(yǔ)義;
volatile 實(shí)現(xiàn)原理
volatile
通過(guò)插入內(nèi)存屏障保證線程可見(jiàn)性和禁止重排序旅急。
volatile可見(jiàn)性實(shí)現(xiàn)原理
從JVM編譯器的字節(jié)碼角度來(lái)看:
volatile在JVM編譯時(shí)是采用“內(nèi)存屏障”來(lái)實(shí)現(xiàn)的逢勾。觀察加入volatile關(guān)鍵字和沒(méi)有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時(shí)藐吮,會(huì)多出一個(gè)lock前綴指令溺拱。lock前綴指令其實(shí)就相當(dāng)于一個(gè)內(nèi)存屏障逃贝。內(nèi)存屏障是一組處理指令,用來(lái)實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制迫摔。
從操作系統(tǒng)角度看:
加了內(nèi)存屏障的字節(jié)碼在執(zhí)行時(shí)沐扳,操作系統(tǒng)解決了緩存一致性;
操作系統(tǒng)解決緩存一致性方案有兩種:
1句占、 通過(guò)在總線加LOCK的方式
2沪摄、通過(guò)緩存一致性協(xié)議
但是方案1存在一個(gè)問(wèn)題,它是采用一種獨(dú)占的方式來(lái)實(shí)現(xiàn)的纱烘,即總線加LOCK#鎖的話杨拐,只能有一個(gè)CPU能夠運(yùn)行,其他CPU都得阻塞擂啥,效率較為低下戏阅。 第二種方案,緩存一致性協(xié)議(MESI協(xié)議)它確保每個(gè)緩存中使用的共享變量的副本是一致的啤它。其核心思想如下:當(dāng)某個(gè)CPU在寫(xiě)數(shù)據(jù)時(shí)奕筐,如果發(fā)現(xiàn)操作的變量是共享變量,則會(huì)通知其他CPU告知該變量的緩存行是無(wú)效的变骡,因此其他CPU在讀取該變量時(shí)离赫,發(fā)現(xiàn)其無(wú)效會(huì)重新從主存中加載數(shù)據(jù)。
volatile 禁止重排序?qū)崿F(xiàn)原理
從JVM編譯器的字節(jié)碼角度來(lái)看:
首先是插入內(nèi)存屏障塌碌;其次會(huì)禁止一些特定類(lèi)型的編譯器重排序渊胸;
從操作系統(tǒng)的角度看:
處理器重排序,是指令級(jí)并行的重排序√ㄗ保現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行翎猛。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序接剩;
針對(duì)處理器重排序切厘,編譯器在生成指令序列的時(shí)候會(huì)通過(guò)插入內(nèi)存屏障指令來(lái)禁止某些特殊的處理器重排序。
synchronize 實(shí)現(xiàn)原理
synchronize 比 volatile更重一些懊缺,映射到操作系統(tǒng)的底層原理基本一致疫稿,在JVM層級(jí)的實(shí)現(xiàn)會(huì)更復(fù)雜,synchronize的JVM實(shí)現(xiàn)做過(guò)很多優(yōu)化鹃两,不僅保證可見(jiàn)性遗座、有序性,還能保證代碼塊的原子性俊扳;
synchronize是通過(guò)java對(duì)象頭的Mark區(qū)來(lái)輔助實(shí)現(xiàn)途蒋,分為無(wú)鎖、偏向鎖馋记、輕量級(jí)鎖(自旋)号坡、重量級(jí)鎖(系統(tǒng)調(diào)用)等狀態(tài)懊烤;
ObjectMonitor() {
_count = 0; //用來(lái)記錄該對(duì)象被線程獲取鎖的次數(shù)
_waiters = 0;
_recursions = 0; //鎖的重入次數(shù)
_owner = NULL; //指向持有ObjectMonitor對(duì)象的線程
_WaitSet = NULL; //處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程筋帖,會(huì)被加入到該列表
}
JMM實(shí)現(xiàn)了更友好的并發(fā)編程
總的來(lái)說(shuō)奸晴,在多線程開(kāi)發(fā)時(shí)需要從原子性冤馏,有序性日麸,可見(jiàn)性三個(gè)方面進(jìn)行考慮;
as-if-serial
與 happens-before
- as-if-serial語(yǔ)義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變逮光,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變代箭。
- as-if-serial語(yǔ)義給編寫(xiě)單線程程序的程序員創(chuàng)造了一個(gè)幻境:?jiǎn)尉€程程序是按程序的順序來(lái)執(zhí)行的。
- happens-before關(guān)系給編寫(xiě)正確同步的多線程程序的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程序是按happens-before指定的順序來(lái)執(zhí)行的涕刚。
- as-if-serial語(yǔ)義和happens-before這么做的目的嗡综,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度杜漠。
站在JMM設(shè)計(jì)者的角度极景,在設(shè)計(jì)JMM時(shí)需要考慮兩個(gè)關(guān)鍵因素:
1、程序員對(duì)內(nèi)存模型的使用
- 程序員希望內(nèi)存模型易于理解驾茴、易于編程盼樟。
- 程序員希望基于一個(gè)強(qiáng)內(nèi)存模型來(lái)編寫(xiě)代碼。
2锈至、編譯器和處理器對(duì)內(nèi)存模型的實(shí)現(xiàn)
- 編譯器和處理器希望內(nèi)存模型對(duì)它們的束縛越少越好晨缴,這樣它們就可以做盡可能多的優(yōu)化來(lái)提高性能。
- 編譯器和處理器希望實(shí)現(xiàn)一個(gè)弱內(nèi)存模型峡捡。
JMM其實(shí)是在遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序)击碗,編譯器和處理器怎么優(yōu)化都行。
例如们拙,如果編譯器經(jīng)過(guò)細(xì)致的分析后稍途,認(rèn)定一個(gè)鎖只會(huì)被單個(gè)線程訪問(wèn),那么這個(gè)鎖可以被消除砚婆。再如晰房,如果編譯器經(jīng)過(guò)細(xì)致的分析后,認(rèn)定一個(gè)volatile變量只會(huì)被單個(gè)線程訪問(wèn)射沟,那么編譯器可以把這個(gè)volatile變量當(dāng)作一個(gè)普通變量來(lái)對(duì)待殊者。這些優(yōu)化既不會(huì)改變程序的執(zhí)行結(jié)果,又能提高程序的執(zhí)行效率验夯。
一個(gè)happens-before規(guī)則對(duì)應(yīng)于一個(gè)或多個(gè)編譯器和處理器重排序規(guī)則猖吴。對(duì)于Java程序員來(lái)說(shuō),happens-before規(guī)則簡(jiǎn)單易懂挥转,它避免Java程序員為了理解JMM提供的可見(jiàn)性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)方法.