前言
這篇主要了解 JVM 內(nèi)在的運行結(jié)構(gòu)是怎么樣的边灭。
一丘跌、虛擬機的意義
Java 作為一門高級程序語言,它的語法非常復(fù)雜狠角,抽象程度也很高号杠。因此,直接在硬件上運行這種復(fù)雜的程序并不現(xiàn)實丰歌。所以呢姨蟋,在運行 Java 程序之前,我們需要對其進行一番轉(zhuǎn)換立帖。
Java 虛擬機可以由硬件實現(xiàn)眼溶,但更為常見的是在各個現(xiàn)有平臺(如 Windows_x64、Linux_aarch64)上提供軟件實現(xiàn)晓勇。這么做的意義在于堂飞,一旦一個程序被轉(zhuǎn)換成 Java 字節(jié)碼,那么它便可以在不同平臺上的虛擬機實現(xiàn)里運行绑咱。這也就是我們經(jīng)常說的“一次編寫绰筛,到處運行”。
虛擬機的另外一個好處是它帶來了一個托管環(huán)境(Managed Runtime)描融。這個托管環(huán)境能夠代替我們處理一些代碼中冗長而且容易出錯的部分别智。其中最廣為人知的當(dāng)屬自動內(nèi)存管理與垃圾回收,這部分內(nèi)容甚至催生了一波垃圾回收調(diào)優(yōu)的業(yè)務(wù)稼稿。
使用虛擬機還有一個好處薄榛,可以在編譯的過程中對代碼進行優(yōu)化,對代碼進行精簡让歼,以提高執(zhí)行效率敞恋。
虛擬機結(jié)構(gòu)
可以看出,JVM主要由類加載器子系統(tǒng)谋右、運行時數(shù)據(jù)區(qū)(內(nèi)存空間)硬猫、執(zhí)行引擎以及與本地方法接口等組成。其中運行時數(shù)據(jù)區(qū)又由方法區(qū)、堆啸蜜、Java棧坑雅、PC寄存器、本地方法棧組成衬横。
從上圖中還可以看出裹粤,在內(nèi)存空間中方法區(qū)和堆是所有Java線程共享的,而 Java棧蜂林、本地方法棧遥诉、PC寄存器則由每個線程私有,這會引出一些問題噪叙,后文會進行具體討論矮锈。
眾所周知,Java語言具有跨平臺的特性睁蕾,這也是由JVM來實現(xiàn)的苞笨。更準確地說,是Sun利用JVM在不同平臺上的實現(xiàn)幫我們把平臺相關(guān)性的問題給解決了子眶,這就好比是HTML語言可以在不同廠商的瀏覽器上呈現(xiàn)元素(雖然某些瀏覽器在對W3C標準的支持上還有一些問題)猫缭。同時,Java語言支持通過JNI(Java Native Interface)來實現(xiàn)本地方法的調(diào)用壹店,但是需要注意到猜丹,如果你在Java程序用調(diào)用了本地方法,那么你的程序就很可能不再具有跨平臺性硅卢,即本地方法會破壞平臺無關(guān)性射窒。
二、ClassLoader的分類
- 啟動類加載器(BootStrap Class Loader):啟動類加載器主要加載的是JVM自身需要的類将塑,這個類加載使用C++語言實現(xiàn)的脉顿,是虛擬機自身的一部分,它負責(zé)將 <JAVA_HOME>/lib 路徑下的核心類庫或 -Xbootclasspath 參數(shù)指定的路徑下的jar包加載到內(nèi)存中点寥,注意必由于虛擬機是按照文件名識別加載jar包的艾疟,如 rt.jar,如果文件名不被虛擬機識別敢辩,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮蔽莱,Bootstrap啟動類加載器只加載包名為java、javax戚长、sun等開頭的類)盗冷,在 Sun JDK中,這個類加載器是由 C++ 實現(xiàn)的同廉,并且在 Java 語言中無法獲得它的引用仪糖。
- 擴展類加載器(Extension Class Loader):擴展類加載器是指Sun公司(已被Oracle收購)實現(xiàn)的sun.misc.Launcher$ExtClassLoader 類柑司,由Java語言實現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類锅劝,它負責(zé)加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量 -Djava.ext.dir 指定位路徑中的類庫攒驰,開發(fā)者可以直接使用標準擴展類加載器。
- 系統(tǒng)類加載器(System Class Loader):也稱應(yīng)用程序加載器是指 Sun公司實現(xiàn)的sun.misc.Launcher$AppClassLoader故爵。它負責(zé)加載系統(tǒng)類路徑j(luò)ava -classpath或 -Djava.class.path 指定路徑下的類庫玻粪,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用系統(tǒng)類加載器稠集,一般情況下該類加載是程序中默認的類加載器奶段,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器饥瓷。通常我們自己寫的Java類也是由該ClassLoader加載剥纷。在Sun JDK中,系統(tǒng)類加載器的名字叫 AppClassLoader呢铆。
- 用戶自定義類加載器(User Defined Class Loader):由用戶自定義類的加載規(guī)則晦鞋,可以手動控制加載過程中的步驟。
1. ClassLoader的工作原理
類加載分為裝載棺克、鏈接悠垛、初始化三步。
ClassLoader loader = TestClassLoader.class.getClassLoader();
System.out.println(loader.toString());
System.out.println(loader.getParent().toString());
System.out.println(loader.getParent().getParent());
1.1裝載
通過類的全限定名和ClassLoader加載類娜谊,主要是將指定的.class文件加載至JVM确买。當(dāng)類被加載以后,在JVM內(nèi)部就以“類的全限定名+ClassLoader實例ID”來標明類纱皆。
在內(nèi)存中湾趾,ClassLoader實例和類的實例都位于堆中,它們的類信息都位于方法區(qū)派草。
裝載過程采用了一種被稱為“雙親委派模型(Parent Delegation Model)”的方式搀缠,當(dāng)一個ClassLoader要加載類時,它會先請求它的雙親ClassLoader(其實這里只有兩個ClassLoader近迁,所以稱為父ClassLoader可能更容易理解)加載類艺普,而它的雙親ClassLoader會繼續(xù)把加載請求提交再上一級的ClassLoader,直到啟動類加載器鉴竭。只有其雙親ClassLoader無法加載指定的類時歧譬,它才會自己加載類。
雙親委派模型是JVM的第一道安全防線搏存,它保證了類的安全加載缴罗,這里同時依賴了類加載器隔離的原理:不同類加載器加載的類之間是無法直接交互的,即使是同一個類祭埂,被不同的ClassLoader加載面氓,它們也無法感知到彼此的存在兵钮。這樣即使有惡意的類冒充自己在核心包(例如java.lang)下,由于它無法被啟動類加載器加載舌界,也造成不了危害掘譬。
由此也可見,如果用戶自定義了類加載器呻拌,那就必須自己保障類加載過程中的安全葱轩。
2. 鏈接
鏈接的任務(wù)是把二進制的類型信息合并到JVM運行時狀態(tài)中去。
鏈接分為以下三步:
- 驗證:校驗.class文件的正確性藐握,確保該文件是符合規(guī)范定義的靴拱,并且適合當(dāng)前JVM使用。
- 準備:為類分配內(nèi)存猾普,同時初始化類中的靜態(tài)變量賦值為默認值袜炕。
- 解析(可選):主要是把類的常量池中的符號引用解析為直接引用,這一步可以在用到相應(yīng)的引用時再解析初家。
3. 初始化
初始化類中的靜態(tài)變量偎窘,并執(zhí)行類中的static代碼、構(gòu)造函數(shù)溜在。
JVM規(guī)范嚴格定義了何時需要對類進行初始化:
- 通過new關(guān)鍵字陌知、反射、clone掖肋、反序列化機制實例化對象時仆葡。
- 調(diào)用類的靜態(tài)方法時。
- 使用類的靜態(tài)字段或?qū)ζ滟x值時志笼。
- 通過反射調(diào)用類的方法時沿盅。
- 初始化該類的子類時(初始化子類前其父類必須已經(jīng)被初始化)。
- JVM啟動時被標記為啟動類的類(簡單理解為具有main方法的類)籽腕。
三嗡呼、運行時數(shù)據(jù)區(qū)
運行時數(shù)據(jù)區(qū)由方法區(qū)、堆皇耗、Java棧南窗、PC寄存器、本地方法棧組成郎楼。
1. Java棧(Java Stack)
Java棧的主要任務(wù)是存儲方法參數(shù)万伤、局部變量、中間運算結(jié)果呜袁,并且提供部分其它模塊工作需要的數(shù)據(jù)敌买。
Java棧總是與線程關(guān)聯(lián)在一起的阶界,每當(dāng)創(chuàng)建一個線程虹钮,JVM就會為該線程創(chuàng)建對應(yīng)的Java棧聋庵,在這個Java棧中又會包含多個棧幀(Stack Frame),這些棧幀是與每個方法關(guān)聯(lián)起來的芙粱,每運行一個方法就創(chuàng)建一個棧幀祭玉,每個棧幀會含有一些局部變量、操作棧和方法返回值等信息春畔。每當(dāng)一個方法執(zhí)行完成時脱货,該棧幀就會彈出棧幀的元素作為這個方法的返回值,并且清除這個棧幀律姨,Java棧的棧頂?shù)臈褪钱?dāng)前正在執(zhí)行的活動棧振峻,也就是當(dāng)前正在執(zhí)行的方法,PC寄存器也會指向該地址择份。只有這個活動的棧幀的本地變量可以被操作棧使用扣孟,當(dāng)在這個棧幀中調(diào)用另外一個方法時,與之對應(yīng)的一個新的棧幀被創(chuàng)建缓淹,這個新創(chuàng)建的棧幀被放到Java棧的棧頂哈打,變?yōu)楫?dāng)前的活動棧塔逃。同樣現(xiàn)在只有這個棧的本地變量才能被使用讯壶,當(dāng)這個棧幀中所有指令都完成時,這個棧幀被移除Java棧湾盗,剛才的那個棧幀變?yōu)榛顒訔茫懊鏃姆祷刂底優(yōu)檫@個棧幀的操作棧的一個操作數(shù)。
由于Java棧是與線程對應(yīng)起來的格粪,Java棧數(shù)據(jù)不是線程共有的躏吊,所以不需要關(guān)心其數(shù)據(jù)一致性,也不會存在同步鎖的問題帐萎。
它分為三部分:局部變量區(qū)比伏、操作數(shù)棧、幀數(shù)據(jù)區(qū)疆导。
局部變量區(qū)
局部變量區(qū)是以字長為單位的數(shù)組赁项,在這里,byte澈段、short悠菜、char類型會被轉(zhuǎn)換成int類型存儲,除了long和double類型占兩個字長以外败富,其余類型都只占用一個字長悔醋。特別地,boolean類型在編譯時會被轉(zhuǎn)換成int或byte類型兽叮,boolean數(shù)組會被當(dāng)做byte類型數(shù)組來處理芬骄。局部變量區(qū)也會包含對象的引用猾愿,包括類引用、接口引用以及數(shù)組引用账阻。
局部變量區(qū)包含了方法參數(shù)和局部變量匪蟀,此外,實例方法隱含第一個局部變量this宰僧,它指向調(diào)用該方法的對象引用材彪。對于對象,局部變量區(qū)中永遠只有指向堆的引用琴儿。
操作數(shù)棧
操作數(shù)棧也是以字長為單位的數(shù)組段化,但是正如其名,它只能進行入棧出棧的基本操作造成。在進行計算時显熏,操作數(shù)被彈出棧,計算完畢后再入棧晒屎。
幀數(shù)據(jù)區(qū)
幀數(shù)據(jù)區(qū)的任務(wù)主要有:
記錄指向類的常量池的指針喘蟆,以便于解析。
幫助方法的正常返回鼓鲁,包括恢復(fù)調(diào)用該方法的棧幀蕴轨,設(shè)置PC寄存器指向調(diào)用方法對應(yīng)的下一條指令,把返回值壓入調(diào)用棧幀的操作數(shù)棧中骇吭。
記錄異常表橙弱,發(fā)生異常時將控制權(quán)交由對應(yīng)異常的catch子句,如果沒有找到對應(yīng)的catch子句燥狰,會恢復(fù)調(diào)用方法的棧幀并重新拋出異常棘脐。
局部變量區(qū)和操作數(shù)棧的大小依照具體方法在編譯時就已經(jīng)確定。調(diào)用方法時會從方法區(qū)中找到對應(yīng)類的類型信息龙致,從中得到具體方法的局部變量區(qū)和操作數(shù)棧的大小蛀缝,依此分配棧幀內(nèi)存,壓入Java棧目代。
在Java虛擬機規(guī)范中屈梁,對這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常像啼;如果虛擬機可以動態(tài)擴展俘闯,如果擴展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常忽冻。
2. 本地方法棧(Native Method Stack)
本地方法棧類似于Java棧真朗,主要存儲了本地方法調(diào)用的狀態(tài)。區(qū)別不過是Java棧為JVM執(zhí)行Java方法服務(wù)僧诚,而本地方法棧為JVM執(zhí)行Native方法服務(wù)遮婶。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常蝗碎。在Sun JDK中,本地方法棧和Java棧是同一個旗扑。
3. PC寄存器/程序計數(shù)器(Program Count Register)
嚴格來說是一個數(shù)據(jù)結(jié)構(gòu)蹦骑,用于保存當(dāng)前正在執(zhí)行的程序的內(nèi)存地址,由于Java是支持多線程執(zhí)行的臀防,所以程序執(zhí)行的軌跡不可能一直都是線性執(zhí)行眠菇。當(dāng)有多個線程交叉執(zhí)行時,被中斷的線程的程序當(dāng)前執(zhí)行到哪條內(nèi)存地址必然要保存下來袱衷,以便用于被中斷的線程恢復(fù)執(zhí)行時再按照被中斷時的指令地址繼續(xù)執(zhí)行下去捎废。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每個線程都需要有一個獨立的程序計數(shù)器致燥,各個線程之間計數(shù)器互不影響登疗,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存,這在某種程度上有點類似于“ThreadLocal”嫌蚤,是線程安全的辐益。
4. 方法區(qū)(Method Area)
類型信息和類的靜態(tài)變量都存儲在方法區(qū)中。方法區(qū)中對于每個類存儲了以下數(shù)據(jù):
- 類及其父類的全限定名(java.lang.Object沒有父類)
- 類的類型(Class or Interface)
- 訪問修飾符(public, abstract, final)
- 實現(xiàn)的接口的全限定名的列表
- 常量池
- 字段信息
- 方法信息
- 靜態(tài)變量
- ClassLoader引用
- Class引用
可見類的所有信息都存儲在方法區(qū)中脱吱。由于方法區(qū)是所有線程共享的智政,所以必須保證線程安全,舉例來說急凰,如果兩個類同時要加載一個尚未被加載的類女仰,那么一個類會請求它的ClassLoader去加載需要的類猜年,另一個類只能等待而不會重復(fù)加載抡锈。
常量池本身是方法區(qū)中的一個數(shù)據(jù)結(jié)構(gòu)。常量池中存儲了如字符串乔外、final變量值床三、類名和方法名常量。常量池在編譯期間就被確定杨幼,并保存在已編譯的.class文件中撇簿。一般分為兩類:字面量和應(yīng)用量舔哪。字面量就是字符串困檩、final變量等。類名和方法名屬于引用量因妇。引用量最常見的是在調(diào)用方法的時候欲逃,根據(jù)方法名找到方法的引用找蜜,并以此定為到函數(shù)體進行函數(shù)代碼的執(zhí)行。引用量包含:類和接口的權(quán)限定名稳析、字段的名稱和描述符洗做,方法的名稱和描述符弓叛。
此外為了加快調(diào)用方法的速度,通常還會為每個非抽象類創(chuàng)建私有的方法表诚纸,方法表是一個數(shù)組撰筷,存放了實例可能被調(diào)用的實例方法的直接引用。
在Sun JDK中畦徘,方法區(qū)對應(yīng)了持久代(Permanent Generation)毕籽,默認最小值為16MB,最大值為64MB井辆。大小可以通過參數(shù)來設(shè)置,可以通過-XX:PermSize指定初始值影钉,-XX:MaxPermSize指定最大值。
5. 堆(Heap)
堆是JVM所管理的內(nèi)存中最大的一塊掘剪,是被所有Java線程鎖共享的平委,不是線程安全的,在JVM啟動時創(chuàng)建夺谁。
堆用于存儲對象實例以及數(shù)組值廉赔。堆是存儲Java對象的地方,這一點Java虛擬機規(guī)范中描述是:所有的對象實例以及數(shù)組都要在堆上分配匾鸥。堆中有指向類數(shù)據(jù)的指針蜡塌,該指針指向了方法區(qū)中對應(yīng)的類型信息。堆中還可能存放了指向方法表的指針勿负。堆是所有線程共享的馏艾,所以在進行實例化對象等操作時,需要解決同步問題奴愉。此外琅摩,堆中的實例數(shù)據(jù)中還包含了對象鎖,并且針對不同的垃圾收集策略锭硼,可能存放了引用計數(shù)或清掃標記等數(shù)據(jù)房资。
在堆的管理上,Sun JDK從1.2版本開始引入了分代管理的方式檀头。主要分為新生代轰异、舊生代。分代方式大大改善了垃圾收集的效率暑始。
1. 新生代(New Generation):大多數(shù)情況下新對象都被分配在新生代中搭独,新生代由Eden Space和兩塊相同大小的Survivor Space組成,后兩者主要用于Minor GC時的對象復(fù)制(Minor GC的過程在此不詳細討論)廊镜。JVM在Eden Space中會開辟一小塊獨立的TLAB(Thread Local Allocation Buffer)區(qū)域用于更高效的內(nèi)存分配牙肝,我們知道在堆上分配內(nèi)存需要鎖定整個堆,而在TLAB上則不需要,JVM在分配對象時會盡量在TLAB上分配惊奇,以提高效率互躬。
2. 老年代(Old Generation/Tenuring Generation):在新生代中存活時間較久的對象將會被轉(zhuǎn)入老年代,老年代進行垃圾收集的頻率沒有新生代高颂郎。
四吼渡、執(zhí)行引擎
執(zhí)行引擎是JVM執(zhí)行Java字節(jié)碼的核心,執(zhí)行方式主要分為解釋執(zhí)行乓序、編譯執(zhí)行寺酪、自適應(yīng)優(yōu)化執(zhí)行、硬件芯片執(zhí)行方式替劈。
JVM的指令集是基于棧而非寄存器的寄雀,這樣做的好處在于可以使指令盡可能緊湊,便于快速地在網(wǎng)絡(luò)上傳輸(別忘了Java最初就是為網(wǎng)絡(luò)設(shè)計的)陨献,同時也很容易適應(yīng)通用寄存器較少的平臺盒犹,并且有利于代碼優(yōu)化,由于Java棧和PC寄存器是線程私有的眨业,線程之間無法互相干涉彼此的棧急膀。每個線程擁有獨立的JVM執(zhí)行引擎實例。
JVM指令由單字節(jié)操作碼和若干操作數(shù)組成龄捡。對于需要操作數(shù)的指令卓嫂,通常是先把操作數(shù)壓入操作數(shù)棧,即使是對局部變量賦值聘殖,也會先入棧再賦值晨雳。注意這里是“通常”情況奸腺,之后會講到由于優(yōu)化導(dǎo)致的例外餐禁。
1. 解釋執(zhí)行
和一些動態(tài)語言類似,JVM可以解釋執(zhí)行字節(jié)碼洋机。Sun JDK采用了token-threading的方式坠宴,感興趣的同學(xué)可以深入了解一下。解釋執(zhí)行中有幾種優(yōu)化方式:
- 棧頂緩存:將位于操作數(shù)棧頂?shù)闹抵苯泳彺嬖诩拇嫫魃媳疗欤瑢τ诖蟛糠种恍枰粋€操作數(shù)的指令而言,就無需再入棧副砍,可以直接在寄存器上進行計算衔肢,結(jié)果壓入操作數(shù)棧。這樣便減少了寄存器和內(nèi)存的交換開銷豁翎。
- 部分棧幀共享:被調(diào)用方法可將調(diào)用方法棧幀中的操作數(shù)棧作為自己的局部變量區(qū)角骤,這樣在獲取方法參數(shù)時減少了復(fù)制參數(shù)的開銷。
- 執(zhí)行機器指令:在一些特殊情況下,JVM會執(zhí)行機器指令以提高速度邦尊。
2. 編譯執(zhí)行
為了提升執(zhí)行速度背桐,Sun JDK提供了將字節(jié)碼編譯為機器指令的支持,主要利用了JIT(Just-In-Time)編譯器在運行時進行編譯蝉揍,它會在第一次執(zhí)行時編譯字節(jié)碼為機器碼并緩存链峭,之后就可以重復(fù)利用。Oracle JRockit采用的是完全的編譯執(zhí)行又沾。
3. 自適應(yīng)優(yōu)化執(zhí)行
自適應(yīng)優(yōu)化執(zhí)行的思想是程序中10%20%的代碼占據(jù)了80%90%的執(zhí)行時間弊仪,所以通過將那少部分代碼編譯為優(yōu)化過的機器碼就可以大大提升執(zhí)行效率。自適應(yīng)優(yōu)化的典型代表是Sun的Hotspot VM杖刷,正如其名励饵,JVM會監(jiān)測代碼的執(zhí)行情況,當(dāng)判斷特定方法是瓶頸或熱點時滑燃,將會啟動一個后臺線程役听,把該方法的字節(jié)碼編譯為極度優(yōu)化的、靜態(tài)鏈接的C++代碼表窘。當(dāng)方法不再是熱區(qū)時禾嫉,則會取消編譯過的代碼,重新進行解釋執(zhí)行蚊丐。
自適應(yīng)優(yōu)化不僅通過利用小部分的編譯時間獲得大部分的效率提升熙参,而且由于在執(zhí)行過程中時刻監(jiān)測,對內(nèi)聯(lián)代碼等優(yōu)化也起到了很大的作用麦备。由于面向?qū)ο蟮亩鄳B(tài)性孽椰,一個方法可能對應(yīng)了很多種不同實現(xiàn),自適應(yīng)優(yōu)化就可以通過監(jiān)測只內(nèi)聯(lián)那些用到的代碼凛篙,大大減少了內(nèi)聯(lián)函數(shù)的大小黍匾。
Sun JDK在編譯上采用了兩種模式:Client和Server模式。前者較為輕量級呛梆,占用內(nèi)存較少锐涯。后者的優(yōu)化程序更高,占用內(nèi)存更多填物。
在Server模式中會進行對象的逃逸分析纹腌,即方法中的對象是否會在方法外使用,如果被其它方法使用了滞磺,則該對象是逃逸的升薯。對于非逃逸對象,JVM會在棧上直接分配對象(所以對象不一定是在堆上分配的)击困,線程獲取對象會更加快速涎劈,同時當(dāng)方法返回時,由于棧幀被拋棄,也有利于對象的垃圾收集蛛枚。Server模式還會通過分析去除一些不必要的同步谅海,感興趣的同學(xué)可以研究一下Sun JDK 6引入的Biased Locking機制。
此外蹦浦,執(zhí)行引擎也必須保證線程安全性扭吁,因而JMM(Java Memory Model)也是由執(zhí)行引擎確保的。
五白筹、Java 內(nèi)存模型(JMM)
Java內(nèi)存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問規(guī)則智末,以及在JVM中將變量存儲到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細節(jié)。
多個線程同時對主內(nèi)存的一個共享變量進行讀取和修改時徒河,首先會讀取這個變量到自己的工作內(nèi)存中成為一個副本系馆,對這個副本進行改動之后,再更新回主內(nèi)存中變量所在的地方顽照。
(由于CPU時間片是以線程為最小單位由蘑,所以這里的工作內(nèi)存實際上就是指的物理緩存,CPU運算時獲取數(shù)據(jù)的地方代兵;而主內(nèi)存也就是指的是內(nèi)存尼酿,也就是原始的共享變量存放的位置)
JMM 關(guān)鍵技術(shù)點都是圍繞多線程的原子性、可見性植影、有序性來建立的裳擎。
- 原子性:原子性是指一個操作是不可中斷的。即使是在多個線程一起執(zhí)行的時候思币,一個操作一旦開始鹿响,就不會被其他線程干擾。
- 可見性:可見性是指當(dāng)一個線程修改了某一個共享變量的值谷饿,其他線程是否能夠立即知道這個修改惶我。在串行程序中是不存在可見性的問題,但在多線程場景就存在比較多的問題博投。
- 有序性:按先后順序執(zhí)行绸贡。有序性問題的原因是因為程序在執(zhí)行時,可能會進行指令重排毅哗,重排后的指令與原指令的順序未必一致听怕。
- 所有的變量都存儲在主內(nèi)存中
- 每個線程都有自己獨立的工作內(nèi)存,里面保存該線程使用到的變量的副本(主內(nèi)存中該變量的一份拷貝)
- 兩條規(guī)定
- 線程對共享變量的所有操作都必須在自己的工作內(nèi)存中進行黎做,不能直接從主內(nèi)存中讀寫
- 不同線程之間無法直接訪問其他線程工作內(nèi)存中的變量叉跛,線程間變量值的傳遞需要功過主內(nèi)存來完成。
- 共享變量可見性實現(xiàn)的原理
1. 可見性
一個線程對共享變量值的修改蒸殿,能夠及時地被其他線程看到。
線程1對共享變量的修改要想被線程2及時看到,必須要經(jīng)過如下的兩個步驟:
- 把工作內(nèi)存1中更新過的共享變量刷新到主內(nèi)存中
- 把內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中
1.1 可見性分析
導(dǎo)致共享變量在線程間不可見的原因:
- 線程的交叉執(zhí)行
- 重排序結(jié)合線程交叉執(zhí)行
- 共享變量更新后的值沒有在工作內(nèi)存與主內(nèi)存間及時更新
1.2 synchronized實現(xiàn)可見性
- 線程解鎖前宏所,必須把共享變量的最新值刷新到主內(nèi)存中酥艳。
- 線程加鎖時,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要的是同一把鎖)
這兩點結(jié)合起來爬骤,就可以保證線程解鎖前對共享變量的修改在下次加鎖時對其他的線程可見充石,也就保證了線程之間共享變量的可見性。
1.3 線程執(zhí)行互斥代碼的過程:
- 獲得互斥鎖
- 清空工作內(nèi)存
- 從主內(nèi)存拷貝最新副本到工作內(nèi)存中
- 執(zhí)行代碼
- 將更改過后的共享變量的值刷新到主內(nèi)存中去霞玄。
- 釋放互斥鎖
2. 重排序
重排序:代碼書寫的順序與實際執(zhí)行的順序不同骤铃,指令重排序是編譯器或處理器為了提供程序的性能而做的優(yōu)化。
指令重排能保證串行語義一致坷剧,但沒有義務(wù)保證多線程間的語義也一致惰爬。
之所以存在指令重排完全是為了提高性能。
問題:為什么指令重排可以提高性能呢惫企?
減少執(zhí)行流水線中斷撕瞧,從而提高了 CPU 處理性能。
分類:
- 編譯器優(yōu)化的重排序(編譯器優(yōu)化)
- 指令級并行重排序(處理器優(yōu)化)
- 內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)
2.1 as-if-serial
無論如何重排序狞尔,程序執(zhí)行的結(jié)果應(yīng)該和代碼順尋執(zhí)行的結(jié)果一致(Java編譯器丛版、運行時和處理器都會保證Java在單線程下遵循as-if-serial語義),重排序不會給單線程帶來內(nèi)存可見性問題偏序。
int num1=1;//第一行
int num2=2;//第二行
int sum=num1+num;//第三行
- 單線程:第一行和第二行可以重排序页畦,但第三行不行
- 重排序不會給單線程帶來內(nèi)存可見性問題
- 多線程中程序交錯執(zhí)行時,重排序可能會照成內(nèi)存可見性問題
指令重排是基于以下原則之上
- 程序順序原則:一個線程內(nèi)保證語義的串行性
- volatile 規(guī)則:volatile 變量的寫研儒,先發(fā)生于讀豫缨,保證了 volatile 變量的可見性
- 鎖規(guī)則:解鎖必然發(fā)生在隨后的加鎖之前
- 傳遞性:A 先于 B,B 先于 C殉摔,那么 A 必然先于 C
- 線程的 start() 方法先于他的每一個動作
- 線程的所有操作先于線程的終結(jié)
- 線程的中斷先于被中斷線程的代碼
- 對象的構(gòu)造函數(shù)執(zhí)行州胳、結(jié)束先于 finalize() 方法
3. volatile實現(xiàn)可見性
3.1 volatile 關(guān)鍵字
- 能夠保證volatile變量的可見性
- 不能保證volatile變量的原子性
3.2 volatile如何實現(xiàn)內(nèi)存可見性:
深入來說:通過加入內(nèi)存屏障和禁止重排序優(yōu)化來實現(xiàn)的。
通俗的講:volatile 變量在每次被線程訪問時逸月,都強迫從主內(nèi)存中重讀該變量的值栓撞,而當(dāng)變量發(fā)生變化時,又強迫線程將最新的值刷新到主內(nèi)存碗硬。這樣任何時刻瓤湘,不同的線程總能看到該變量的最新的值。
- 對 volatile 變量執(zhí)行寫操作時恩尾,會在寫操作后加入一條 store 屏障指令
- store 指令會在寫操作后把最新的值強制刷新到主內(nèi)存中弛说。同時還會禁止 cpu 對代碼進行重排序優(yōu)化。這樣就保證了值在主內(nèi)存中是最新的翰意。
- 對 volatile 變量執(zhí)行讀操作時木人,會在讀操作前加入一條 load 屏障指令
- load 指令會在讀操作前把工作內(nèi)存緩存中的值清空后信柿,再從主內(nèi)存中讀取最新的值。
線程寫volatile變量的過程:
- 改變線程工作內(nèi)存中volatile變量副本的值醒第。
- 將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存渔嚷。
線程讀volatile變量的過程:
- 從主內(nèi)存中讀取最新的volatile變量的值到工作內(nèi)存中。
- 從工作內(nèi)存中讀取volatile變量的副本稠曼。
3.3 volatile 不能保證原子性
private int number=0;//原子性操作
number++;//不是原子性操作
從 Load 到store 到內(nèi)存屏障形病,一共4步,其中最后一步j(luò)vm讓這個最新的變量的值在所有線程可見霞幅,也就是最后一步讓所有的CPU內(nèi)核都獲得了最新的值漠吻,但中間的幾步(從Load到Store)是不安全的,中間如果其他的 CPU 修改了值將會丟失司恳。
為 volatile 變量賦值的場景途乃,不要存在依賴于 volatile 變量情況。
比如:
public class Wrongsingleton {
private static volatile Wrongsingleton _instance = null;
private wrongsingleton() {}
public static wrongsingleton getInstance() {
if (_instance == null) {
// 可能執(zhí)行下面的語句的時候抵赢,其他的線程已經(jīng)執(zhí)行 new 操作了
_instance = new Wrongsingleton();
}
return _instance;
}
}
3.4 保證方法操作的原子性
解決方案:
- 使用synchronized關(guān)鍵字
- 使用ReentrantLock(java.until.concurrent.locks包下)
- 使用AtomicInterger(vava,util.concurrent.atomic包下)
3.5 volatile 適用場景
要在多線程總安全的使用volatile變量欺劳,必須同時滿足:
- 對變量的寫入操作不依賴其當(dāng)前值
- 不滿足:number++、count=count*5
- 滿足:boolean變量铅鲤、記錄溫度變化的變量等
- 該變量沒有包含在具有其他變量的不變式中
- 不滿足:不變式 low < up
4. 內(nèi)存屏障
內(nèi)存屏障(memory barrier)是一個 CPU 指令划提。基本上邢享,它是這樣一條指令: a) 確保一些特定操作執(zhí)行的順序鹏往; b) 影響一些數(shù)據(jù)的可見性(可能是某些指令執(zhí)行后的結(jié)果)。編譯器和 CPU 可以在保證輸出結(jié)果一樣的情況下對指令重排序骇塘,使性能得到優(yōu)化伊履。插入一個內(nèi)存屏障,相當(dāng)于告訴 CPU 和編譯器先于這個命令的必須先執(zhí)行款违,后于這個命令的必須后執(zhí)行唐瀑。內(nèi)存屏障另一個作用是強制更新一次不同 CPU 之間的緩存。例如插爹,一個寫屏障會把這個屏障前寫入的數(shù)據(jù)刷新到緩存哄辣,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值,而不用考慮到底是被哪個 CPU 核心或者哪顆 CPU 執(zhí)行的赠尾。
4.1 Store Barrier
Store屏障力穗,是x86的”sfence“指令,強制所有在store屏障指令之前的store指令气嫁,都在該store屏障指令執(zhí)行之前被執(zhí)行当窗,并把store緩沖區(qū)的數(shù)據(jù)都刷到CPU緩存。這會使得程序狀態(tài)對其它CPU可見寸宵,這樣其它CPU可以根據(jù)需要介入崖面。一個實際的好例子是Disruptor中的BatchEventProcessor元咙。當(dāng)序列Sequence被一個消費者更新時,其它消費者(Consumers)和生產(chǎn)者(Producers)知道該消費者的進度嘶朱,因此可以采取合適的動作蛾坯。所以屏障之前發(fā)生的內(nèi)存更新都可見了光酣。
4.2 Load Barrier
Load屏障疏遏,是x86上的”ifence“指令,強制所有在load屏障指令之后的load指令救军,都在該load屏障指令執(zhí)行之后被執(zhí)行财异,并且一直等到load緩沖區(qū)被該CPU讀完才能執(zhí)行之后的load指令。這使得從其它CPU暴露出來的程序狀態(tài)對該CPU可見唱遭,這之后CPU可以進行后續(xù)處理戳寸。一個好例子是上面的BatchEventProcessor的sequence對象是放在屏障后被生產(chǎn)者或消費者使用。
4.3 Full Barrier
Full屏障拷泽,是x86上的”mfence“指令疫鹊,復(fù)合了load和save屏障的功能。
5. 對 64 位(long司致、double)變量的讀寫可能不是原子操作
Java 內(nèi)存模型允許JVM將沒有被 volatile 修飾的 64 位數(shù)據(jù)類型讀寫操作劃分為兩次 32 位的讀寫操作來進行拆吆,這就會導(dǎo)致有可能讀取到“半個變量”的情況,解決辦法就是加上 volatile 關(guān)鍵字脂矫。
6. final 也可以保證線程之間內(nèi)存變量的可見性
Final 變量在并發(fā)當(dāng)中枣耀,原理是通過禁止 cpu 的指令集重排序,來提供現(xiàn)成的可見性庭再,來保證對象的安全發(fā)布捞奕,防止對象引用被其他線程在對象被完全構(gòu)造完成前拿到并使用。
與鎖和 volatile 相比較拄轻,對 final 域的讀和寫更像是普通的變量訪問颅围。對于 final 域,編譯器和處理器要遵守兩個重排序規(guī)則:
在構(gòu)造函數(shù)內(nèi)對一個 final 域的寫入恨搓,與隨后把這個被構(gòu)造對象的引用賦值給一個引用變量院促,這兩個操作之間不能重排序。
初次讀一個包含 final 域的對象的引用奶卓,與隨后初次讀這個 final 域一疯,這兩個操作之間不能重排序。
六夺姑、面試題:
1. 代碼在 Windows 上面被編譯為 class 文件墩邀,到其他系統(tǒng)下可以直接運行嗎?
可以盏浙,java文件一旦被編譯轉(zhuǎn)換為 Java 字節(jié)碼文件眉睹,也就是 class 文件之后荔茬,就運行被其他平臺所支持和運行。
2. 實現(xiàn) Java 虛擬機的方式有哪些竹海?
軟件方式和硬件方式慕蔚,常見的方式是軟件方式。
3. JVM 由哪些組成斋配?
類加載器子系統(tǒng)孔飒、運行時數(shù)據(jù)區(qū)(內(nèi)存空間)、執(zhí)行引擎以及與本地方法接口艰争。
4. 運行時數(shù)據(jù)區(qū)有哪些組成坏瞄?
由方法區(qū)、堆甩卓、Java棧鸠匀、PC寄存器、本地方法棧組成逾柿,其中內(nèi)存空間中方法區(qū)和堆是所有Java線程共享的缀棍,而Java棧、本地方法棧机错、PC寄存器則由每個線程私有爬范。
4個部分組成:
- PC寄存器/程序計數(shù)器:線程私有;記錄指令執(zhí)行的位置毡熏,這里不會出現(xiàn) OutOfMemoryError
- Java棧:線程私有坦敌,生命周期和線程一致,存儲方法參數(shù)痢法、局部變量狱窘、中間運算結(jié)果,會出現(xiàn)異常:如果線程請求的棧深度大于虛擬機所允許的深度财搁,將拋出StackOverflowError異常蘸炸;如果虛擬機可以動態(tài)擴展,如果擴展時無法申請到足夠的內(nèi)存尖奔,就會拋出OutOfMemoryError異常搭儒。
- 本地方法棧:存儲了本地方法調(diào)用的狀態(tài)。區(qū)別不過是Java棧為JVM執(zhí)行Java方法服務(wù)提茁,而本地方法棧為JVM執(zhí)行Native方法服務(wù)淹禾。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。在Sun JDK中茴扁,本地方法棧和Java棧是同一個铃岔。
- 堆:堆是JVM所管理的內(nèi)存中最大的一塊,是被所有Java線程鎖共享的峭火,不是線程安全的毁习,也會有 StackOverflowError和OutOfMemoryError異常
- 方法區(qū):線程共享智嚷;類型信息和類的靜態(tài)變量都存儲在方法區(qū)中。
5. 什么操作會導(dǎo)致程序的跨平臺性纺且?
在程序中調(diào)用本地方法會不在具有跨平臺性盏道。
6. Java 是通過什么方式實現(xiàn) Java 語言的本地方法調(diào)用?
使用 JNI(Java Native Interface)方式實現(xiàn)载碌。
7. 啟動類加載器是通過什么方式初始化加載的猜嘱?
它不是Java類,因此它不需要被別人加載恐仑,它嵌套在Java虛擬機內(nèi)核里面泉坐,也就是JVM啟動的時候Bootstrap就已經(jīng)啟動,它是用C++寫的二進制代碼(不是字節(jié)碼)
8. sun.misc.Launcher$ExtClassLoader 與 java.lang.ClassLoader 關(guān)系裳仆?
除了 啟動類加載器 外其他都是繼承自 java.lang.ClassLoader。
9. 其他類加載器如何初始化孤钦?
其他的類加載器都是通過 sun.misc.Launcher 類下面的靜態(tài)內(nèi)部類實現(xiàn)歧斟,拓展類加載器就是 ExtClassLoader,應(yīng)用加載器就是 AppClassLoader偏形。
10. 說一下類加載的流程是怎么樣的静袖?
裝載、鏈接(校驗俊扭、準備队橙、解析)、初始化萨惑。
- 裝載:通過類的全限定名和ClassLoader加載類捐康,主要是將指定的.class文件加載至JVM
- 校驗:校驗.class文件的正確性,確保該文件是符合規(guī)范定義的庸蔼,并且適合當(dāng)前JVM使用
- 準備:為類分配內(nèi)存解总,同時初始化類中的靜態(tài)變量賦值為默認值
- 解析:主要是把類的常量池中的符號引用解析為直接引用,這一步可以在用到相應(yīng)的引用時再解析
- 初始化:初始化類中的靜態(tài)變量姐仅,并執(zhí)行類中的static代碼花枫、構(gòu)造函數(shù)
11. 講一下類加載的過程采用了什么模型?
雙親委派模型掏膏。
某個特定的類加載器在接到加載類的請求時劳翰,首先將加載任務(wù)委托給父類加載器,依次遞歸馒疹,如果父類加載器可以完成類加載任務(wù)佳簸,就成功返回;只有父類加載器無法完成此加載任務(wù)時行冰,才自己去加載溺蕉。
當(dāng) Java 虛擬機要加載一個類時伶丐,到底派出哪個類加載器去加載呢?
- 首先當(dāng)前線程的類加載器去加載線程中的第一個類(假設(shè)為類A)疯特。
注:當(dāng)前線程的類加載器可以通過Thread類的getContextClassLoader()獲得哗魂,也可以通過setContextClassLoader()自己設(shè)置類加載器。 - 如果類A中引用了類B漓雅,Java虛擬機將使用加載類A的類加載器去加載類B录别。
- 還可以直接調(diào)用ClassLoader.loadClass()方法來指定某個類加載器去加載某個類。
12. 雙親委派的意義是什么邻吞?
委托機制的意義 — 防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼
比如兩個類A和類B都要加載System類:
- 如果不用委托而是自己加載自己的组题,那么類A就會加載一份System字節(jié)碼,然后類B又會加載一份System字節(jié)碼抱冷,這樣內(nèi)存中就出現(xiàn)了兩份System字節(jié)碼崔列。
- 如果使用委托機制,會遞歸的向父類查找旺遮,也就是首選用Bootstrap嘗試加載赵讯,如果找不到再向下。這里的System就能在Bootstrap中找到然后加載耿眉,如果此時類B也要加載System边翼,也從Bootstrap開始,此時Bootstrap發(fā)現(xiàn)已經(jīng)加載過了System那么直接返回內(nèi)存中的System即可而不需要重新加載鸣剪,這樣內(nèi)存中就只有一份System的字節(jié)碼了组底。
13. 能不能自己寫個類叫java.lang.System?
通常不可以筐骇,但可以采取另類方法達到這個需求债鸡。
為了不讓我們寫System類,類加載采用委托機制拥褂,這樣可以保證爸爸們優(yōu)先娘锁,爸爸們能找到的類,兒子就沒有機會加載饺鹃。而System類是Bootstrap加載器加載的莫秆,就算自己重寫,也總是使用Java系統(tǒng)提供的System悔详,自己寫的System類根本沒有機會得到加載镊屎。
但是,我們可以自己定義一個類加載器來達到這個目的茄螃,為了避免雙親委托機制缝驳,這個類加載器也必須是特殊的。由于系統(tǒng)自帶的三個類加載器都加載特定目錄下的類,如果我們自己的類加載器放在一個特殊的目錄用狱,那么系統(tǒng)的加載器就無法加載运怖,也就是最終還是由我們自己的加載器加載。
14. JVM 中何時會觸發(fā)類加載的行為夏伊?
- 通過new關(guān)鍵字摇展、反射、clone溺忧、反序列化機制實例化對象時咏连。
- 調(diào)用類的靜態(tài)方法時。
- 使用類的靜態(tài)字段或?qū)ζ滟x值時鲁森。
- 通過反射調(diào)用類的方法時祟滴。
- 初始化該類的子類時(初始化子類前其父類必須已經(jīng)被初始化)。
- JVM啟動時被標記為啟動類的類(簡單理解為具有main方法的類)歌溉。
15. 常量池的作用是什么垄懂?
常量池本身是方法區(qū)中的一個數(shù)據(jù)結(jié)構(gòu)。常量池中存儲了如字符串研底、final變量值埠偿、類名和方法名常量。常量池在編譯期間就被確定榜晦,并保存在已編譯的.class文件中。一般分為兩類:字面量和應(yīng)用量羽圃。字面量就是字符串乾胶、final變量等。類名和方法名屬于引用量朽寞。
16. 解釋一下 Java內(nèi)存模型识窿?
Java內(nèi)存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問規(guī)則,以及在JVM中將變量存儲到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細節(jié)脑融。
17. 什么叫可見性喻频?
一個線程對共享變量值的修改,能夠及時地被其他線程看到肘迎。
18. 什么叫有序性甥温?
按先后順序執(zhí)行。有序性問題的原因是因為程序在執(zhí)行時妓布,可能會進行指令重排姻蚓,重排后的指令與原指令的順序未必一致。
19. 什么叫原子性匣沼?
原子性是指一個操作是不可中斷的狰挡。即使是在多個線程一起執(zhí)行的時候,一個操作一旦開始,就不會被其他線程干擾加叁。
20. 導(dǎo)致共享變量在線程間不可見的原因倦沧?
- 線程的交叉執(zhí)行
- 重排序結(jié)合線程交叉執(zhí)行
- 共享變量更新后的值沒有在工作內(nèi)存與主內(nèi)存間及時更新
21. synchronize 如何保證有序性?
- 線程解鎖前它匕,必須把共享變量的最新值刷新到主內(nèi)存中展融。
- 線程加鎖時,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要的是同一把鎖)
22. 線程執(zhí)行互斥代碼的過程超凳?
- 獲得互斥鎖
- 清空工作內(nèi)存
- 從主內(nèi)存拷貝最新副本到工作內(nèi)存中
- 執(zhí)行代碼
- 將更改過后的共享變量的值刷新到主內(nèi)存中去愈污。
- 釋放互斥鎖
23. 指令重排序的價值是什么段多?
指令重排序是編譯器或處理器為了提供程序的性能而做的優(yōu)化苏遥,
24. 為什么指令重排序可以提升性能?
減少執(zhí)行流水線中斷脓魏,從而提高了 CPU 處理性能创夜。
25. 重排序類別杭跪?
- 編譯器優(yōu)化的重排序(編譯器優(yōu)化)
- 指令級并行重排序(處理器優(yōu)化)
- 內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)
26. 重排序規(guī)則?
指令重排是基于以下原則之上
- 程序順序原則:一個線程內(nèi)保證語義的串行性
- volatile 規(guī)則:volatile 變量的寫驰吓,先發(fā)生于讀涧尿,保證了 volatile 變量的可見性
- 鎖規(guī)則:解鎖必然發(fā)生在隨后的加鎖之前
- 傳遞性:A 先于 B,B 先于 C檬贰,那么 A 必然先于 C
- 線程的 start() 方法先于他的每一個動作
- 線程的所有操作先于線程的終結(jié)
- 線程的中斷先于被中斷線程的代碼
- 對象的構(gòu)造函數(shù)執(zhí)行姑廉、結(jié)束先于 finalize() 方法
27. volatile如何實現(xiàn)內(nèi)存可見性?
通過加入內(nèi)存屏障和禁止重排序優(yōu)化來實現(xiàn)的翁涤。
volatile 變量在每次被線程訪問時桥言,都強迫從主內(nèi)存中重讀該變量的值,而當(dāng)變量發(fā)生變化時葵礼,又強迫線程將最新的值刷新到主內(nèi)存号阿。這樣任何時刻,不同的線程總能看到該變量的最新的值鸳粉。
28. 什么是內(nèi)存屏障扔涧?
內(nèi)存屏障(memory barrier)是一個 CPU 指令〗焯福基本上枯夜,它是這樣一條指令: a) 確保一些特定操作執(zhí)行的順序; b) 影響一些數(shù)據(jù)的可見性(可能是某些指令執(zhí)行后的結(jié)果)疼约。
它們通過確保從另一個CPU來看屏障的兩邊的所有指令都是正確的程序順序卤档,而保持程序順序的外部可見性;其次它們可以實現(xiàn)內(nèi)存數(shù)據(jù)可見性程剥,確保內(nèi)存數(shù)據(jù)會同步到CPU緩存子系統(tǒng)劝枣。
29. 為什么內(nèi)存屏障能保證可見性和禁止指令重排序汤踏?
編譯器和 CPU 可以在保證輸出結(jié)果一樣的情況下對指令重排序,使性能得到優(yōu)化舔腾。插入一個內(nèi)存屏障溪胶,相當(dāng)于告訴 CPU 和編譯器先于這個命令的必須先執(zhí)行,后于這個命令的必須后執(zhí)行稳诚。內(nèi)存屏障另一個作用是強制更新一次不同 CPU 之間的緩存哗脖。
Java內(nèi)存模型中volatile變量在寫操作之后會插入一個store屏障,在讀操作之前會插入一個load屏障扳还。一個類的final字段會在初始化后插入一個store屏障才避,來確保final字段在構(gòu)造函數(shù)初始化完成并可被使用時可見。
30. 重排序是怎么做到執(zhí)行性能優(yōu)化的氨距?
編譯器優(yōu)化重排序:在不影響結(jié)果的前提下桑逝,對執(zhí)行順序進行優(yōu)化
指令級并行重排序:使用CPU核內(nèi)部包含的多個執(zhí)行單元,組合進行算術(shù)運算俏让,提高計算并行度和執(zhí)行效率楞遏,但這個過程中可能就會存在程序順序的不確定性。
內(nèi)存系統(tǒng)重排序:為了盡可能地避免處理器訪問主內(nèi)存的時間開銷首昔,處理器大多會利用緩存(cache)以提高性能寡喝。
在這種模型下會存在一個現(xiàn)象,即緩存中的數(shù)據(jù)與主內(nèi)存的數(shù)據(jù)并不是實時同步的勒奇,各CPU(或CPU核心)間緩存的數(shù)據(jù)也不是實時同步的预鬓。這導(dǎo)致在同一個時間點,各CPU所看到同一內(nèi)存地址的數(shù)據(jù)的值可能是不一致的赊颠。從程序的視角來看珊皿,就是在同一個時間點,各個線程所看到的共享變量的值可能是不一致的巨税。
31. CPU 讀取緩存優(yōu)先級?
寄存器》高速緩存》內(nèi)存
讀者福利:點擊下方傳送門粉臊,即可免費領(lǐng)取筆者整理的Java后端面試題及Java架構(gòu)師成長路線圖2萏怼!扼仲!