【JVM】肝了一周,吐血整理出這份超硬核的JVM筆記(升級(jí)版)!!

寫在前面

最近,一直有小伙伴讓我整理下關(guān)于JVM的知識(shí),經(jīng)過(guò)十幾天的收集與整理,初版算是整理出來(lái)了。希望對(duì)大家有所幫助。

JDK 是什么?

JDK 是用于支持 Java 程序開發(fā)的最小環(huán)境。

  1. Java 程序設(shè)計(jì)語(yǔ)言
  2. Java 虛擬機(jī)
  3. Java API類庫(kù)

JRE 是什么?

JRE 是支持 Java 程序運(yùn)行的標(biāo)準(zhǔn)環(huán)境。

  1. Java SE API 子集
  2. 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ū)域包括哪些?

  1. 程序計(jì)數(shù)器
  2. Java 虛擬機(jī)棧
  3. 本地方法棧
  4. Java 堆
  5. 方法區(qū)
  6. 運(yùn)行時(shí)常量池
  7. 直接內(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ǔ)。

  1. 如果線程正在執(zhí)行的是一個(gè) Java 方法,計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;
  2. 如果正在執(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ǔ)

  1. 局部變量表
  2. 操作棧
  3. 動(dòng)態(tài)鏈接
  4. 方法出口

每一個(gè)方法被調(diào)用到執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。

這個(gè)區(qū)域有兩種異常情況:

  1. StackOverflowError:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度
  2. 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ì)象是否“死去”?

  1. 引用計(jì)數(shù)法
  2. 根搜索算法

什么是引用計(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ò)充,將引用分為

  1. 強(qiáng)引用 Strong Reference
  2. 軟引用 Soft Reference
  3. 弱引用 Weak Reference
  4. 虛引用 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)虛引用。

有哪些垃圾收集算法?

  1. 標(biāo)記-清除算法
  2. 復(fù)制算法
  3. 標(biāo)記-整理算法
  4. 分代收集算法

標(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)存分為哪幾塊?

  1. Young Generation Space 新生區(qū)(也稱新生代)
  2. Tenure Generation Space養(yǎng)老區(qū)(也稱舊生代)
  3. Permanent Space 永久存儲(chǔ)區(qū)

分代收集算法

內(nèi)存分配有哪些原則?

  1. 對(duì)象優(yōu)先分配在 Eden
  2. 大對(duì)象直接進(jìn)入老年代
  3. 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
  4. 動(dòng)態(tài)對(duì)象年齡判定
  5. 空間分配擔(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)。

006.jpg

前四個(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è)類就不相等。

類加載器有哪些?

  1. 啟動(dòng)類加載器(Bootstrap ClassLoader):使用C++實(shí)現(xiàn)(僅限于HotSpot),是虛擬機(jī)自身的一部分。負(fù)責(zé)將存放在\lib目錄中的類庫(kù)加載到虛擬機(jī)中。其無(wú)法被Java程序直接引用。
  2. 擴(kuò)展類加載器(Extention ClassLoader)由ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載\lib\ext目錄中的所有類庫(kù),開發(fā)者可以直接使用。
  3. 應(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ò)程分為哪些步驟?

  1. 解析與填充符號(hào)表
  2. 插入式注解處理器的注解處理
  3. 分析與字節(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ì)比

  1. 即時(shí)編譯器運(yùn)行占用的是用戶程序的運(yùn)行時(shí)間,具有很大的時(shí)間壓力。
  2. 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)化編譯器。
  3. 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)化。
  4. 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之后,具備兩種特性:

  1. 保證此變量對(duì)所有線程的可見性。當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程是可以立即得知的。而普通變量做不到這一點(diǎn)。
  2. 禁止指令重排序優(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版)》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,541評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,896評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,062評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,356評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,555評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,769評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,289評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,516評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容