寫在前面
最近,一直有小伙伴讓我整理下關(guān)于JVM的知識(shí),經(jīng)過(guò)十幾天的收集與整理,初版算是整理出來(lái)了。希望對(duì)大家有所幫助。
JDK 是什么?
JDK 是用于支持 Java 程序開發(fā)的最小環(huán)境。
- Java 程序設(shè)計(jì)語(yǔ)言
- Java 虛擬機(jī)
- Java API類庫(kù)
JRE 是什么?
JRE 是支持 Java 程序運(yùn)行的標(biāo)準(zhǔn)環(huán)境。
- Java SE API 子集
- Java 虛擬機(jī)
Java歷史版本的特性?
Java Version SE 5.0
- 引入泛型;
- 增強(qiáng)循環(huán),可以使用迭代方式;
- 自動(dòng)裝箱與自動(dòng)拆箱;
- 類型安全的枚舉;
- 可變參數(shù);
- 靜態(tài)引入;
- 元數(shù)據(jù)(注解);
- 引入Instrumentation。
Java Version SE 6
- 支持腳本語(yǔ)言;
- 引入JDBC 4.0 API;
- 引入Java Compiler API;
- 可插拔注解;
- 增加對(duì)Native PKI(Public Key Infrastructure)、Java GSS(Generic Security Service)、Kerberos和LDAP(Lightweight Directory Access Protocol)的支持;
- 繼承Web Services;
- 做了很多優(yōu)化。
Java Version SE 7
- switch語(yǔ)句塊中允許以字符串作為分支條件;
- 在創(chuàng)建泛型對(duì)象時(shí)應(yīng)用類型推斷;
- 在一個(gè)語(yǔ)句塊中捕獲多種異常;
- 支持動(dòng)態(tài)語(yǔ)言;
- 支持try-with-resources;
- 引入Java NIO.2開發(fā)包;
- 數(shù)值類型可以用2進(jìn)制字符串表示,并且可以在字符串表示中添加下劃線;
- 鉆石型語(yǔ)法;
- null值的自動(dòng)處理。
Java 8
- 函數(shù)式接口
- Lambda表達(dá)式
- Stream API
- 接口的增強(qiáng)
- 時(shí)間日期增強(qiáng)API
- 重復(fù)注解與類型注解
- 默認(rèn)方法與靜態(tài)方法
- Optional 容器類
運(yùn)行時(shí)數(shù)據(jù)區(qū)域包括哪些?
- 程序計(jì)數(shù)器
- Java 虛擬機(jī)棧
- 本地方法棧
- Java 堆
- 方法區(qū)
- 運(yùn)行時(shí)常量池
- 直接內(nèi)存
程序計(jì)數(shù)器(線程私有)
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行字節(jié)碼的行號(hào)指示器。分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器完成。
由于 Java 虛擬機(jī)的多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式實(shí)現(xiàn)的。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,各線程之間的計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。
- 如果線程正在執(zhí)行的是一個(gè) Java 方法,計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;
- 如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器的值為空。
程序計(jì)數(shù)器是唯一一個(gè)沒(méi)有規(guī)定任何 OutOfMemoryError 的區(qū)域。
Java 虛擬機(jī)棧(線程私有)
Java 虛擬機(jī)棧(Java Virtual Machine Stacks)是線程私有的,生命周期與線程相同。
虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),存儲(chǔ)
- 局部變量表
- 操作棧
- 動(dòng)態(tài)鏈接
- 方法出口
每一個(gè)方法被調(diào)用到執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。
這個(gè)區(qū)域有兩種異常情況:
- StackOverflowError:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度
- OutOfMemoryError:虛擬機(jī)棧擴(kuò)展到無(wú)法申請(qǐng)足夠的內(nèi)存時(shí)
本地方法棧(線程私有)
虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(字節(jié)碼)服務(wù)。
本地方法棧(Native Method Stacks)為虛擬機(jī)使用到的 Native 方法服務(wù)。
Java 堆(線程共享)
Java 堆(Java Heap)是 Java 虛擬機(jī)中內(nèi)存最大的一塊。Java 堆在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,被所有線程共享。
作用:存放對(duì)象實(shí)例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上可以不連續(xù),只要邏輯上連續(xù)即可。
方法區(qū)(線程共享)
方法區(qū)(Method Area)被所有線程共享,用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
和 Java 堆一樣,不需要連續(xù)的內(nèi)存,可以選擇固定的大小,更可以選擇不實(shí)現(xiàn)垃圾收集。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。保存 Class 文件中的符號(hào)引用、翻譯出來(lái)的直接引用。運(yùn)行時(shí)常量池可以在運(yùn)行期間將新的常量放入池中。
Java 中對(duì)象訪問(wèn)是如何進(jìn)行的?
Object obj = new Object();
對(duì)于上述最簡(jiǎn)單的訪問(wèn),也會(huì)涉及到 Java 棧、Java 堆、方法區(qū)這三個(gè)最重要內(nèi)存區(qū)域。
Object obj
如果出現(xiàn)在方法體中,則上述代碼會(huì)反映到 Java 棧的本地變量表中,作為 reference 類型數(shù)據(jù)出現(xiàn)。
new Object()
反映到 Java 堆中,形成一塊存儲(chǔ)了 Object 類型所有對(duì)象實(shí)例數(shù)據(jù)值的內(nèi)存。Java堆中還包含對(duì)象類型數(shù)據(jù)的地址信息,這些類型數(shù)據(jù)存儲(chǔ)在方法區(qū)中。
如何判斷對(duì)象是否“死去”?
- 引用計(jì)數(shù)法
- 根搜索算法
什么是引用計(jì)數(shù)法?
給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它,計(jì)數(shù)器就+1,;當(dāng)引用失效時(shí),計(jì)數(shù)器就-1;任何時(shí)刻計(jì)數(shù)器都為0的對(duì)象就是不能再被使用的。
引用計(jì)數(shù)法的缺點(diǎn)?
很難解決對(duì)象之間的循環(huán)引用問(wèn)題。
什么是根搜索算法?
通過(guò)一系列的名為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈相連(用圖論的話來(lái)說(shuō)就是從 GC Roots 到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的。
Java 的4種引用方式?
在 JDK 1.2 之后,Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為
- 強(qiáng)引用 Strong Reference
- 軟引用 Soft Reference
- 弱引用 Weak Reference
- 虛引用 Phantom Reference
強(qiáng)引用
Object obj = new Object();
代碼中普遍存在的,像上述的引用。只要強(qiáng)引用還在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
軟引用
用來(lái)描述一些還有用,但并非必須的對(duì)象。軟引用所關(guān)聯(lián)的對(duì)象,有在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍,并進(jìn)行第二次回收。如果這次回收還是沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存異常。提供了 SoftReference 類實(shí)現(xiàn)軟引用。
弱引用
描述非必須的對(duì)象,強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象,只能生存到下一次垃圾收集發(fā)生前。當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。提供了 WeakReference 類來(lái)實(shí)現(xiàn)弱引用。
虛引用
一個(gè)對(duì)象是否有虛引用,完全不會(huì)對(duì)其生存時(shí)間夠成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象關(guān)聯(lián)虛引用的唯一目的,就是希望在這個(gè)對(duì)象被收集器回收時(shí),收到一個(gè)系統(tǒng)通知。提供了 PhantomReference 類來(lái)實(shí)現(xiàn)虛引用。
有哪些垃圾收集算法?
- 標(biāo)記-清除算法
- 復(fù)制算法
- 標(biāo)記-整理算法
- 分代收集算法
標(biāo)記-清除算法(Mark-Sweep)
什么是標(biāo)記-清除算法?
分為標(biāo)記和清除兩個(gè)階段。首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收被標(biāo)記的對(duì)象。
有什么缺點(diǎn)?
效率問(wèn)題:標(biāo)記和清除過(guò)程的效率都不高。
空間問(wèn)題:標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能導(dǎo)致,程序分配較大對(duì)象時(shí)無(wú)法找到足夠的連續(xù)內(nèi)存,不得不提前出發(fā)另一次垃圾收集動(dòng)作。
復(fù)制算法(Copying)- 新生代
將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊。當(dāng)這一塊的內(nèi)存用完了,就將存活著的對(duì)象復(fù)制到另一塊上面,然后再把已經(jīng)使用過(guò)的內(nèi)存空間一次清理掉。
優(yōu)點(diǎn)
復(fù)制算法使得每次都是針對(duì)其中的一塊進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。
缺點(diǎn)
將內(nèi)存縮小為原來(lái)的一半。在對(duì)象存活率較高時(shí),需要執(zhí)行較多的復(fù)制操作,效率會(huì)變低。
應(yīng)用
商業(yè)的虛擬機(jī)都采用復(fù)制算法來(lái)回收新生代。因?yàn)樾律械膶?duì)象容易死亡,所以并不需要按照1:1的比例劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間。每次使用 Eden 和其中的一塊 Survivor。
當(dāng)回收時(shí),將 Eden 和 Survivor 中還存活的對(duì)象一次性拷貝到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過(guò)的 Survivor 空間。Hotspot 虛擬機(jī)默認(rèn) Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80% + 10%),只有10%的內(nèi)存是會(huì)被“浪費(fèi)”的。
標(biāo)記-整理算法(Mark-Compact)-老年代
標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
分代收集算法
根據(jù)對(duì)象的存活周期,將內(nèi)存劃分為幾塊。一般是把 Java 堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn),采用最適當(dāng)?shù)氖占惴ā?/p>
- 新生代:每次垃圾收集時(shí)會(huì)有大批對(duì)象死去,只有少量存活,所以選擇復(fù)制算法,只需要少量存活對(duì)象的復(fù)制成本就可以完成收集。
- 老年代:對(duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法進(jìn)行回收。
Minor GC 和 Full GC有什么區(qū)別?
Minor GC:新生代 GC,指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)?Java 對(duì)象大多死亡頻繁,所以 Minor GC 非常頻繁,一般回收速度較快。
Full GC:老年代 GC,也叫 Major GC,速度一般比 Minor GC 慢 10 倍以上。
Java 內(nèi)存
為什么要將堆內(nèi)存分區(qū)?
對(duì)于一個(gè)大型的系統(tǒng),當(dāng)創(chuàng)建的對(duì)象及方法變量比較多時(shí),即堆內(nèi)存中的對(duì)象比較多,如果逐一分析對(duì)象是否該回收,效率很低。分區(qū)是為了進(jìn)行模塊化管理,管理不同的對(duì)象及變量,以提高 JVM 的執(zhí)行效率。
堆內(nèi)存分為哪幾塊?
- Young Generation Space 新生區(qū)(也稱新生代)
- Tenure Generation Space養(yǎng)老區(qū)(也稱舊生代)
- Permanent Space 永久存儲(chǔ)區(qū)
分代收集算法
內(nèi)存分配有哪些原則?
- 對(duì)象優(yōu)先分配在 Eden
- 大對(duì)象直接進(jìn)入老年代
- 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
- 動(dòng)態(tài)對(duì)象年齡判定
- 空間分配擔(dān)保
Young Generation Space (采用復(fù)制算法)
主要用來(lái)存儲(chǔ)新創(chuàng)建的對(duì)象,內(nèi)存較小,垃圾回收頻繁。這個(gè)區(qū)又分為三個(gè)區(qū)域:一個(gè) Eden Space 和兩個(gè) Survivor Space。
- 當(dāng)對(duì)象在堆創(chuàng)建時(shí),將進(jìn)入年輕代的Eden Space。
- 垃圾回收器進(jìn)行垃圾回收時(shí),掃描Eden Space和A Suvivor Space,如果對(duì)象仍然存活,則復(fù)制到B Suvivor Space,如果B Suvivor Space已經(jīng)滿,則復(fù)制 Old Gen
- 掃描A Suvivor Space時(shí),如果對(duì)象已經(jīng)經(jīng)過(guò)了幾次的掃描仍然存活,JVM認(rèn)為其為一個(gè)Old對(duì)象,則將其移到Old Gen。
- 掃描完畢后,JVM將Eden Space和A Suvivor Space清空,然后交換A和B的角色(即下次垃圾回收時(shí)會(huì)掃描Eden Space和B Suvivor Space。
Tenure Generation Space(采用標(biāo)記-整理算法)
主要用來(lái)存儲(chǔ)長(zhǎng)時(shí)間被引用的對(duì)象。它里面存放的是經(jīng)過(guò)幾次在 Young Generation Space 進(jìn)行掃描判斷過(guò)仍存活的對(duì)象,內(nèi)存較大,垃圾回收頻率較小。
Permanent Space
存儲(chǔ)不變的類定義、字節(jié)碼和常量等。
Class文件
Java虛擬機(jī)的平臺(tái)無(wú)關(guān)性
Class文件的組成?
Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目間沒(méi)有任何分隔符。當(dāng)遇到8位字節(jié)以上空間的數(shù)據(jù)項(xiàng)時(shí),則會(huì)按照高位在前的方式分隔成若干個(gè)8位字節(jié)進(jìn)行存儲(chǔ)。
魔數(shù)與Class文件的版本
每個(gè)Class文件的頭4個(gè)字節(jié)稱為魔數(shù)(Magic Number),它的唯一作用是用于確定這個(gè)文件是否為一個(gè)能被虛擬機(jī)接受的Class文件。OxCAFEBABE。
接下來(lái)是Class文件的版本號(hào):第5,6字節(jié)是次版本號(hào)(Minor Version),第7,8字節(jié)是主版本號(hào)(Major Version)。
前四個(gè)字節(jié)為魔數(shù),次版本號(hào)是0x0000,主版本號(hào)是0x0033,說(shuō)明本文件是可以被1.7及以上版本的虛擬機(jī)執(zhí)行的文件。
- 33:JDK1.7
- 32:JDK1.6
- 31:JDK1.5
- 30:JDK1.4
- 2F:JDK1.3
類加載器
類加載器的作用是什么?
類加載器實(shí)現(xiàn)類的加載動(dòng)作,同時(shí)用于確定一個(gè)類。對(duì)于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性。即使兩個(gè)類來(lái)源于同一個(gè)Class文件,只要加載它們的類加載器不同,這兩個(gè)類就不相等。
類加載器有哪些?
- 啟動(dòng)類加載器(Bootstrap ClassLoader):使用C++實(shí)現(xiàn)(僅限于HotSpot),是虛擬機(jī)自身的一部分。負(fù)責(zé)將存放在\lib目錄中的類庫(kù)加載到虛擬機(jī)中。其無(wú)法被Java程序直接引用。
- 擴(kuò)展類加載器(Extention ClassLoader)由ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載\lib\ext目錄中的所有類庫(kù),開發(fā)者可以直接使用。
- 應(yīng)用程序類加載器(Application ClassLoader):由APPClassLoader實(shí)現(xiàn)。負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫(kù)。
類加載機(jī)制
什么是雙親委派模型?
雙親委派模型(Parents Delegation Model)要求除了頂層的啟動(dòng)類加載器外,其余加載器都應(yīng)當(dāng)有自己的父類加載器。類加載器之間的父子關(guān)系,通過(guò)組合關(guān)系復(fù)用。
工作過(guò)程:如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器完成。每個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有到父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍沒(méi)有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
為什么要使用雙親委派模型,組織類加載器之間的關(guān)系?
Java類隨著它的類加載器一起具備了一種帶優(yōu)先級(jí)的層次關(guān)系。比如java.lang.Object,它存放在rt.jar中,無(wú)論哪個(gè)類加載器要加載這個(gè)類,最終都是委派給啟動(dòng)類加載器進(jìn)行加載,因此Object類在程序的各個(gè)類加載器環(huán)境中,都是同一個(gè)類。
如果沒(méi)有使用雙親委派模型,讓各個(gè)類加載器自己去加載,那么Java類型體系中最基礎(chǔ)的行為也得不到保障,應(yīng)用程序會(huì)變得一片混亂。
什么是類加載機(jī)制?
Class文件描述的各種信息,都需要加載到虛擬機(jī)后才能運(yùn)行。虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。
虛擬機(jī)和物理機(jī)的區(qū)別是什么?
這兩種機(jī)器都有代碼執(zhí)行的能力,但是:
- 物理機(jī)的執(zhí)行引擎是直接建立在處理器、硬件、指令集和操作系統(tǒng)層面的。
- 虛擬機(jī)的執(zhí)行引擎是自己實(shí)現(xiàn)的,因此可以自行制定指令集和執(zhí)行引擎的結(jié)構(gòu)體系,并且能夠執(zhí)行那些不被硬件直接支持的指令集格式。
運(yùn)行時(shí)棧幀結(jié)構(gòu)
棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu), 存儲(chǔ)了方法的
- 局部變量表
- 操作數(shù)棧
- 動(dòng)態(tài)連接
- 方法返回地址
每一個(gè)方法從調(diào)用開始到執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過(guò)程。
Java 方法調(diào)用
什么是方法調(diào)用?
方法調(diào)用唯一的任務(wù)是確定被調(diào)用方法的版本(調(diào)用哪個(gè)方法),暫時(shí)還不涉及方法內(nèi)部的具體運(yùn)行過(guò)程。
Java的方法調(diào)用,有什么特殊之處?
Class文件的編譯過(guò)程不包含傳統(tǒng)編譯的連接步驟,一切方法調(diào)用在Class文件里面存儲(chǔ)的都只是符號(hào)引用,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址。這使得Java有強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,但使Java方法的調(diào)用過(guò)程變得相對(duì)復(fù)雜,需要在類加載期間甚至到運(yùn)行時(shí)才能確定目標(biāo)方法的直接引用。
Java虛擬機(jī)調(diào)用字節(jié)碼指令有哪些?
- invokestatic:調(diào)用靜態(tài)方法
- invokespecial:調(diào)用實(shí)例構(gòu)造器方法、私有方法和父類方法
- invokevirtual:調(diào)用所有的虛方法
- invokeinterface:調(diào)用接口方法
虛擬機(jī)是如何執(zhí)行方法里面的字節(jié)碼指令的?
解釋執(zhí)行(通過(guò)解釋器執(zhí)行)
編譯執(zhí)行(通過(guò)即時(shí)編譯器產(chǎn)生本地代碼)
解釋執(zhí)行
當(dāng)主流的虛擬機(jī)中都包含了即時(shí)編譯器后,Class文件中的代碼到底會(huì)被解釋執(zhí)行還是編譯執(zhí)行,只有虛擬機(jī)自己才能準(zhǔn)確判斷。
Javac編譯器完成了程序代碼經(jīng)過(guò)詞法分析、語(yǔ)法分析到抽象語(yǔ)法樹,再遍歷語(yǔ)法樹生成線性的字節(jié)碼指令流的過(guò)程。因?yàn)檫@一動(dòng)作是在Java虛擬機(jī)之外進(jìn)行的,而解釋器在虛擬機(jī)的內(nèi)部,所以Java程序的編譯是半獨(dú)立的實(shí)現(xiàn)。
基于棧的指令集和基于寄存器的指令集
什么是基于棧的指令集?
Java編譯器輸出的指令流,里面的指令大部分都是零地址指令,它們依賴操作數(shù)棧進(jìn)行工作。
計(jì)算“1+1=2”,基于棧的指令集是這樣的:
iconst_1
iconst_1
iadd
istore_0
兩條iconst_1指令連續(xù)地把兩個(gè)常量1壓入棧中,iadd指令把棧頂?shù)膬蓚€(gè)值出棧相加,把結(jié)果放回棧頂,最后istore_0把棧頂?shù)闹捣诺骄植孔兞勘淼牡?個(gè)Slot中。
什么是基于寄存器的指令集?
最典型的是x86的地址指令集,依賴寄存器工作。
計(jì)算“1+1=2”,基于寄存器的指令集是這樣的:
mov eax, 1
add eax, 1
mov指令把EAX寄存器的值設(shè)為1,然后add指令再把這個(gè)值加1,結(jié)果就保存在EAX寄存器里。
基于棧的指令集的優(yōu)缺點(diǎn)?
優(yōu)點(diǎn):
- 可移植性好:用戶程序不會(huì)直接用到這些寄存器,由虛擬機(jī)自行決定把一些訪問(wèn)最頻繁的數(shù)據(jù)(程序計(jì)數(shù)器、棧頂緩存)放到寄存器以獲取更好的性能。
- 代碼相對(duì)緊湊:字節(jié)碼中每個(gè)字節(jié)就對(duì)應(yīng)一條指令
- 編譯器實(shí)現(xiàn)簡(jiǎn)單:不需要考慮空間分配問(wèn)題,所需空間都在棧上操作
缺點(diǎn):
- 執(zhí)行速度稍慢
- 完成相同功能所需的指令熟練多
頻繁的訪問(wèn)棧,意味著頻繁的訪問(wèn)內(nèi)存,相對(duì)于處理器,內(nèi)存才是執(zhí)行速度的瓶頸。
Javac編譯過(guò)程分為哪些步驟?
- 解析與填充符號(hào)表
- 插入式注解處理器的注解處理
-
分析與字節(jié)碼生成
什么是即時(shí)編譯器?
Java程序最初是通過(guò)解釋器進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁,就會(huì)把這些代碼認(rèn)定為“熱點(diǎn)代碼”(Hot Spot Code)。
為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時(shí),虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺(tái)相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化,完成這個(gè)任務(wù)的編譯器成為即時(shí)編譯器(Just In Time Compiler,JIT編譯器)。
解釋器和編譯器
許多主流的商用虛擬機(jī),都同時(shí)包含解釋器和編譯器。
- 當(dāng)程序需要快速啟動(dòng)和執(zhí)行時(shí),解釋器首先發(fā)揮作用,省去編譯的時(shí)間,立即執(zhí)行。
- 當(dāng)程序運(yùn)行后,隨著時(shí)間的推移,編譯器逐漸發(fā)揮作用,把越來(lái)越多的代碼編譯成本地代碼,可以提高執(zhí)行效率。
如果內(nèi)存資源限制較大(部分嵌入式系統(tǒng)),可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來(lái)提升效率。同時(shí)編譯器的代碼還能退回成解釋器的代碼。
為什么要采用分層編譯?
因?yàn)榧磿r(shí)編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,要編譯出優(yōu)化程度更高的代碼,所花費(fèi)的時(shí)間越長(zhǎng)。
分層編譯器有哪些層次?
分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模和耗時(shí),劃分不同的編譯層次,包括:
- 第0層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能,可出發(fā)第1層編譯。
- 第1層:也成為C1編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡(jiǎn)單可靠的優(yōu)化,如有必要加入性能監(jiān)控的邏輯。
- 第2層:也成為C2編譯,也是將字節(jié)碼編譯為本地代碼,但是會(huì)啟用一些編譯耗時(shí)較長(zhǎng)的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。
用Client Compiler和Server Compiler將會(huì)同時(shí)工作。用Client Compiler獲取更高的編譯速度,用Server Compiler獲取更好的編譯質(zhì)量。
編譯對(duì)象與觸發(fā)條件
熱點(diǎn)代碼有哪些?
- 被多次調(diào)用的方法
- 被多次執(zhí)行的循環(huán)體
如何判斷一段代碼是不是熱點(diǎn)代碼?
要知道一段代碼是不是熱點(diǎn)代碼,是不是需要觸發(fā)即時(shí)編譯,這個(gè)行為稱為熱點(diǎn)探測(cè)。主要有兩種方法:
- 基于采樣的熱點(diǎn)探測(cè),虛擬機(jī)周期性檢查各個(gè)線程的棧頂,如果發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是“熱點(diǎn)方法”。實(shí)現(xiàn)簡(jiǎn)單高效,但是很難精確確認(rèn)一個(gè)方法的熱度。
- 基于計(jì)數(shù)器的熱點(diǎn)探測(cè),虛擬機(jī)會(huì)為每個(gè)方法建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過(guò)一定的閾值,就認(rèn)為它是熱點(diǎn)方法。
HotSpot虛擬機(jī)使用第二種,有兩個(gè)計(jì)數(shù)器:
- 方法調(diào)用計(jì)數(shù)器
- 回邊計(jì)數(shù)器(判斷循環(huán)代碼)
方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)方法
統(tǒng)計(jì)的是一個(gè)相對(duì)的執(zhí)行頻率,即一段時(shí)間內(nèi)方法被調(diào)用的次數(shù)。當(dāng)超過(guò)一定的時(shí)間限度,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時(shí)編譯器編譯,那這個(gè)方法的調(diào)用計(jì)數(shù)器就會(huì)被減少一半,這個(gè)過(guò)程稱為方法調(diào)用計(jì)數(shù)器的熱度衰減,這個(gè)時(shí)間就被稱為半衰周期。
有哪些經(jīng)典的優(yōu)化技術(shù)(即時(shí)編譯器)?
- 語(yǔ)言無(wú)關(guān)的經(jīng)典優(yōu)化技術(shù)之一:公共子表達(dá)式消除
- 語(yǔ)言相關(guān)的經(jīng)典優(yōu)化技術(shù)之一:數(shù)組范圍檢查消除
- 最重要的優(yōu)化技術(shù)之一:方法內(nèi)聯(lián)
- 最前沿的優(yōu)化技術(shù)之一:逃逸分析
公共子表達(dá)式消除
普遍應(yīng)用于各種編譯器的經(jīng)典優(yōu)化技術(shù),它的含義是:
如果一個(gè)表達(dá)式E已經(jīng)被計(jì)算過(guò)了,并且從先前的計(jì)算到現(xiàn)在E中所有變量的值都沒(méi)有發(fā)生變化,那么E的這次出現(xiàn)就成了公共子表達(dá)式。沒(méi)有必要重新計(jì)算,直接用結(jié)果代替E就可以了。
數(shù)組邊界檢查消除
因?yàn)镴ava會(huì)自動(dòng)檢查數(shù)組越界,每次數(shù)組元素的讀寫都帶有一次隱含的條件判定操作,對(duì)于擁有大量數(shù)組訪問(wèn)的程序代碼,這無(wú)疑是一種性能負(fù)擔(dān)。
如果數(shù)組訪問(wèn)發(fā)生在循環(huán)之中,并且使用循環(huán)變量來(lái)進(jìn)行數(shù)組訪問(wèn),如果編譯器只要通過(guò)數(shù)據(jù)流分析就可以判定循環(huán)變量的取值范圍永遠(yuǎn)在數(shù)組區(qū)間內(nèi),那么整個(gè)循環(huán)中就可以把數(shù)組的上下界檢查消除掉,可以節(jié)省很多次的條件判斷操作。
方法內(nèi)聯(lián)
內(nèi)聯(lián)消除了方法調(diào)用的成本,還為其他優(yōu)化手段建立良好的基礎(chǔ)。
編譯器在進(jìn)行內(nèi)聯(lián)時(shí),如果是非虛方法,那么直接內(nèi)聯(lián)。如果遇到虛方法,則會(huì)查詢當(dāng)前程序下是否有多個(gè)目標(biāo)版本可供選擇,如果查詢結(jié)果只有一個(gè)版本,那么也可以內(nèi)聯(lián),不過(guò)這種內(nèi)聯(lián)屬于激進(jìn)優(yōu)化,需要預(yù)留一個(gè)逃生門(Guard條件不成立時(shí)的Slow Path),稱為守護(hù)內(nèi)聯(lián)。
如果程序的后續(xù)執(zhí)行過(guò)程中,虛擬機(jī)一直沒(méi)有加載到會(huì)令這個(gè)方法的接受者的繼承關(guān)系發(fā)現(xiàn)變化的類,那么內(nèi)聯(lián)優(yōu)化的代碼可以一直使用。否則需要拋棄掉已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新進(jìn)行編譯。
逃逸分析
逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:當(dāng)一個(gè)對(duì)象在方法里面被定義后,它可能被外部方法所引用,這種行為被稱為方法逃逸。被外部線程訪問(wèn)到,被稱為線程逃逸。
如果對(duì)象不會(huì)逃逸到方法或線程外,可以做什么優(yōu)化?
- 棧上分配:一般對(duì)象都是分配在Java堆中的,對(duì)于各個(gè)線程都是共享和可見的,只要持有這個(gè)對(duì)象的引用,就可以訪問(wèn)堆中存儲(chǔ)的對(duì)象數(shù)據(jù)。但是垃圾回收和整理都會(huì)耗時(shí),如果一個(gè)對(duì)象不會(huì)逃逸出方法,可以讓這個(gè)對(duì)象在棧上分配內(nèi)存,對(duì)象所占用的內(nèi)存空間就可以隨著棧幀出棧而銷毀。如果能使用棧上分配,那大量的對(duì)象會(huì)隨著方法的結(jié)束而自動(dòng)銷毀,垃圾回收的壓力會(huì)小很多。
- 同步消除:線程同步本身就是很耗時(shí)的過(guò)程。如果逃逸分析能確定一個(gè)變量不會(huì)逃逸出線程,那這個(gè)變量的讀寫肯定就不會(huì)有競(jìng)爭(zhēng),同步措施就可以消除掉。
- 標(biāo)量替換:不創(chuàng)建這個(gè)對(duì)象,直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來(lái)替換。
Java與C/C++的編譯器對(duì)比
- 即時(shí)編譯器運(yùn)行占用的是用戶程序的運(yùn)行時(shí)間,具有很大的時(shí)間壓力。
- Java語(yǔ)言雖然沒(méi)有virtual關(guān)鍵字,但是使用虛方法的頻率遠(yuǎn)大于C++,所以即時(shí)編譯器進(jìn)行優(yōu)化時(shí)難度要遠(yuǎn)遠(yuǎn)大于C++的靜態(tài)優(yōu)化編譯器。
- Java語(yǔ)言是可以動(dòng)態(tài)擴(kuò)展的語(yǔ)言,運(yùn)行時(shí)加載新的類可能改變程序類型的繼承關(guān)系,使得全局的優(yōu)化難以進(jìn)行,因?yàn)榫幾g器無(wú)法看見程序的全貌,編譯器不得不時(shí)刻注意并隨著類型的變化,而在運(yùn)行時(shí)撤銷或重新進(jìn)行一些優(yōu)化。
- Java語(yǔ)言對(duì)象的內(nèi)存分配是在堆上,只有方法的局部變量才能在棧上分配。C++的對(duì)象有多種內(nèi)存分配方式。
物理機(jī)如何處理并發(fā)問(wèn)題?
運(yùn)算任務(wù),除了需要處理器計(jì)算之外,還需要與內(nèi)存交互,如讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果等(不能僅靠寄存器來(lái)解決)。
計(jì)算機(jī)的存儲(chǔ)設(shè)備和處理器的運(yùn)算速度差了幾個(gè)數(shù)量級(jí),所以不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(Cache),作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算快速運(yùn)行。當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存,這樣處理器就無(wú)需等待緩慢的內(nèi)存讀寫了。
基于高速緩存的存儲(chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是引入了一個(gè)新的問(wèn)題:緩存一致性。在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,它們又共享同一主內(nèi)存。當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存時(shí),可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。
為了解決一致性的問(wèn)題,需要各個(gè)處理器訪問(wèn)緩存時(shí)遵循緩存一致性協(xié)議。同時(shí)為了使得處理器充分被利用,處理器可能會(huì)對(duì)輸出代碼進(jìn)行亂序執(zhí)行優(yōu)化。Java虛擬機(jī)的即時(shí)編譯器也有類似的指令重排序優(yōu)化。
Java 內(nèi)存模型
什么是Java內(nèi)存模型?
Java虛擬機(jī)的規(guī)范,用來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各個(gè)平臺(tái)下都能達(dá)到一致的并發(fā)效果。
Java內(nèi)存模型的目標(biāo)?
定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出這樣的底層細(xì)節(jié)。此處的變量包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量和方法參數(shù),因?yàn)檫@些是線程私有的,不會(huì)被共享,所以不存在競(jìng)爭(zhēng)問(wèn)題。
主內(nèi)存與工作內(nèi)存
所以的變量都存儲(chǔ)在主內(nèi)存,每條線程還有自己的工作內(nèi)存,保存了被該線程使用到的變量的主內(nèi)存副本拷貝。線程對(duì)變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫主內(nèi)存的變量。不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存的變量,線程間變量值的傳遞需要通過(guò)主內(nèi)存。
內(nèi)存間的交互操作
一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存,Java內(nèi)存模型定義了8種操作:
原子性、可見性、有序性
- 原子性:對(duì)基本數(shù)據(jù)類型的訪問(wèn)和讀寫是具備原子性的。對(duì)于更大范圍的原子性保證,可以使用字節(jié)碼指令monitorenter和monitorexit來(lái)隱式使用lock和unlock操作。這兩個(gè)字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關(guān)鍵字。因此synchronized塊之間的操作也具有原子性。
- 可見性:當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改。Java內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取之前從主內(nèi)存刷新變量值來(lái)實(shí)現(xiàn)可見性的。volatile的特殊規(guī)則保證了新值能夠立即同步到主內(nèi)存,每次使用前立即從主內(nèi)存刷新。synchronized和final也能實(shí)現(xiàn)可見性。final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒(méi)有把this的引用傳遞出去,那么其他線程中就能看見final字段的值。
- 有序性:Java程序的有序性可以總結(jié)為一句話,如果在本線程內(nèi)觀察,所有的操作都是有序的(線程內(nèi)表現(xiàn)為串行的語(yǔ)義);如果在一個(gè)線程中觀察另一個(gè)線程,所有的操作都是無(wú)序的(指令重排序和工作內(nèi)存與主內(nèi)存同步延遲線性)。
volatile
什么是volatile?
關(guān)鍵字volatile是Java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制。當(dāng)一個(gè)變量被定義成volatile之后,具備兩種特性:
- 保證此變量對(duì)所有線程的可見性。當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程是可以立即得知的。而普通變量做不到這一點(diǎn)。
- 禁止指令重排序優(yōu)化。普通變量?jī)H僅能保證在該方法執(zhí)行過(guò)程中,得到正確結(jié)果,但是不保證程序代碼的執(zhí)行順序。
為什么基于volatile變量的運(yùn)算在并發(fā)下不一定是安全的?
volatile變量在各個(gè)線程的工作內(nèi)存,不存在一致性問(wèn)題(各個(gè)線程的工作內(nèi)存中volatile變量,每次使用前都要刷新到主內(nèi)存)。但是Java里面的運(yùn)算并非原子操作,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的。
為什么使用volatile?
在某些情況下,volatile同步機(jī)制的性能要優(yōu)于鎖(synchronized關(guān)鍵字),但是由于虛擬機(jī)對(duì)鎖實(shí)行的許多消除和優(yōu)化,所以并不是很快。
volatile變量讀操作的性能消耗與普通變量幾乎沒(méi)有差別,但是寫操作則可能慢一些,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來(lái)保證處理器不發(fā)生亂序執(zhí)行。
并發(fā)與線程
并發(fā)與線程的關(guān)系?
并發(fā)不一定要依賴多線程,PHP中有多進(jìn)程并發(fā)。但是Java里面的并發(fā)是多線程的。
什么是線程?
線程是比進(jìn)程更輕量級(jí)的調(diào)度執(zhí)行單位。線程可以把一個(gè)進(jìn)程的資源分配和執(zhí)行調(diào)度分開,各個(gè)線程既可以共享進(jìn)程資源(內(nèi)存地址、文件I/O),又可以獨(dú)立調(diào)度(線程是CPU調(diào)度的最基本單位)。
實(shí)現(xiàn)線程有哪些方式?
- 使用內(nèi)核線程實(shí)現(xiàn)
- 使用用戶線程實(shí)現(xiàn)
- 使用用戶線程+輕量級(jí)進(jìn)程混合實(shí)現(xiàn)
Java線程的實(shí)現(xiàn)
操作系統(tǒng)支持怎樣的線程模型,在很大程度上就決定了Java虛擬機(jī)的線程是怎樣映射的。
Java線程調(diào)度
什么是線程調(diào)度?
線程調(diào)度是系統(tǒng)為線程分配處理器使用權(quán)的過(guò)程。
線程調(diào)度有哪些方法?
- 協(xié)同式線程調(diào)度:實(shí)現(xiàn)簡(jiǎn)單,沒(méi)有線程同步的問(wèn)題。但是線程執(zhí)行時(shí)間不可控,容易系統(tǒng)崩潰。
- 搶占式線程調(diào)度:每個(gè)線程由系統(tǒng)來(lái)分配執(zhí)行時(shí)間,不會(huì)有線程導(dǎo)致整個(gè)進(jìn)程阻塞的問(wèn)題。
雖然Java線程調(diào)度是系統(tǒng)自動(dòng)完成的,但是我們可以建議系統(tǒng)給某些線程多分配點(diǎn)時(shí)間——設(shè)置線程優(yōu)先級(jí)。Java語(yǔ)言有10個(gè)級(jí)別的線程優(yōu)先級(jí),優(yōu)先級(jí)越高的線程,越容易被系統(tǒng)選擇執(zhí)行。
但是并不能完全依靠線程優(yōu)先級(jí)。因?yàn)镴ava的線程是被映射到系統(tǒng)的原生線程上,所以線程調(diào)度最終還是由操作系統(tǒng)說(shuō)了算。如Windows中只有7種優(yōu)先級(jí),所以Java不得不出現(xiàn)幾個(gè)優(yōu)先級(jí)相同的情況。同時(shí)優(yōu)先級(jí)可能會(huì)被系統(tǒng)自行改變。Windows系統(tǒng)中存在一個(gè)“優(yōu)先級(jí)推進(jìn)器”,當(dāng)系統(tǒng)發(fā)現(xiàn)一個(gè)線程執(zhí)行特別勤奮,可能會(huì)越過(guò)線程優(yōu)先級(jí)為它分配執(zhí)行時(shí)間。
線程安全的定義?
當(dāng)多個(gè)線程訪問(wèn)一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方法進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確的結(jié)果,那這個(gè)對(duì)象就是線程安全的。
Java語(yǔ)言操作的共享數(shù)據(jù),包括哪些?
- 不可變
- 絕對(duì)線程安全
- 相對(duì)線程安全
- 線程兼容
- 線程對(duì)立
不可變
在Java語(yǔ)言里,不可變的對(duì)象一定是線程安全的,只要一個(gè)不可變的對(duì)象被正確構(gòu)建出來(lái),那其外部的可見狀態(tài)永遠(yuǎn)也不會(huì)改變,永遠(yuǎn)也不會(huì)在多個(gè)線程中處于不一致的狀態(tài)。
如何實(shí)現(xiàn)線程安全?
虛擬機(jī)提供了同步和鎖機(jī)制。
- 阻塞同步(互斥同步)
- 非阻塞同步
阻塞同步(互斥同步)
互斥是實(shí)現(xiàn)同步的一種手段,臨界區(qū)、互斥量和信號(hào)量都是主要的互斥實(shí)現(xiàn)方式。Java中最基本的同步手段就是synchronized關(guān)鍵字,其編譯后會(huì)在同步塊的前后分別形成monitorenter和monitorexit兩個(gè)字節(jié)碼指令。這兩個(gè)字節(jié)碼都需要一個(gè)Reference類型的參數(shù)指明要鎖定和解鎖的對(duì)象。如果Java程序中的synchronized明確指定了對(duì)象參數(shù),那么這個(gè)對(duì)象就是Reference;如果沒(méi)有明確指定,那就根據(jù)synchronized修飾的是實(shí)例方法還是類方法,去獲取對(duì)應(yīng)的對(duì)象實(shí)例或Class對(duì)象作為鎖對(duì)象。
在執(zhí)行monitorenter指令時(shí),首先要嘗試獲取對(duì)象的鎖。
- 如果這個(gè)對(duì)象沒(méi)有鎖定,或者當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,把鎖的計(jì)數(shù)器+1;當(dāng)執(zhí)行monitorexit指令時(shí)將鎖計(jì)數(shù)器-1。當(dāng)計(jì)數(shù)器為0時(shí),鎖就被釋放了。
- 如果獲取對(duì)象失敗了,那當(dāng)前線程就要阻塞等待,知道對(duì)象鎖被另外一個(gè)線程釋放為止。
除了synchronized之外,還可以使用java.util.concurrent包中的重入鎖(ReentrantLock)來(lái)實(shí)現(xiàn)同步。ReentrantLock比synchronized增加了高級(jí)功能:等待可中斷、可實(shí)現(xiàn)公平鎖、鎖可以綁定多個(gè)條件。
等待可中斷:當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,對(duì)處理執(zhí)行時(shí)間非常長(zhǎng)的同步塊很有用。
公平鎖:多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來(lái)依次獲得鎖。synchronized中的鎖是非公平的。
非阻塞同步
互斥同步最大的問(wèn)題,就是進(jìn)行線程阻塞和喚醒所帶來(lái)的性能問(wèn)題,是一種悲觀的并發(fā)策略。總是認(rèn)為只要不去做正確的同步措施(加鎖),那就肯定會(huì)出問(wèn)題,無(wú)論共享數(shù)據(jù)是否真的會(huì)出現(xiàn)競(jìng)爭(zhēng),它都要進(jìn)行加鎖、用戶態(tài)核心態(tài)轉(zhuǎn)換、維護(hù)鎖計(jì)數(shù)器和檢查是否有被阻塞的線程需要被喚醒等操作。
隨著硬件指令集的發(fā)展,我們可以使用基于沖突檢測(cè)的樂(lè)觀并發(fā)策略。先進(jìn)行操作,如果沒(méi)有其他線程征用數(shù)據(jù),那操作就成功了;如果共享數(shù)據(jù)有征用,產(chǎn)生了沖突,那就再進(jìn)行其他的補(bǔ)償措施。這種樂(lè)觀的并發(fā)策略的許多實(shí)現(xiàn)不需要線程掛起,所以被稱為非阻塞同步。
鎖優(yōu)化是在JDK的那個(gè)版本?
JDK1.6的一個(gè)重要主題,就是高效并發(fā)。HotSpot虛擬機(jī)開發(fā)團(tuán)隊(duì)在這個(gè)版本上,實(shí)現(xiàn)了各種鎖優(yōu)化:
- 適應(yīng)性自旋
- 鎖消除
- 鎖粗化
- 輕量級(jí)鎖
- 偏向鎖
為什么要提出自旋鎖?
互斥同步對(duì)性能最大的影響是阻塞的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性帶來(lái)很大壓力。同時(shí)很多應(yīng)用共享數(shù)據(jù)的鎖定狀態(tài),只會(huì)持續(xù)很短的一段時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線程并不值得。先不掛起線程,等一會(huì)兒。
自旋鎖的原理?
如果物理機(jī)器有一個(gè)以上的處理器,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,讓后面請(qǐng)求鎖的線程稍等一會(huì),但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋)。
自旋的缺點(diǎn)?
自旋等待本身雖然避免了線程切換的開銷,但它要占用處理器時(shí)間。所以如果鎖被占用的時(shí)間很短,自旋等待的效果就非常好;如果時(shí)間很長(zhǎng),那么自旋的線程只會(huì)白白消耗處理器的資源。所以自旋等待的時(shí)間要有一定的限度,如果自旋超過(guò)了限定的次數(shù)仍然沒(méi)有成功獲得鎖,那就應(yīng)該使用傳統(tǒng)的方式掛起線程了。
什么是自適應(yīng)自旋?
自旋的時(shí)間不固定了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。
- 如果一個(gè)鎖對(duì)象,自旋等待剛剛成功獲得鎖,并且持有鎖的線程正在運(yùn)行,那么虛擬機(jī)認(rèn)為這次自旋仍然可能成功,進(jìn)而運(yùn)行自旋等待更長(zhǎng)的時(shí)間。
- 如果對(duì)于某個(gè)鎖,自旋很少成功,那在以后要獲取這個(gè)鎖,可能省略掉自旋過(guò)程,以免浪費(fèi)處理器資源。
有了自適應(yīng)自旋,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來(lái)越準(zhǔn)確,虛擬機(jī)也會(huì)越來(lái)越聰明。
鎖消除
鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。主要根據(jù)逃逸分析。
程序員怎么會(huì)在明知道不存在數(shù)據(jù)競(jìng)爭(zhēng)的情況下使用同步呢?很多不是程序員自己加入的。
鎖粗化
原則上,同步塊的作用范圍要盡量小。但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作在循環(huán)體內(nèi),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
鎖粗化就是增大鎖的作用域。
輕量級(jí)鎖
在沒(méi)有多線程競(jìng)爭(zhēng)的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
偏向鎖
消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能。即在無(wú)競(jìng)爭(zhēng)的情況下,把整個(gè)同步都消除掉。這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖沒(méi)有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要同步。
參考:《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第2版)》