阿里巴巴最新JVM面試解析——運(yùn)行結(jié)構(gòu)(附測(cè)試題)

前言

這篇主要了解 JVM 內(nèi)在的運(yùn)行結(jié)構(gòu)是怎么樣的。

一、虛擬機(jī)的意義

Java 作為一門(mén)高級(jí)程序語(yǔ)言,它的語(yǔ)法非常復(fù)雜,抽象程度也很高。因此,直接在硬件上運(yùn)行這種復(fù)雜的程序并不現(xiàn)實(shí)。所以呢,在運(yùn)行 Java 程序之前,我們需要對(duì)其進(jìn)行一番轉(zhuǎn)換。

Java 虛擬機(jī)可以由硬件實(shí)現(xiàn),但更為常見(jiàn)的是在各個(gè)現(xiàn)有平臺(tái)(如 Windows_x64、Linux_aarch64)上提供軟件實(shí)現(xiàn)。這么做的意義在于,一旦一個(gè)程序被轉(zhuǎn)換成 Java 字節(jié)碼,那么它便可以在不同平臺(tái)上的虛擬機(jī)實(shí)現(xiàn)里運(yùn)行。這也就是我們經(jīng)常說(shuō)的“一次編寫(xiě),到處運(yùn)行”。

虛擬機(jī)的另外一個(gè)好處是它帶來(lái)了一個(gè)托管環(huán)境(Managed Runtime)。這個(gè)托管環(huán)境能夠代替我們處理一些代碼中冗長(zhǎng)而且容易出錯(cuò)的部分。其中最廣為人知的當(dāng)屬自動(dòng)內(nèi)存管理與垃圾回收,這部分內(nèi)容甚至催生了一波垃圾回收調(diào)優(yōu)的業(yè)務(wù)。

使用虛擬機(jī)還有一個(gè)好處,可以在編譯的過(guò)程中對(duì)代碼進(jìn)行優(yōu)化,對(duì)代碼進(jìn)行精簡(jiǎn),以提高執(zhí)行效率。

虛擬機(jī)結(jié)構(gòu)

可以看出,JVM主要由類(lèi)加載器子系統(tǒng)、運(yùn)行時(shí)數(shù)據(jù)區(qū)(內(nèi)存空間)、執(zhí)行引擎以及與本地方法接口等組成。其中運(yùn)行時(shí)數(shù)據(jù)區(qū)又由方法區(qū)、堆、Java棧、PC寄存器、本地方法棧組成。

從上圖中還可以看出,在內(nèi)存空間中方法區(qū)和堆是所有Java線程共享的,而 Java棧、本地方法棧、PC寄存器則由每個(gè)線程私有,這會(huì)引出一些問(wèn)題,后文會(huì)進(jìn)行具體討論。

眾所周知,Java語(yǔ)言具有跨平臺(tái)的特性,這也是由JVM來(lái)實(shí)現(xiàn)的。更準(zhǔn)確地說(shuō),是Sun利用JVM在不同平臺(tái)上的實(shí)現(xiàn)幫我們把平臺(tái)相關(guān)性的問(wèn)題給解決了,這就好比是HTML語(yǔ)言可以在不同廠商的瀏覽器上呈現(xiàn)元素(雖然某些瀏覽器在對(duì)W3C標(biāo)準(zhǔn)的支持上還有一些問(wèn)題)。同時(shí),Java語(yǔ)言支持通過(guò)JNI(Java Native Interface)來(lái)實(shí)現(xiàn)本地方法的調(diào)用,但是需要注意到,如果你在Java程序用調(diào)用了本地方法,那么你的程序就很可能不再具有跨平臺(tái)性,即本地方法會(huì)破壞平臺(tái)無(wú)關(guān)性。

二、ClassLoader的分類(lèi)

  • 啟動(dòng)類(lèi)加載器(BootStrap Class Loader):?jiǎn)?dòng)類(lèi)加載器主要加載的是JVM自身需要的類(lèi),這個(gè)類(lèi)加載使用C++語(yǔ)言實(shí)現(xiàn)的,是虛擬機(jī)自身的一部分,它負(fù)責(zé)將 <JAVA_HOME>/lib 路徑下的核心類(lèi)庫(kù)或 -Xbootclasspath 參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意必由于虛擬機(jī)是按照文件名識(shí)別加載jar包的,如 rt.jar,如果文件名不被虛擬機(jī)識(shí)別,即使把jar包丟到lib目錄下也是沒(méi)有作用的(出于安全考慮,Bootstrap啟動(dòng)類(lèi)加載器只加載包名為java、javax、sun等開(kāi)頭的類(lèi)),在 Sun JDK中,這個(gè)類(lèi)加載器是由 C++ 實(shí)現(xiàn)的,并且在 Java 語(yǔ)言中無(wú)法獲得它的引用。
  • 擴(kuò)展類(lèi)加載器(Extension Class Loader):擴(kuò)展類(lèi)加載器是指Sun公司(已被Oracle收購(gòu))實(shí)現(xiàn)的sun.misc.Launcher$ExtClassLoader 類(lèi),由Java語(yǔ)言實(shí)現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類(lèi),它負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量 -Djava.ext.dir 指定位路徑中的類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用標(biāo)準(zhǔn)擴(kuò)展類(lèi)加載器。
  • 系統(tǒng)類(lèi)加載器(System Class Loader):也稱應(yīng)用程序加載器是指 Sun公司實(shí)現(xiàn)的sun.misc.Launcher$AppClassLoader。它負(fù)責(zé)加載系統(tǒng)類(lèi)路徑j(luò)ava -classpath或 -Djava.class.path 指定路徑下的類(lèi)庫(kù),也就是我們經(jīng)常用到的classpath路徑,開(kāi)發(fā)者可以直接使用系統(tǒng)類(lèi)加載器,一般情況下該類(lèi)加載是程序中默認(rèn)的類(lèi)加載器,通過(guò)ClassLoader#getSystemClassLoader()方法可以獲取到該類(lèi)加載器。通常我們自己寫(xiě)的Java類(lèi)也是由該ClassLoader加載。在Sun JDK中,系統(tǒng)類(lèi)加載器的名字叫 AppClassLoader。
  • 用戶自定義類(lèi)加載器(User Defined Class Loader):由用戶自定義類(lèi)的加載規(guī)則,可以手動(dòng)控制加載過(guò)程中的步驟。

1. ClassLoader的工作原理

類(lèi)加載分為裝載、鏈接、初始化三步。

ClassLoader loader = TestClassLoader.class.getClassLoader();

System.out.println(loader.toString());

System.out.println(loader.getParent().toString());

System.out.println(loader.getParent().getParent());

1.1裝載

通過(guò)類(lèi)的全限定名和ClassLoader加載類(lèi),主要是將指定的.class文件加載至JVM。當(dāng)類(lèi)被加載以后,在JVM內(nèi)部就以“類(lèi)的全限定名+ClassLoader實(shí)例ID”來(lái)標(biāo)明類(lèi)。

在內(nèi)存中,ClassLoader實(shí)例和類(lèi)的實(shí)例都位于堆中,它們的類(lèi)信息都位于方法區(qū)。

裝載過(guò)程采用了一種被稱為“雙親委派模型(Parent Delegation Model)”的方式,當(dāng)一個(gè)ClassLoader要加載類(lèi)時(shí),它會(huì)先請(qǐng)求它的雙親ClassLoader(其實(shí)這里只有兩個(gè)ClassLoader,所以稱為父ClassLoader可能更容易理解)加載類(lèi),而它的雙親ClassLoader會(huì)繼續(xù)把加載請(qǐng)求提交再上一級(jí)的ClassLoader,直到啟動(dòng)類(lèi)加載器。只有其雙親ClassLoader無(wú)法加載指定的類(lèi)時(shí),它才會(huì)自己加載類(lèi)。

雙親委派模型是JVM的第一道安全防線,它保證了類(lèi)的安全加載,這里同時(shí)依賴了類(lèi)加載器隔離的原理:不同類(lèi)加載器加載的類(lèi)之間是無(wú)法直接交互的,即使是同一個(gè)類(lèi),被不同的ClassLoader加載,它們也無(wú)法感知到彼此的存在。這樣即使有惡意的類(lèi)冒充自己在核心包(例如java.lang)下,由于它無(wú)法被啟動(dòng)類(lèi)加載器加載,也造成不了危害。
由此也可見(jiàn),如果用戶自定義了類(lèi)加載器,那就必須自己保障類(lèi)加載過(guò)程中的安全。

2. 鏈接

鏈接的任務(wù)是把二進(jìn)制的類(lèi)型信息合并到JVM運(yùn)行時(shí)狀態(tài)中去。
鏈接分為以下三步:

  • 驗(yàn)證:校驗(yàn).class文件的正確性,確保該文件是符合規(guī)范定義的,并且適合當(dāng)前JVM使用。
  • 準(zhǔn)備:為類(lèi)分配內(nèi)存,同時(shí)初始化類(lèi)中的靜態(tài)變量賦值為默認(rèn)值。
  • 解析(可選):主要是把類(lèi)的常量池中的符號(hào)引用解析為直接引用,這一步可以在用到相應(yīng)的引用時(shí)再解析。

3. 初始化

初始化類(lèi)中的靜態(tài)變量,并執(zhí)行類(lèi)中的static代碼、構(gòu)造函數(shù)。

JVM規(guī)范嚴(yán)格定義了何時(shí)需要對(duì)類(lèi)進(jìn)行初始化:

  • 通過(guò)new關(guān)鍵字、反射、clone、反序列化機(jī)制實(shí)例化對(duì)象時(shí)。
  • 調(diào)用類(lèi)的靜態(tài)方法時(shí)。
  • 使用類(lèi)的靜態(tài)字段或?qū)ζ滟x值時(shí)。
  • 通過(guò)反射調(diào)用類(lèi)的方法時(shí)。
  • 初始化該類(lèi)的子類(lèi)時(shí)(初始化子類(lèi)前其父類(lèi)必須已經(jīng)被初始化)。
  • JVM啟動(dòng)時(shí)被標(biāo)記為啟動(dòng)類(lèi)的類(lèi)(簡(jiǎn)單理解為具有main方法的類(lèi))。

三、運(yùn)行時(shí)數(shù)據(jù)區(qū)

運(yùn)行時(shí)數(shù)據(jù)區(qū)由方法區(qū)、堆、Java棧、PC寄存器、本地方法棧組成。

1. Java棧(Java Stack)

Java棧的主要任務(wù)是存儲(chǔ)方法參數(shù)、局部變量、中間運(yùn)算結(jié)果,并且提供部分其它模塊工作需要的數(shù)據(jù)。

Java棧總是與線程關(guān)聯(lián)在一起的,每當(dāng)創(chuàng)建一個(gè)線程,JVM就會(huì)為該線程創(chuàng)建對(duì)應(yīng)的Java棧,在這個(gè)Java棧中又會(huì)包含多個(gè)棧幀(Stack Frame),這些棧幀是與每個(gè)方法關(guān)聯(lián)起來(lái)的,每運(yùn)行一個(gè)方法就創(chuàng)建一個(gè)棧幀,每個(gè)棧幀會(huì)含有一些局部變量、操作棧和方法返回值等信息。每當(dāng)一個(gè)方法執(zhí)行完成時(shí),該棧幀就會(huì)彈出棧幀的元素作為這個(gè)方法的返回值,并且清除這個(gè)棧幀,Java棧的棧頂?shù)臈褪钱?dāng)前正在執(zhí)行的活動(dòng)棧,也就是當(dāng)前正在執(zhí)行的方法,PC寄存器也會(huì)指向該地址。只有這個(gè)活動(dòng)的棧幀的本地變量可以被操作棧使用,當(dāng)在這個(gè)棧幀中調(diào)用另外一個(gè)方法時(shí),與之對(duì)應(yīng)的一個(gè)新的棧幀被創(chuàng)建,這個(gè)新創(chuàng)建的棧幀被放到Java棧的棧頂,變?yōu)楫?dāng)前的活動(dòng)棧。同樣現(xiàn)在只有這個(gè)棧的本地變量才能被使用,當(dāng)這個(gè)棧幀中所有指令都完成時(shí),這個(gè)棧幀被移除Java棧,剛才的那個(gè)棧幀變?yōu)榛顒?dòng)棧幀,前面棧幀的返回值變?yōu)檫@個(gè)棧幀的操作棧的一個(gè)操作數(shù)。

由于Java棧是與線程對(duì)應(yīng)起來(lái)的,Java棧數(shù)據(jù)不是線程共有的,所以不需要關(guān)心其數(shù)據(jù)一致性,也不會(huì)存在同步鎖的問(wèn)題。

它分為三部分:局部變量區(qū)、操作數(shù)棧、幀數(shù)據(jù)區(qū)。

局部變量區(qū)

局部變量區(qū)是以字長(zhǎng)為單位的數(shù)組,在這里,byte、short、char類(lèi)型會(huì)被轉(zhuǎn)換成int類(lèi)型存儲(chǔ),除了long和double類(lèi)型占兩個(gè)字長(zhǎng)以外,其余類(lèi)型都只占用一個(gè)字長(zhǎng)。特別地,boolean類(lèi)型在編譯時(shí)會(huì)被轉(zhuǎn)換成int或byte類(lèi)型,boolean數(shù)組會(huì)被當(dāng)做byte類(lèi)型數(shù)組來(lái)處理。局部變量區(qū)也會(huì)包含對(duì)象的引用,包括類(lèi)引用、接口引用以及數(shù)組引用。

局部變量區(qū)包含了方法參數(shù)和局部變量,此外,實(shí)例方法隱含第一個(gè)局部變量this,它指向調(diào)用該方法的對(duì)象引用。對(duì)于對(duì)象,局部變量區(qū)中永遠(yuǎn)只有指向堆的引用。

操作數(shù)棧

操作數(shù)棧也是以字長(zhǎng)為單位的數(shù)組,但是正如其名,它只能進(jìn)行入棧出棧的基本操作。在進(jìn)行計(jì)算時(shí),操作數(shù)被彈出棧,計(jì)算完畢后再入棧。

幀數(shù)據(jù)區(qū)

幀數(shù)據(jù)區(qū)的任務(wù)主要有:
記錄指向類(lèi)的常量池的指針,以便于解析。
幫助方法的正常返回,包括恢復(fù)調(diào)用該方法的棧幀,設(shè)置PC寄存器指向調(diào)用方法對(duì)應(yīng)的下一條指令,把返回值壓入調(diào)用棧幀的操作數(shù)棧中。
記錄異常表,發(fā)生異常時(shí)將控制權(quán)交由對(duì)應(yīng)異常的catch子句,如果沒(méi)有找到對(duì)應(yīng)的catch子句,會(huì)恢復(fù)調(diào)用方法的棧幀并重新拋出異常。

局部變量區(qū)和操作數(shù)棧的大小依照具體方法在編譯時(shí)就已經(jīng)確定。調(diào)用方法時(shí)會(huì)從方法區(qū)中找到對(duì)應(yīng)類(lèi)的類(lèi)型信息,從中得到具體方法的局部變量區(qū)和操作數(shù)棧的大小,依此分配棧幀內(nèi)存,壓入Java棧。

在Java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)可以動(dòng)態(tài)擴(kuò)展,如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常。

2. 本地方法棧(Native Method Stack)

本地方法棧類(lèi)似于Java棧,主要存儲(chǔ)了本地方法調(diào)用的狀態(tài)。區(qū)別不過(guò)是Java棧為JVM執(zhí)行Java方法服務(wù),而本地方法棧為JVM執(zhí)行Native方法服務(wù)。本地方法棧也會(huì)拋出StackOverflowError和OutOfMemoryError異常。在Sun JDK中,本地方法棧和Java棧是同一個(gè)。

3. PC寄存器/程序計(jì)數(shù)器(Program Count Register)

嚴(yán)格來(lái)說(shuō)是一個(gè)數(shù)據(jù)結(jié)構(gòu),用于保存當(dāng)前正在執(zhí)行的程序的內(nèi)存地址,由于Java是支持多線程執(zhí)行的,所以程序執(zhí)行的軌跡不可能一直都是線性執(zhí)行。當(dāng)有多個(gè)線程交叉執(zhí)行時(shí),被中斷的線程的程序當(dāng)前執(zhí)行到哪條內(nèi)存地址必然要保存下來(lái),以便用于被中斷的線程恢復(fù)執(zhí)行時(shí)再按照被中斷時(shí)的指令地址繼續(xù)執(zhí)行下去。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每個(gè)線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各個(gè)線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類(lèi)內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存,這在某種程度上有點(diǎn)類(lèi)似于“ThreadLocal”,是線程安全的。

4. 方法區(qū)(Method Area)

類(lèi)型信息和類(lèi)的靜態(tài)變量都存儲(chǔ)在方法區(qū)中。方法區(qū)中對(duì)于每個(gè)類(lèi)存儲(chǔ)了以下數(shù)據(jù):

  • 類(lèi)及其父類(lèi)的全限定名(java.lang.Object沒(méi)有父類(lèi))
  • 類(lèi)的類(lèi)型(Class or Interface)
  • 訪問(wèn)修飾符(public, abstract, final)
  • 實(shí)現(xiàn)的接口的全限定名的列表
  • 常量池
  • 字段信息
  • 方法信息
  • 靜態(tài)變量
  • ClassLoader引用
  • Class引用

可見(jiàn)類(lèi)的所有信息都存儲(chǔ)在方法區(qū)中。由于方法區(qū)是所有線程共享的,所以必須保證線程安全,舉例來(lái)說(shuō),如果兩個(gè)類(lèi)同時(shí)要加載一個(gè)尚未被加載的類(lèi),那么一個(gè)類(lèi)會(huì)請(qǐng)求它的ClassLoader去加載需要的類(lèi),另一個(gè)類(lèi)只能等待而不會(huì)重復(fù)加載。

常量池本身是方法區(qū)中的一個(gè)數(shù)據(jù)結(jié)構(gòu)。常量池中存儲(chǔ)了如字符串、final變量值、類(lèi)名和方法名常量。常量池在編譯期間就被確定,并保存在已編譯的.class文件中。一般分為兩類(lèi):字面量和應(yīng)用量。字面量就是字符串、final變量等。類(lèi)名和方法名屬于引用量。引用量最常見(jiàn)的是在調(diào)用方法的時(shí)候,根據(jù)方法名找到方法的引用,并以此定為到函數(shù)體進(jìn)行函數(shù)代碼的執(zhí)行。引用量包含:類(lèi)和接口的權(quán)限定名、字段的名稱和描述符,方法的名稱和描述符。

此外為了加快調(diào)用方法的速度,通常還會(huì)為每個(gè)非抽象類(lèi)創(chuàng)建私有的方法表,方法表是一個(gè)數(shù)組,存放了實(shí)例可能被調(diào)用的實(shí)例方法的直接引用。

在Sun JDK中,方法區(qū)對(duì)應(yīng)了持久代(Permanent Generation),默認(rèn)最小值為16MB,最大值為64MB。大小可以通過(guò)參數(shù)來(lái)設(shè)置,可以通過(guò)-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

5. 堆(Heap)

堆是JVM所管理的內(nèi)存中最大的一塊,是被所有Java線程鎖共享的,不是線程安全的,在JVM啟動(dòng)時(shí)創(chuàng)建。

堆用于存儲(chǔ)對(duì)象實(shí)例以及數(shù)組值。堆是存儲(chǔ)Java對(duì)象的地方,這一點(diǎn)Java虛擬機(jī)規(guī)范中描述是:所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配。堆中有指向類(lèi)數(shù)據(jù)的指針,該指針指向了方法區(qū)中對(duì)應(yīng)的類(lèi)型信息。堆中還可能存放了指向方法表的指針。堆是所有線程共享的,所以在進(jìn)行實(shí)例化對(duì)象等操作時(shí),需要解決同步問(wèn)題。此外,堆中的實(shí)例數(shù)據(jù)中還包含了對(duì)象鎖,并且針對(duì)不同的垃圾收集策略,可能存放了引用計(jì)數(shù)或清掃標(biāo)記等數(shù)據(jù)。

在堆的管理上,Sun JDK從1.2版本開(kāi)始引入了分代管理的方式。主要分為新生代、舊生代。分代方式大大改善了垃圾收集的效率。

1. 新生代(New Generation):大多數(shù)情況下新對(duì)象都被分配在新生代中,新生代由Eden Space和兩塊相同大小的Survivor Space組成,后兩者主要用于Minor GC時(shí)的對(duì)象復(fù)制(Minor GC的過(guò)程在此不詳細(xì)討論)。JVM在Eden Space中會(huì)開(kāi)辟一小塊獨(dú)立的TLAB(Thread Local Allocation Buffer)區(qū)域用于更高效的內(nèi)存分配,我們知道在堆上分配內(nèi)存需要鎖定整個(gè)堆,而在TLAB上則不需要,JVM在分配對(duì)象時(shí)會(huì)盡量在TLAB上分配,以提高效率。
2. 老年代(Old Generation/Tenuring Generation):在新生代中存活時(shí)間較久的對(duì)象將會(huì)被轉(zhuǎn)入老年代,老年代進(jìn)行垃圾收集的頻率沒(méi)有新生代高。

四、執(zhí)行引擎

執(zhí)行引擎是JVM執(zhí)行Java字節(jié)碼的核心,執(zhí)行方式主要分為解釋執(zhí)行、編譯執(zhí)行、自適應(yīng)優(yōu)化執(zhí)行、硬件芯片執(zhí)行方式。

JVM的指令集是基于棧而非寄存器的,這樣做的好處在于可以使指令盡可能緊湊,便于快速地在網(wǎng)絡(luò)上傳輸(別忘了Java最初就是為網(wǎng)絡(luò)設(shè)計(jì)的),同時(shí)也很容易適應(yīng)通用寄存器較少的平臺(tái),并且有利于代碼優(yōu)化,由于Java棧和PC寄存器是線程私有的,線程之間無(wú)法互相干涉彼此的棧。每個(gè)線程擁有獨(dú)立的JVM執(zhí)行引擎實(shí)例。

JVM指令由單字節(jié)操作碼和若干操作數(shù)組成。對(duì)于需要操作數(shù)的指令,通常是先把操作數(shù)壓入操作數(shù)棧,即使是對(duì)局部變量賦值,也會(huì)先入棧再賦值。注意這里是“通常”情況,之后會(huì)講到由于優(yōu)化導(dǎo)致的例外。

1. 解釋執(zhí)行

和一些動(dòng)態(tài)語(yǔ)言類(lèi)似,JVM可以解釋執(zhí)行字節(jié)碼。Sun JDK采用了token-threading的方式,感興趣的同學(xué)可以深入了解一下。解釋執(zhí)行中有幾種優(yōu)化方式:

  • 棧頂緩存:將位于操作數(shù)棧頂?shù)闹抵苯泳彺嬖诩拇嫫魃?,?duì)于大部分只需要一個(gè)操作數(shù)的指令而言,就無(wú)需再入棧,可以直接在寄存器上進(jìn)行計(jì)算,結(jié)果壓入操作數(shù)棧。這樣便減少了寄存器和內(nèi)存的交換開(kāi)銷(xiāo)。
  • 部分棧幀共享:被調(diào)用方法可將調(diào)用方法棧幀中的操作數(shù)棧作為自己的局部變量區(qū),這樣在獲取方法參數(shù)時(shí)減少了復(fù)制參數(shù)的開(kāi)銷(xiāo)。
  • 執(zhí)行機(jī)器指令:在一些特殊情況下,JVM會(huì)執(zhí)行機(jī)器指令以提高速度。

2. 編譯執(zhí)行

為了提升執(zhí)行速度,Sun JDK提供了將字節(jié)碼編譯為機(jī)器指令的支持,主要利用了JIT(Just-In-Time)編譯器在運(yùn)行時(shí)進(jìn)行編譯,它會(huì)在第一次執(zhí)行時(shí)編譯字節(jié)碼為機(jī)器碼并緩存,之后就可以重復(fù)利用。Oracle JRockit采用的是完全的編譯執(zhí)行。

3. 自適應(yīng)優(yōu)化執(zhí)行

自適應(yīng)優(yōu)化執(zhí)行的思想是程序中10%20%的代碼占據(jù)了80%90%的執(zhí)行時(shí)間,所以通過(guò)將那少部分代碼編譯為優(yōu)化過(guò)的機(jī)器碼就可以大大提升執(zhí)行效率。自適應(yīng)優(yōu)化的典型代表是Sun的Hotspot VM,正如其名,JVM會(huì)監(jiān)測(cè)代碼的執(zhí)行情況,當(dāng)判斷特定方法是瓶頸或熱點(diǎn)時(shí),將會(huì)啟動(dòng)一個(gè)后臺(tái)線程,把該方法的字節(jié)碼編譯為極度優(yōu)化的、靜態(tài)鏈接的C++代碼。當(dāng)方法不再是熱區(qū)時(shí),則會(huì)取消編譯過(guò)的代碼,重新進(jìn)行解釋執(zhí)行。

自適應(yīng)優(yōu)化不僅通過(guò)利用小部分的編譯時(shí)間獲得大部分的效率提升,而且由于在執(zhí)行過(guò)程中時(shí)刻監(jiān)測(cè),對(duì)內(nèi)聯(lián)代碼等優(yōu)化也起到了很大的作用。由于面向?qū)ο蟮亩鄳B(tài)性,一個(gè)方法可能對(duì)應(yīng)了很多種不同實(shí)現(xiàn),自適應(yīng)優(yōu)化就可以通過(guò)監(jiān)測(cè)只內(nèi)聯(lián)那些用到的代碼,大大減少了內(nèi)聯(lián)函數(shù)的大小。

Sun JDK在編譯上采用了兩種模式:Client和Server模式。前者較為輕量級(jí),占用內(nèi)存較少。后者的優(yōu)化程序更高,占用內(nèi)存更多。

在Server模式中會(huì)進(jìn)行對(duì)象的逃逸分析,即方法中的對(duì)象是否會(huì)在方法外使用,如果被其它方法使用了,則該對(duì)象是逃逸的。對(duì)于非逃逸對(duì)象,JVM會(huì)在棧上直接分配對(duì)象(所以對(duì)象不一定是在堆上分配的),線程獲取對(duì)象會(huì)更加快速,同時(shí)當(dāng)方法返回時(shí),由于棧幀被拋棄,也有利于對(duì)象的垃圾收集。Server模式還會(huì)通過(guò)分析去除一些不必要的同步,感興趣的同學(xué)可以研究一下Sun JDK 6引入的Biased Locking機(jī)制。

此外,執(zhí)行引擎也必須保證線程安全性,因而JMM(Java Memory Model)也是由執(zhí)行引擎確保的。

五、Java 內(nèi)存模型(JMM)

Java內(nèi)存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問(wèn)規(guī)則,以及在JVM中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細(xì)節(jié)。

多個(gè)線程同時(shí)對(duì)主內(nèi)存的一個(gè)共享變量進(jìn)行讀取和修改時(shí),首先會(huì)讀取這個(gè)變量到自己的工作內(nèi)存中成為一個(gè)副本,對(duì)這個(gè)副本進(jìn)行改動(dòng)之后,再更新回主內(nèi)存中變量所在的地方。

(由于CPU時(shí)間片是以線程為最小單位,所以這里的工作內(nèi)存實(shí)際上就是指的物理緩存,CPU運(yùn)算時(shí)獲取數(shù)據(jù)的地方;而主內(nèi)存也就是指的是內(nèi)存,也就是原始的共享變量存放的位置)

JMM 關(guān)鍵技術(shù)點(diǎn)都是圍繞多線程的原子性、可見(jiàn)性、有序性來(lái)建立的。

  • 原子性:原子性是指一個(gè)操作是不可中斷的。即使是在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開(kāi)始,就不會(huì)被其他線程干擾。
  • 可見(jiàn)性:可見(jiàn)性是指當(dāng)一個(gè)線程修改了某一個(gè)共享變量的值,其他線程是否能夠立即知道這個(gè)修改。在串行程序中是不存在可見(jiàn)性的問(wèn)題,但在多線程場(chǎng)景就存在比較多的問(wèn)題。
  • 有序性:按先后順序執(zhí)行。有序性問(wèn)題的原因是因?yàn)槌绦蛟趫?zhí)行時(shí),可能會(huì)進(jìn)行指令重排,重排后的指令與原指令的順序未必一致。
  • 所有的變量都存儲(chǔ)在主內(nèi)存中
  • 每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,里面保存該線程使用到的變量的副本(主內(nèi)存中該變量的一份拷貝)
  • 兩條規(guī)定
    • 線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行,不能直接從主內(nèi)存中讀寫(xiě)
    • 不同線程之間無(wú)法直接訪問(wèn)其他線程工作內(nèi)存中的變量,線程間變量值的傳遞需要功過(guò)主內(nèi)存來(lái)完成。
  • 共享變量可見(jiàn)性實(shí)現(xiàn)的原理

1. 可見(jiàn)性

一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到。

線程1對(duì)共享變量的修改要想被線程2及時(shí)看到,必須要經(jīng)過(guò)如下的兩個(gè)步驟:

  • 把工作內(nèi)存1中更新過(guò)的共享變量刷新到主內(nèi)存中
  • 把內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中

1.1 可見(jiàn)性分析

導(dǎo)致共享變量在線程間不可見(jiàn)的原因:

  • 線程的交叉執(zhí)行
  • 重排序結(jié)合線程交叉執(zhí)行
  • 共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新

1.2 synchronized實(shí)現(xiàn)可見(jiàn)性

  • 線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中。
  • 線程加鎖時(shí),將清空工作內(nèi)存中共享變量的值,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要的是同一把鎖)

這兩點(diǎn)結(jié)合起來(lái),就可以保證線程解鎖前對(duì)共享變量的修改在下次加鎖時(shí)對(duì)其他的線程可見(jiàn),也就保證了線程之間共享變量的可見(jiàn)性。

1.3 線程執(zhí)行互斥代碼的過(guò)程:

  • 獲得互斥鎖
  • 清空工作內(nèi)存
  • 從主內(nèi)存拷貝最新副本到工作內(nèi)存中
  • 執(zhí)行代碼
  • 將更改過(guò)后的共享變量的值刷新到主內(nèi)存中去。
  • 釋放互斥鎖

2. 重排序

重排序:代碼書(shū)寫(xiě)的順序與實(shí)際執(zhí)行的順序不同,指令重排序是編譯器或處理器為了提供程序的性能而做的優(yōu)化。

指令重排能保證串行語(yǔ)義一致,但沒(méi)有義務(wù)保證多線程間的語(yǔ)義也一致。

之所以存在指令重排完全是為了提高性能。

問(wèn)題:為什么指令重排可以提高性能呢?

減少執(zhí)行流水線中斷,從而提高了 CPU 處理性能。

分類(lèi):

  • 編譯器優(yōu)化的重排序(編譯器優(yōu)化)
  • 指令級(jí)并行重排序(處理器優(yōu)化)
  • 內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)

2.1 as-if-serial

無(wú)論如何重排序,程序執(zhí)行的結(jié)果應(yīng)該和代碼順尋執(zhí)行的結(jié)果一致(Java編譯器、運(yùn)行時(shí)和處理器都會(huì)保證Java在單線程下遵循as-if-serial語(yǔ)義),重排序不會(huì)給單線程帶來(lái)內(nèi)存可見(jiàn)性問(wèn)題。

int num1=1;//第一行
int num2=2;//第二行
int sum=num1+num;//第三行
  • 單線程:第一行和第二行可以重排序,但第三行不行
  • 重排序不會(huì)給單線程帶來(lái)內(nèi)存可見(jiàn)性問(wèn)題
  • 多線程中程序交錯(cuò)執(zhí)行時(shí),重排序可能會(huì)照成內(nèi)存可見(jiàn)性問(wèn)題

指令重排是基于以下原則之上

  • 程序順序原則:一個(gè)線程內(nèi)保證語(yǔ)義的串行性
  • volatile 規(guī)則:volatile 變量的寫(xiě),先發(fā)生于讀,保證了 volatile 變量的可見(jiàn)性
  • 鎖規(guī)則:解鎖必然發(fā)生在隨后的加鎖之前
  • 傳遞性:A 先于 B,B 先于 C,那么 A 必然先于 C
  • 線程的 start() 方法先于他的每一個(gè)動(dòng)作
  • 線程的所有操作先于線程的終結(jié)
  • 線程的中斷先于被中斷線程的代碼
  • 對(duì)象的構(gòu)造函數(shù)執(zhí)行、結(jié)束先于 finalize() 方法

3. volatile實(shí)現(xiàn)可見(jiàn)性

3.1 volatile 關(guān)鍵字

  • 能夠保證volatile變量的可見(jiàn)性
  • 不能保證volatile變量的原子性

3.2 volatile如何實(shí)現(xiàn)內(nèi)存可見(jiàn)性:

深入來(lái)說(shuō):通過(guò)加入內(nèi)存屏障和禁止重排序優(yōu)化來(lái)實(shí)現(xiàn)的。

通俗的講:volatile 變量在每次被線程訪問(wèn)時(shí),都強(qiáng)迫從主內(nèi)存中重讀該變量的值,而當(dāng)變量發(fā)生變化時(shí),又強(qiáng)迫線程將最新的值刷新到主內(nèi)存。這樣任何時(shí)刻,不同的線程總能看到該變量的最新的值。

  • 對(duì) volatile 變量執(zhí)行寫(xiě)操作時(shí),會(huì)在寫(xiě)操作后加入一條 store 屏障指令
    • store 指令會(huì)在寫(xiě)操作后把最新的值強(qiáng)制刷新到主內(nèi)存中。同時(shí)還會(huì)禁止 cpu 對(duì)代碼進(jìn)行重排序優(yōu)化。這樣就保證了值在主內(nèi)存中是最新的。
  • 對(duì) volatile 變量執(zhí)行讀操作時(shí),會(huì)在讀操作前加入一條 load 屏障指令
    • load 指令會(huì)在讀操作前把工作內(nèi)存緩存中的值清空后,再?gòu)闹鲀?nèi)存中讀取最新的值。

線程寫(xiě)volatile變量的過(guò)程

  • 改變線程工作內(nèi)存中volatile變量副本的值。
  • 將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存。

線程讀volatile變量的過(guò)程

  • 從主內(nèi)存中讀取最新的volatile變量的值到工作內(nèi)存中。
  • 從工作內(nèi)存中讀取volatile變量的副本。

3.3 volatile 不能保證原子性

private int number=0;//原子性操作
number++;//不是原子性操作

從 Load 到store 到內(nèi)存屏障,一共4步,其中最后一步j(luò)vm讓這個(gè)最新的變量的值在所有線程可見(jiàn),也就是最后一步讓所有的CPU內(nèi)核都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的 CPU 修改了值將會(huì)丟失。

為 volatile 變量賦值的場(chǎng)景,不要存在依賴于 volatile 變量情況。

比如:

public class Wrongsingleton {
    private static volatile Wrongsingleton _instance = null; 
 
    private wrongsingleton() {}
 
    public static wrongsingleton getInstance() {
 
        if (_instance == null) {
          // 可能執(zhí)行下面的語(yǔ)句的時(shí)候,其他的線程已經(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 適用場(chǎng)景

要在多線程總安全的使用volatile變量,必須同時(shí)滿足:

  • 對(duì)變量的寫(xiě)入操作不依賴其當(dāng)前值
    • 不滿足:number++、count=count*5
    • 滿足:boolean變量、記錄溫度變化的變量等
  • 該變量沒(méi)有包含在具有其他變量的不變式中
    • 不滿足:不變式 low < up

4. 內(nèi)存屏障

內(nèi)存屏障(memory barrier)是一個(gè) CPU 指令?;旧?,它是這樣一條指令: a) 確保一些特定操作執(zhí)行的順序; b) 影響一些數(shù)據(jù)的可見(jiàn)性(可能是某些指令執(zhí)行后的結(jié)果)。編譯器和 CPU 可以在保證輸出結(jié)果一樣的情況下對(duì)指令重排序,使性能得到優(yōu)化。插入一個(gè)內(nèi)存屏障,相當(dāng)于告訴 CPU 和編譯器先于這個(gè)命令的必須先執(zhí)行,后于這個(gè)命令的必須后執(zhí)行。內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同 CPU 之間的緩存。例如,一個(gè)寫(xiě)屏障會(huì)把這個(gè)屏障前寫(xiě)入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值,而不用考慮到底是被哪個(gè) CPU 核心或者哪顆 CPU 執(zhí)行的。

4.1 Store Barrier

Store屏障,是x86的”sfence“指令,強(qiáng)制所有在store屏障指令之前的store指令,都在該store屏障指令執(zhí)行之前被執(zhí)行,并把store緩沖區(qū)的數(shù)據(jù)都刷到CPU緩存。這會(huì)使得程序狀態(tài)對(duì)其它CPU可見(jiàn),這樣其它CPU可以根據(jù)需要介入。一個(gè)實(shí)際的好例子是Disruptor中的BatchEventProcessor。當(dāng)序列Sequence被一個(gè)消費(fèi)者更新時(shí),其它消費(fèi)者(Consumers)和生產(chǎn)者(Producers)知道該消費(fèi)者的進(jìn)度,因此可以采取合適的動(dòng)作。所以屏障之前發(fā)生的內(nèi)存更新都可見(jiàn)了。

4.2 Load Barrier

Load屏障,是x86上的”ifence“指令,強(qiáng)制所有在load屏障指令之后的load指令,都在該load屏障指令執(zhí)行之后被執(zhí)行,并且一直等到load緩沖區(qū)被該CPU讀完才能執(zhí)行之后的load指令。這使得從其它CPU暴露出來(lái)的程序狀態(tài)對(duì)該CPU可見(jiàn),這之后CPU可以進(jìn)行后續(xù)處理。一個(gè)好例子是上面的BatchEventProcessor的sequence對(duì)象是放在屏障后被生產(chǎn)者或消費(fèi)者使用。

4.3 Full Barrier

Full屏障,是x86上的”mfence“指令,復(fù)合了load和save屏障的功能。

5. 對(duì) 64 位(long、double)變量的讀寫(xiě)可能不是原子操作

Java 內(nèi)存模型允許JVM將沒(méi)有被 volatile 修飾的 64 位數(shù)據(jù)類(lèi)型讀寫(xiě)操作劃分為兩次 32 位的讀寫(xiě)操作來(lái)進(jìn)行,這就會(huì)導(dǎo)致有可能讀取到“半個(gè)變量”的情況,解決辦法就是加上 volatile 關(guān)鍵字。

6. final 也可以保證線程之間內(nèi)存變量的可見(jiàn)性

Final 變量在并發(fā)當(dāng)中,原理是通過(guò)禁止 cpu 的指令集重排序,來(lái)提供現(xiàn)成的可見(jiàn)性,來(lái)保證對(duì)象的安全發(fā)布,防止對(duì)象引用被其他線程在對(duì)象被完全構(gòu)造完成前拿到并使用。

與鎖和 volatile 相比較,對(duì) final 域的讀和寫(xiě)更像是普通的變量訪問(wèn)。對(duì)于 final 域,編譯器和處理器要遵守兩個(gè)重排序規(guī)則:

在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè) final 域的寫(xiě)入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
初次讀一個(gè)包含 final 域的對(duì)象的引用,與隨后初次讀這個(gè) final 域,這兩個(gè)操作之間不能重排序。

六、面試題:

1. 代碼在 Windows 上面被編譯為 class 文件,到其他系統(tǒng)下可以直接運(yùn)行嗎?

可以,java文件一旦被編譯轉(zhuǎn)換為 Java 字節(jié)碼文件,也就是 class 文件之后,就運(yùn)行被其他平臺(tái)所支持和運(yùn)行。

2. 實(shí)現(xiàn) Java 虛擬機(jī)的方式有哪些?

軟件方式和硬件方式,常見(jiàn)的方式是軟件方式。

3. JVM 由哪些組成?

類(lèi)加載器子系統(tǒng)、運(yùn)行時(shí)數(shù)據(jù)區(qū)(內(nèi)存空間)、執(zhí)行引擎以及與本地方法接口。

4. 運(yùn)行時(shí)數(shù)據(jù)區(qū)有哪些組成?

由方法區(qū)、堆、Java棧、PC寄存器、本地方法棧組成,其中內(nèi)存空間中方法區(qū)和堆是所有Java線程共享的,而Java棧、本地方法棧、PC寄存器則由每個(gè)線程私有。

4個(gè)部分組成:

  • PC寄存器/程序計(jì)數(shù)器:線程私有;記錄指令執(zhí)行的位置,這里不會(huì)出現(xiàn) OutOfMemoryError
  • Java棧:線程私有,生命周期和線程一致,存儲(chǔ)方法參數(shù)、局部變量、中間運(yùn)算結(jié)果,會(huì)出現(xiàn)異常:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常;如果虛擬機(jī)可以動(dòng)態(tài)擴(kuò)展,如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常。
  • 本地方法棧:存儲(chǔ)了本地方法調(diào)用的狀態(tài)。區(qū)別不過(guò)是Java棧為JVM執(zhí)行Java方法服務(wù),而本地方法棧為JVM執(zhí)行Native方法服務(wù)。本地方法棧也會(huì)拋出StackOverflowError和OutOfMemoryError異常。在Sun JDK中,本地方法棧和Java棧是同一個(gè)。
  • 堆:堆是JVM所管理的內(nèi)存中最大的一塊,是被所有Java線程鎖共享的,不是線程安全的,也會(huì)有 StackOverflowError和OutOfMemoryError異常
  • 方法區(qū):線程共享;類(lèi)型信息和類(lèi)的靜態(tài)變量都存儲(chǔ)在方法區(qū)中。

5. 什么操作會(huì)導(dǎo)致程序的跨平臺(tái)性?

在程序中調(diào)用本地方法會(huì)不在具有跨平臺(tái)性。

6. Java 是通過(guò)什么方式實(shí)現(xiàn) Java 語(yǔ)言的本地方法調(diào)用?

使用 JNI(Java Native Interface)方式實(shí)現(xiàn)。

7. 啟動(dòng)類(lèi)加載器是通過(guò)什么方式初始化加載的?

它不是Java類(lèi),因此它不需要被別人加載,它嵌套在Java虛擬機(jī)內(nèi)核里面,也就是JVM啟動(dòng)的時(shí)候Bootstrap就已經(jīng)啟動(dòng),它是用C++寫(xiě)的二進(jìn)制代碼(不是字節(jié)碼)

8. sun.misc.Launcher$ExtClassLoader 與 java.lang.ClassLoader 關(guān)系?

除了 啟動(dòng)類(lèi)加載器 外其他都是繼承自 java.lang.ClassLoader。

9. 其他類(lèi)加載器如何初始化?

其他的類(lèi)加載器都是通過(guò) sun.misc.Launcher 類(lèi)下面的靜態(tài)內(nèi)部類(lèi)實(shí)現(xiàn),拓展類(lèi)加載器就是 ExtClassLoader,應(yīng)用加載器就是 AppClassLoader。

10. 說(shuō)一下類(lèi)加載的流程是怎么樣的?

裝載、鏈接(校驗(yàn)、準(zhǔn)備、解析)、初始化。

  • 裝載:通過(guò)類(lèi)的全限定名和ClassLoader加載類(lèi),主要是將指定的.class文件加載至JVM
  • 校驗(yàn):校驗(yàn).class文件的正確性,確保該文件是符合規(guī)范定義的,并且適合當(dāng)前JVM使用
  • 準(zhǔn)備:為類(lèi)分配內(nèi)存,同時(shí)初始化類(lèi)中的靜態(tài)變量賦值為默認(rèn)值
  • 解析:主要是把類(lèi)的常量池中的符號(hào)引用解析為直接引用,這一步可以在用到相應(yīng)的引用時(shí)再解析
  • 初始化:初始化類(lèi)中的靜態(tài)變量,并執(zhí)行類(lèi)中的static代碼、構(gòu)造函數(shù)

11. 講一下類(lèi)加載的過(guò)程采用了什么模型?

雙親委派模型。

某個(gè)特定的類(lèi)加載器在接到加載類(lèi)的請(qǐng)求時(shí),首先將加載任務(wù)委托給父類(lèi)加載器,依次遞歸,如果父類(lèi)加載器可以完成類(lèi)加載任務(wù),就成功返回;只有父類(lèi)加載器無(wú)法完成此加載任務(wù)時(shí),才自己去加載。

當(dāng) Java 虛擬機(jī)要加載一個(gè)類(lèi)時(shí),到底派出哪個(gè)類(lèi)加載器去加載呢?

  • 首先當(dāng)前線程的類(lèi)加載器去加載線程中的第一個(gè)類(lèi)(假設(shè)為類(lèi)A)。
    注:當(dāng)前線程的類(lèi)加載器可以通過(guò)Thread類(lèi)的getContextClassLoader()獲得,也可以通過(guò)setContextClassLoader()自己設(shè)置類(lèi)加載器。
  • 如果類(lèi)A中引用了類(lèi)B,Java虛擬機(jī)將使用加載類(lèi)A的類(lèi)加載器去加載類(lèi)B。
  • 還可以直接調(diào)用ClassLoader.loadClass()方法來(lái)指定某個(gè)類(lèi)加載器去加載某個(gè)類(lèi)。

12. 雙親委派的意義是什么?

委托機(jī)制的意義 — 防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼
比如兩個(gè)類(lèi)A和類(lèi)B都要加載System類(lèi):

  • 如果不用委托而是自己加載自己的,那么類(lèi)A就會(huì)加載一份System字節(jié)碼,然后類(lèi)B又會(huì)加載一份System字節(jié)碼,這樣內(nèi)存中就出現(xiàn)了兩份System字節(jié)碼。
  • 如果使用委托機(jī)制,會(huì)遞歸的向父類(lèi)查找,也就是首選用Bootstrap嘗試加載,如果找不到再向下。這里的System就能在Bootstrap中找到然后加載,如果此時(shí)類(lèi)B也要加載System,也從Bootstrap開(kāi)始,此時(shí)Bootstrap發(fā)現(xiàn)已經(jīng)加載過(guò)了System那么直接返回內(nèi)存中的System即可而不需要重新加載,這樣內(nèi)存中就只有一份System的字節(jié)碼了。

13. 能不能自己寫(xiě)個(gè)類(lèi)叫java.lang.System?

通常不可以,但可以采取另類(lèi)方法達(dá)到這個(gè)需求。

為了不讓我們寫(xiě)System類(lèi),類(lèi)加載采用委托機(jī)制,這樣可以保證爸爸們優(yōu)先,爸爸們能找到的類(lèi),兒子就沒(méi)有機(jī)會(huì)加載。而System類(lèi)是Bootstrap加載器加載的,就算自己重寫(xiě),也總是使用Java系統(tǒng)提供的System,自己寫(xiě)的System類(lèi)根本沒(méi)有機(jī)會(huì)得到加載。

但是,我們可以自己定義一個(gè)類(lèi)加載器來(lái)達(dá)到這個(gè)目的,為了避免雙親委托機(jī)制,這個(gè)類(lèi)加載器也必須是特殊的。由于系統(tǒng)自帶的三個(gè)類(lèi)加載器都加載特定目錄下的類(lèi),如果我們自己的類(lèi)加載器放在一個(gè)特殊的目錄,那么系統(tǒng)的加載器就無(wú)法加載,也就是最終還是由我們自己的加載器加載。

14. JVM 中何時(shí)會(huì)觸發(fā)類(lèi)加載的行為?

  • 通過(guò)new關(guān)鍵字、反射、clone、反序列化機(jī)制實(shí)例化對(duì)象時(shí)。
  • 調(diào)用類(lèi)的靜態(tài)方法時(shí)。
  • 使用類(lèi)的靜態(tài)字段或?qū)ζ滟x值時(shí)。
  • 通過(guò)反射調(diào)用類(lèi)的方法時(shí)。
  • 初始化該類(lèi)的子類(lèi)時(shí)(初始化子類(lèi)前其父類(lèi)必須已經(jīng)被初始化)。
  • JVM啟動(dòng)時(shí)被標(biāo)記為啟動(dòng)類(lèi)的類(lèi)(簡(jiǎn)單理解為具有main方法的類(lèi))。

15. 常量池的作用是什么?

常量池本身是方法區(qū)中的一個(gè)數(shù)據(jù)結(jié)構(gòu)。常量池中存儲(chǔ)了如字符串、final變量值、類(lèi)名和方法名常量。常量池在編譯期間就被確定,并保存在已編譯的.class文件中。一般分為兩類(lèi):字面量和應(yīng)用量。字面量就是字符串、final變量等。類(lèi)名和方法名屬于引用量。

16. 解釋一下 Java內(nèi)存模型?

Java內(nèi)存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問(wèn)規(guī)則,以及在JVM中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細(xì)節(jié)。

17. 什么叫可見(jiàn)性?

一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到。

18. 什么叫有序性?

按先后順序執(zhí)行。有序性問(wèn)題的原因是因?yàn)槌绦蛟趫?zhí)行時(shí),可能會(huì)進(jìn)行指令重排,重排后的指令與原指令的順序未必一致。

19. 什么叫原子性?

原子性是指一個(gè)操作是不可中斷的。即使是在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開(kāi)始,就不會(huì)被其他線程干擾。

20. 導(dǎo)致共享變量在線程間不可見(jiàn)的原因?

  • 線程的交叉執(zhí)行
  • 重排序結(jié)合線程交叉執(zhí)行
  • 共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新

21. synchronize 如何保證有序性?

  • 線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中。
  • 線程加鎖時(shí),將清空工作內(nèi)存中共享變量的值,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要的是同一把鎖)

22. 線程執(zhí)行互斥代碼的過(guò)程?

  • 獲得互斥鎖
  • 清空工作內(nèi)存
  • 從主內(nèi)存拷貝最新副本到工作內(nèi)存中
  • 執(zhí)行代碼
  • 將更改過(guò)后的共享變量的值刷新到主內(nèi)存中去。
  • 釋放互斥鎖

23. 指令重排序的價(jià)值是什么?

指令重排序是編譯器或處理器為了提供程序的性能而做的優(yōu)化,

24. 為什么指令重排序可以提升性能?

減少執(zhí)行流水線中斷,從而提高了 CPU 處理性能。

25. 重排序類(lèi)別?

  • 編譯器優(yōu)化的重排序(編譯器優(yōu)化)
  • 指令級(jí)并行重排序(處理器優(yōu)化)
  • 內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)

26. 重排序規(guī)則?

指令重排是基于以下原則之上

  • 程序順序原則:一個(gè)線程內(nèi)保證語(yǔ)義的串行性
  • volatile 規(guī)則:volatile 變量的寫(xiě),先發(fā)生于讀,保證了 volatile 變量的可見(jiàn)性
  • 鎖規(guī)則:解鎖必然發(fā)生在隨后的加鎖之前
  • 傳遞性:A 先于 B,B 先于 C,那么 A 必然先于 C
  • 線程的 start() 方法先于他的每一個(gè)動(dòng)作
  • 線程的所有操作先于線程的終結(jié)
  • 線程的中斷先于被中斷線程的代碼
  • 對(duì)象的構(gòu)造函數(shù)執(zhí)行、結(jié)束先于 finalize() 方法

27. volatile如何實(shí)現(xiàn)內(nèi)存可見(jiàn)性?

通過(guò)加入內(nèi)存屏障和禁止重排序優(yōu)化來(lái)實(shí)現(xiàn)的。

volatile 變量在每次被線程訪問(wèn)時(shí),都強(qiáng)迫從主內(nèi)存中重讀該變量的值,而當(dāng)變量發(fā)生變化時(shí),又強(qiáng)迫線程將最新的值刷新到主內(nèi)存。這樣任何時(shí)刻,不同的線程總能看到該變量的最新的值。

28. 什么是內(nèi)存屏障?

內(nèi)存屏障(memory barrier)是一個(gè) CPU 指令?;旧?,它是這樣一條指令: a) 確保一些特定操作執(zhí)行的順序; b) 影響一些數(shù)據(jù)的可見(jiàn)性(可能是某些指令執(zhí)行后的結(jié)果)。

它們通過(guò)確保從另一個(gè)CPU來(lái)看屏障的兩邊的所有指令都是正確的程序順序,而保持程序順序的外部可見(jiàn)性;其次它們可以實(shí)現(xiàn)內(nèi)存數(shù)據(jù)可見(jiàn)性,確保內(nèi)存數(shù)據(jù)會(huì)同步到CPU緩存子系統(tǒng)。

29. 為什么內(nèi)存屏障能保證可見(jiàn)性和禁止指令重排序?

編譯器和 CPU 可以在保證輸出結(jié)果一樣的情況下對(duì)指令重排序,使性能得到優(yōu)化。插入一個(gè)內(nèi)存屏障,相當(dāng)于告訴 CPU 和編譯器先于這個(gè)命令的必須先執(zhí)行,后于這個(gè)命令的必須后執(zhí)行。內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同 CPU 之間的緩存。

Java內(nèi)存模型中volatile變量在寫(xiě)操作之后會(huì)插入一個(gè)store屏障,在讀操作之前會(huì)插入一個(gè)load屏障。一個(gè)類(lèi)的final字段會(huì)在初始化后插入一個(gè)store屏障,來(lái)確保final字段在構(gòu)造函數(shù)初始化完成并可被使用時(shí)可見(jiàn)。

30. 重排序是怎么做到執(zhí)行性能優(yōu)化的?

編譯器優(yōu)化重排序:在不影響結(jié)果的前提下,對(duì)執(zhí)行順序進(jìn)行優(yōu)化

指令級(jí)并行重排序:使用CPU核內(nèi)部包含的多個(gè)執(zhí)行單元,組合進(jìn)行算術(shù)運(yùn)算,提高計(jì)算并行度和執(zhí)行效率,但這個(gè)過(guò)程中可能就會(huì)存在程序順序的不確定性。

內(nèi)存系統(tǒng)重排序:為了盡可能地避免處理器訪問(wèn)主內(nèi)存的時(shí)間開(kāi)銷(xiāo),處理器大多會(huì)利用緩存(cache)以提高性能。

在這種模型下會(huì)存在一個(gè)現(xiàn)象,即緩存中的數(shù)據(jù)與主內(nèi)存的數(shù)據(jù)并不是實(shí)時(shí)同步的,各CPU(或CPU核心)間緩存的數(shù)據(jù)也不是實(shí)時(shí)同步的。這導(dǎo)致在同一個(gè)時(shí)間點(diǎn),各CPU所看到同一內(nèi)存地址的數(shù)據(jù)的值可能是不一致的。從程序的視角來(lái)看,就是在同一個(gè)時(shí)間點(diǎn),各個(gè)線程所看到的共享變量的值可能是不一致的。

31. CPU 讀取緩存優(yōu)先級(jí)?

寄存器》高速緩存》內(nèi)存


讀者福利:點(diǎn)擊下方傳送門(mén),即可免費(fèi)領(lǐng)取筆者整理的Java后端面試題及Java架構(gòu)師成長(zhǎng)路線圖!?。?/strong>

傳送門(mén)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容