本文所有內(nèi)容來于:http:///a/100ww java代碼是如何執(zhí)行的- java代碼是運行于java虛擬機上的,通過java虛擬機實現(xiàn)了跨平臺,,并且java虛擬機幫助程序員處理了容易出錯的事務(wù),,比如內(nèi)存管理。
java虛擬機會解釋執(zhí)行java字節(jié)碼,,并且對于熱點代碼會采用即時編譯(Just-In-Time compilation,,,JIT),,即將一個方法中包含的所有字節(jié)碼編譯成機器碼后再執(zhí)行,。如下圖所示: Java 虛擬機將運行時內(nèi)存區(qū)域劃分為五個部分,分別為方法區(qū),、堆,、PC 寄存器、Java 方法棧和本地方法棧,。如下圖所示:
JVM如何加載類??java引用類型分為四種:類,、接口、數(shù)組類和泛型參數(shù),。其中泛型參數(shù)會在編譯過程中被擦除,。因此 Java 虛擬機實際上只有前三種。在類,、接口和數(shù)組類中,,數(shù)組類是由 Java 虛擬機直接生成的,其他兩種則有對應(yīng)的字節(jié)流(接口,,類),。 - 加載:指的是查找字節(jié)流,數(shù)組類由JVM生成,,所以這一過程可以省了。類加載是通過類加載器完成的,。在 Java 虛擬機中,,類的唯一性是由類加載器實例以及類的全名一同確定的。即便是同一串字節(jié)流,,經(jīng)由不同的類加載器加載,,也會得到兩個不同的類。類加載通過雙親委派模型,,先由父類加載,,父類加載不了再由子類加載。除了啟動類加載器,,類加載器都繼承自java.lang.ClassLoader,。類加載器分為:
1:啟動類加載器:由C++編寫,,不對應(yīng)于任何對象。加載JRE/lib目錄下的JAR包和虛擬機參數(shù) -Xbootclasspath 指定的類,。 2:擴展類加載器:父類加載器是啟動類加載器,,負責(zé)加載JRE 的 lib/ext 目錄下 jar 包中的類(以及由系統(tǒng)變量 java.ext.dirs 指定的類)。 3:應(yīng)用類加載器:父類加載器是擴展類加載器,,它負責(zé)加載應(yīng)用程序路徑下的類,。這里的應(yīng)用程序路徑,便是指虛擬機參數(shù) -cp/-classpath,、系統(tǒng)變量 java.class.path 或環(huán)境變量 CLASSPATH 所指定的路徑,。 - 鏈接:是指將類合并至JVM中,使之能夠執(zhí)行的過程,。分為驗證,,準(zhǔn)備,解析,。
1:驗證階段:主要是保證加載的類滿足JVM的約束,,也是為了保證JVM的安全性。 2:準(zhǔn)備階段:為被加載類的靜態(tài)字段分配內(nèi)存,。只是分配內(nèi)存,,具體的初使化,則在初使化階段,。在這個階段也會構(gòu)造類的方法表,。 3:解析階段(非必須):在 class 文件被加載至 Java 虛擬機之前,這個類無法知道其他類及其方法,、字段所對應(yīng)的具體地址,,甚至不知道自己方法、字段的地址,。因此,,每當(dāng)需要引用這些成員時,Java 編譯器會生成一個符號引用,。在運行階段,,這個符號引用一般都能夠無歧義地定位到具體目標(biāo)上。解析階段的目的,,正是將這些符號引用解析成為實際引用,。如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,,那么解析將觸發(fā)這個類的加載(但未必觸發(fā)這個類的鏈接以及初始化,。) - 初使化:初使化靜態(tài)變量(由static修飾的變量)并執(zhí)行static代碼塊。所有的static代碼塊會放到同一方法中,,并命名為,,這個方法會由JVM加鎖保證同步,。類的初使化時機:
1:當(dāng)虛擬機啟動時,初始化用戶指定的主類,; 2:當(dāng)遇到用以新建目標(biāo)類實例的 new 指令時,,初始化 new 指令的目標(biāo)類; 3:當(dāng)遇到調(diào)用靜態(tài)方法的指令時,,初始化該靜態(tài)方法所在的類,; 4:當(dāng)遇到訪問靜態(tài)字段的指令時,初始化該靜態(tài)字段所在的類,; 5:子類的初始化會觸發(fā)父類的初始化,; 6:如果一個接口定義了 default 方法,那么直接實現(xiàn)或者間接實現(xiàn)該接口的類的初始化,,會觸發(fā)該接口的初始化,; 7:使用反射 API 對某個類進行反射調(diào)用時,初始化這個類,; 8:當(dāng)初次調(diào)用 MethodHandle 實例時,,初始化該 MethodHandle 指向的方法所在的類。
JVM如何執(zhí)行方法調(diào)用??Java 虛擬機識別方法的關(guān)鍵在于類名,、方法名,、方法的參數(shù)類型以及返回類型。在同一個類中,,如果同時出現(xiàn)多個名字相同且描述符也相同的方法,,那么 Java 虛擬機會在類的驗證階段報錯。 ??Java 虛擬機與 Java 語言不同,,它并不限制名字與參數(shù)類型相同,,但返回類型不同的方法出現(xiàn)在同一個類中,對于調(diào)用這些方法的字節(jié)碼來說,,由于字節(jié)碼所附帶的方法描述符包含了返回類型,,因此 Java 虛擬機能夠準(zhǔn)確地識別目標(biāo)方法。 ??Java 虛擬機中關(guān)于方法重寫的判定同樣基于方法描述符,。也就是說,,如果子類定義了與父類中非私有、非靜態(tài)方法同名的方法,,那么只有當(dāng)這兩個方法的參數(shù)類型以及返回類型一致,Java 虛擬機才會判定為重寫,。對于 Java 語言中重寫而 Java 虛擬機中非重寫的情況,,編譯器會通過生成橋接方法來實現(xiàn) Java 中的重寫語義。 ??Java 虛擬機中的靜態(tài)綁定指的是在解析時便能夠直接識別目標(biāo)方法的情況,,而動態(tài)綁定則指的是需要在運行過程中根據(jù)調(diào)用者的動態(tài)類型來識別目標(biāo)方法的情況,。 ??Java 字節(jié)碼中與調(diào)用相關(guān)的指令共有五種: 1:invokestatic:用于調(diào)用靜態(tài)方法,,編譯期就可以確定調(diào)用的方法。 2:invokespecial:用于調(diào)用私有實例方法,、構(gòu)造器,,以及使用 super 關(guān)鍵字調(diào)用父類的實例方法或構(gòu)造器,和所實現(xiàn)接口的默認方法,。編譯期就可以確定調(diào)用的方法,。 3:invokevirtual:用于調(diào)用非私有實例方法,需要在運行期確定需要調(diào)用的方法,。 4:invokeinterface:用于調(diào)用接口方法,,需要在運行期確定需要調(diào)用的方法。 5:invokedynamic:用于調(diào)用動態(tài)方法,。 ??在編譯過程中,,我們并不知道目標(biāo)方法的具體內(nèi)存地址。因此,,Java 編譯器會暫時用符號引用來表示該目標(biāo)方法,。這一符號引用包括目標(biāo)方法所在的類或接口的名字,以及目標(biāo)方法的方法名和方法描述符,。符號引用存儲在 class 文件的常量池之中,。根據(jù)目標(biāo)方法是否為接口方法,這些引用可分為接口符號引用和非接口符號引用,。如果虛方法(invokevirtual)調(diào)用指向一個標(biāo)記為 final 的方法,,那么Java虛擬機也可以靜態(tài)綁定該虛方法調(diào)用的目標(biāo)方法。 ??Java 虛擬機中采取了一種用空間換取時間的策略來實現(xiàn)動態(tài)綁定,。它為每個類生成一張方法表(類加載的鏈接階段實現(xiàn)),,用以快速定位目標(biāo)方法。方法表分為虛方法表(invokevirtual調(diào)用)與接口方法表(invokeinterface)調(diào)用,。方法表本質(zhì)上是一個數(shù)組,,每個數(shù)組元素指向一個當(dāng)前類及其祖先類中非私有的實例方法。方法表滿足兩個特質(zhì): - 子類方法表中包含父類方法表中的所有方法,;
- 子類方法在方法表中的索引值,,與它所重寫的父類方法的索引值相同。
??方法調(diào)用指令中的符號引用會在執(zhí)行之前解析成實際引用,。對于靜態(tài)綁定的方法調(diào)用而言,,實際引用將指向具體的目標(biāo)方法。對于動態(tài)綁定的方法調(diào)用而言,,實際引用則是方法表的索引值(實際上并不僅是索引值),。在執(zhí)行過程中,Java 虛擬機將獲取調(diào)用者的實際類型,并在該實際類型的虛方法表中,,根據(jù)索引值獲得目標(biāo)方法,。這個過程便是動態(tài)綁定。Java 虛擬機中的即時編譯器會使用內(nèi)聯(lián)緩存來加速動態(tài)綁定,。Java 虛擬機所采用的單態(tài)內(nèi)聯(lián)緩存將紀(jì)錄調(diào)用者的動態(tài)類型,,以及它所對應(yīng)的目標(biāo)方法。當(dāng)碰到新的調(diào)用者時,,如果其動態(tài)類型與緩存中的類型匹配,,則直接調(diào)用緩存的目標(biāo)方法。否則,,Java 虛擬機將該內(nèi)聯(lián)緩存劣化為超多態(tài)內(nèi)聯(lián)緩存,,在今后的執(zhí)行過程中直接使用方法表進行動態(tài)綁定。 JVM異常處理??拋出異??煞譃轱@式和隱式兩種,。顯式拋異常的主體是應(yīng)用程序,它指的是在程序中使用“throw”關(guān)鍵字,,手動將異常實例拋出,。隱式拋異常的主體則是Java 虛擬機,它指的是 Java 虛擬機在執(zhí)行過程中,,碰到無法繼續(xù)執(zhí)行的異常狀態(tài),,自動拋出異常。 ??異常實例的構(gòu)造十分昂貴,。這是由于在構(gòu)造異常實例時,,Java 虛擬機需要生成該異常的棧軌跡(stack trace)。該操作會逐一訪問當(dāng)前線程的 Java 棧幀,,并且記錄下各種調(diào)試信息,,包括棧幀所指向方法的名字,方法所在的類名,、文件名,,以及在代碼中的第幾行觸發(fā)該異常。 ??在編譯生成的字節(jié)碼中,,每個方法都附帶一個異常表,。異常表中的每一個條目代表一個異常處理器,并且由 from 指針,、to 指針,、target 指針以及所捕獲的異常類型構(gòu)成。這些指針的值是字節(jié)碼索引(bytecode index,,bci),,用以定位字節(jié)碼。 ??當(dāng)程序觸發(fā)異常時,Java 虛擬機會從上至下遍歷異常表中的所有條目,。當(dāng)觸發(fā)異常的字節(jié)碼的索引值在某個異常表條目的監(jiān)控范圍內(nèi),Java 虛擬機會判斷所拋出的異常和該條目想要捕獲的異常是否匹配,。如果匹配,,Java 虛擬機會將控制流轉(zhuǎn)移至該條目 target 指針指向的字節(jié)碼。如果遍歷完所有異常表條目,,Java 虛擬機仍未匹配到異常處理器,,那么它會彈出當(dāng)前方法對應(yīng)的 Java 棧幀,并且在調(diào)用者(caller)中重復(fù)上述操作,。在最壞情況下,,Java 虛擬機需要遍歷當(dāng)前線程 Java 棧上所有方法的異常表。 ??finally 代碼塊的編譯比較復(fù)雜,。當(dāng)前版本 Java 編譯器的做法,,是復(fù)制 finally 代碼塊的內(nèi)容,分別放在 try-catch 代碼塊所有正常執(zhí)行路徑以及異常執(zhí)行路徑的出口中,。 對象的內(nèi)存布局??通過 new 指令新建出來的對象(分存在堆中),,它的內(nèi)存其實涵蓋了所有父類中的實例字段。也就是說,,雖然子類無法訪問父類的私有實例字段,,或者子類的實例字段隱藏了父類的同名實例字段,但是子類的實例還是會為這些父類實例字段分配內(nèi)存的,。 ??在 Java 虛擬機中,,每個 Java 對象都有一個對象頭(object header),這個由標(biāo)記字段(Mark Word)和類型指針?biāo)鶚?gòu)成,。其中,,標(biāo)記字段用以存儲 Java 虛擬機有關(guān)該對象的運行數(shù)據(jù),如哈希碼,、GC 信息以及鎖信息,,而類型指針則指向該對象的類。 ??在 64 位的 Java 虛擬機中,,對象頭的標(biāo)記字段占 64 位,,而類型指針又占了 64 位。也就是說,,每一個 Java 對象在內(nèi)存中的額外開銷就是 16 個字節(jié),。為了盡量較少對象的內(nèi)存使用量,64 位 Java 虛擬機引入了壓縮指針 的概念(對應(yīng)虛擬機選項 -XX:+UseCompressedOops,,默認開啟),,將堆中原本 64 位的 Java 對象類型指針壓縮成 32 位,這樣對象頭就只占用 12位(原來占用16位)。 ??默認情況下,,Java 虛擬機堆中對象的起始地址需要對齊至 8 的倍數(shù),。如果一個對象用不到 8N 個字節(jié),那么空白的那部分空間就浪費掉了,。這些浪費掉的空間我們稱之為對象間的填充(padding),。 &essp;?內(nèi)存對齊不僅存在于對象與對象之間,也存在于對象中的字段之間,。比如說,,Java 虛擬機要求 long 字段、double 字段,,以及非壓縮指針狀態(tài)下的引用字段地址為 8 的倍數(shù),。 ??在默認情況下,Java 虛擬機中的32 位壓縮指針可以尋址到 2 的 35 次方個字節(jié),,也就是 32GB 的地址空間(超過 32GB 則會關(guān)閉壓縮指針),。 具體的內(nèi)存布局可以參考:https://www.jianshu.com/p/3d38cba67f8b JVM垃圾回收??目前 Java 虛擬機的主流垃圾回收器采取的是可達性分析算法。這個算法的實質(zhì)在于將一系列 GC Roots 作為初始的存活對象合集(live set),,然后從該合集出發(fā),,探索所有能夠被該集合引用到的對象,并將其加入到該集合中,,這個過程我們也稱之為標(biāo)記(mark),。最終,未被探索到的對象便是死亡的,,是可以回收的,。GC Roots 包括(但不限于)如下幾種: 1:Java 方法棧楨中的局部變量; 2:已加載類的靜態(tài)變量,; 3:JNI handles,; 4:已啟動且未停止的 Java 線程。 ??Java 虛擬機中的 Stop-the-world 是通過安全點(safepoint)機制來實現(xiàn)的,。當(dāng) Java 虛擬機收到 Stop-the-world 請求,,它便會等待所有的線程都到達安全點,才允許請求 Stop-the-world 的線程進行獨占的工作,。安全點的初始目的并不是讓其他線程停下,,而是找到一個穩(wěn)定的執(zhí)行狀態(tài)。在這個執(zhí)行狀態(tài)下,,Java 虛擬機的堆棧不會發(fā)生變化,。這么一來,垃圾回收器便能夠“安全”地執(zhí)行可達性分析,。 ??回收死亡對象的內(nèi)存共有三種方式,,分別為:會造成內(nèi)存碎片的清除,、性能開銷較大的壓縮、以及堆使用效率較低的復(fù)制,。 ??Java 虛擬機將堆劃分為新生代和老年代,。其中,新生代又被劃分為 Eden 區(qū),,以及兩個大小相同的 Survivor 區(qū),。如下圖所示: 堆空間是線程共享的,JVM通過為每個線程預(yù)分配一塊空間來避免線程間申請內(nèi)存發(fā)生沖突,。這項技術(shù)被稱之為 TLAB(Thread Local Allocation Buffer,對應(yīng)虛擬機參數(shù) -XX:+UseTLAB,,默認開啟),。 ??Java 虛擬機會記錄 Survivor 區(qū)中的對象一共被來回復(fù)制了幾次。如果一個對象被復(fù)制的次數(shù)為 15(對應(yīng)虛擬機參數(shù) -XX:+MaxTenuringThreshold),,那么該對象將被晉升(promote)至老年代,。另外,如果單個 Survivor 區(qū)已經(jīng)被占用了 50%(對應(yīng)虛擬機參數(shù) -XX:TargetSurvivorRatio),,那么較高復(fù)制次數(shù)的對象也會被晉升至老年代,。 ??因為 Minor GC 只針對新生代進行垃圾回收,所以在枚舉 GC Roots 的時候,,它需要考慮從老年代到新生代的引用,。為了避免掃描整個老年代,Java 虛擬機引入了名為卡表的技術(shù),,大致地標(biāo)出可能存在老年代到新生代引用的內(nèi)存區(qū)域,。JVM如下區(qū)域會發(fā)生OutOfMemoryError- 堆內(nèi)存不足是最常見的 OOM 原因之一,拋出的錯誤信息是“java.lang.OutOfMemoryError:Java heap space”,。
- 而對于 Java 虛擬機棧和本地方法棧,,如果我們寫一段程序不斷的進行遞歸調(diào)用,而且沒有退出條件,,就會導(dǎo)致不斷地進行壓棧,。類似這種情況,JVM 實際會拋出 StackOverFlowError,。
- 對于老版本的 Oracle JDK,,因為永久代的大小是有限的,并且 JVM 對永久代垃圾回收(如,,常量池回收,、卸載不再需要的類型)非常不積極,所以當(dāng)我們不斷添加新類型的時候,,永久代出現(xiàn) OutOfMemoryError 也非常多見,,尤其是在運行時存在大量動態(tài)類型生成的場合,;類似 Intern 字符串緩存占用太多空間,也會導(dǎo)致 OOM 問題,。對應(yīng)的異常信息,,會標(biāo)記出來和永久代相關(guān):“java.lang.OutOfMemoryError: PermGen space”。
- 隨著元數(shù)據(jù)區(qū)的引入,,方法區(qū)內(nèi)存已經(jīng)不再那么窘迫,,所以相應(yīng)的 OOM 有所改觀,出現(xiàn) OOM,,異常信息則變成了:“java.lang.OutOfMemoryError: Metaspace”,。
- 程序計數(shù)器是唯一一塊不會拋出內(nèi)存OutOfMemoryError 的區(qū)域,程序計數(shù)器會存儲當(dāng)前線程正在執(zhí)行的 Java 方法的 JVM 指令地址,;或者,,如果是在執(zhí)行本地方法,則是未指定值(undefined),。
Java內(nèi)存模型??即時編譯器(和處理器)需要保證程序能夠遵守 as-if-serial 屬性,。通俗地說,就是在單線程情況下,,要給程序一個順序執(zhí)行的假象,。即經(jīng)過重排序的執(zhí)行結(jié)果要與順序執(zhí)行的結(jié)果保持一致。但這在多線程執(zhí)行的情況下,,就有可能出現(xiàn)意想不到的結(jié)果,。 ??Java 內(nèi)存模型通過定義了一系列的 happens-before 操作,讓應(yīng)用程序開發(fā)者能夠輕易地表達不同線程的操作之間的內(nèi)存可見性,。 Java 內(nèi)存模型還定義了下述線程間的 happens-before 關(guān)系,。 1:解鎖操作 happens-before 之后(這里指時鐘順序先后)對同一把鎖的加鎖操作。 2:volatile 字段的寫操作 happens-before 之后(這里指時鐘順序先后)對同一字段的讀操作,。 3:線程的啟動操作(即 Thread.starts()) happens-before 該線程的第一個操作,。 4:線程的最后一個操作 happens-before 它的終止事件(即其他線程通過 Thread.isAlive() 或 Thread.join() 判斷該線程是否中止)。 5:線程對其他線程的中斷操作 happens-before 被中斷線程所收到的中斷事件(即被中斷線程的 InterruptedException 異常,,或者第三個線程針對被中斷線程的 Thread.interrupted 或者 Thread.isInterrupted 調(diào)用),。 6:構(gòu)造器中的最后一個操作 happens-before 析構(gòu)器的第一個操作。 ??在遵守 Java 內(nèi)存模型的前提下,,即時編譯器以及底層體系架構(gòu)能夠調(diào)整內(nèi)存訪問操作,,以達到性能優(yōu)化的效果。如果開發(fā)者沒有正確地利用 happens-before 規(guī)則,,那么將可能導(dǎo)致數(shù)據(jù)競爭,。 ??Java 內(nèi)存模型是通過內(nèi)存屏障來禁止重排序的。對于即時編譯器來說,,內(nèi)存屏障將限制它所能做的重排序優(yōu)化,。對于處理器來說,,內(nèi)存屏障會導(dǎo)致緩存的刷新操作。 Java基本類型基本類型如下圖: 盡管他們的默認值看起來不一樣,,但在內(nèi)存中都是 0,。- 在 Java 虛擬機規(guī)范中,boolean 類型被映射成 int 類型,。具體來說,,“true”被映射為整數(shù) 1,而“false”被映射為整數(shù) 0,。Java 代碼中的邏輯運算以及條件跳轉(zhuǎn),,都是用整數(shù)相關(guān)的字節(jié)碼來實現(xiàn)的。
- Java 的浮點類型采用 IEEE 754 浮點數(shù)格式,。
- 除 long 和 double 外,,其他基本類型與引用類型在解釋執(zhí)行的方法棧幀中占用的大小是一致的(32位JVM占4個字節(jié),64位JVM占8個字節(jié)),,但它們在堆中占用的大小的確不同,。在將 boolean、byte、char 以及 short 的值存入字段或者數(shù)組(存放堆數(shù)據(jù)時)單元時,,Java 虛擬機會進行掩碼操作。在讀取時,,Java 虛擬機則會將其擴展為 int 類型boolean與char因為沒符號,,高位直接以零填充,byte和short因為有符號,,以符號位填充,。
- boolean 字段和 boolean 數(shù)組比較特殊。在 HotSpot 中,,boolean 字段占用一字節(jié),,而 boolean 數(shù)組則直接用 byte 數(shù)組來實現(xiàn)。為了保證堆中的 boolean 值是合法的,,HotSpot 在存儲時顯式地進行掩碼操作,,也就是說,只取最后一位的值存入 boolean 字段或數(shù)組中,。
JVM實現(xiàn)反射??在默認情況下,,方法的反射調(diào)用為委派實現(xiàn),委派給本地實現(xiàn)來進行方法調(diào)用,。在調(diào)用超過 15 次之后(可以通過 -Dsun.reflect.inflationThreshold= 來調(diào)整),,委派實現(xiàn)便會將委派對象切換至動態(tài)實現(xiàn)。這個動態(tài)實現(xiàn)的字節(jié)碼是自動生成的,,它將直接使用 invoke 指令來調(diào)用目標(biāo)方法,。動態(tài)實現(xiàn)和本地實現(xiàn)相比,,其運行效率要快上 20 倍 。這是因為動態(tài)實現(xiàn)無需經(jīng)過 Java 到 C++ 再到 Java 的切換,,但由于生成字節(jié)碼十分耗時,,僅調(diào)用一次的話,反而是本地實現(xiàn)要快上 3 到 4 倍,。反射調(diào)用的 Inflation 機制是可以通過參數(shù)(-Dsun.reflect.noInflation=true)來關(guān)閉的,。這樣一來,在反射調(diào)用一開始便會直接生成動態(tài)實現(xiàn),,而不會使用委派實現(xiàn)或者本地實現(xiàn),。 ??方法的反射調(diào)用會帶來不少性能開銷,原因主要有三個:變長參數(shù)方法導(dǎo)致的 Object 數(shù)組,,基本類型的自動裝箱,、拆箱,還有最重要的方法內(nèi)聯(lián),。 JVM實現(xiàn)synchronized??當(dāng)聲明 synchronized 代碼塊時,,編譯而成的字節(jié)碼將包含 monitorenter 和 monitorexit 指令。這兩種指令均會消耗操作數(shù)棧上的一個引用類型的元素(也就是 synchronized 關(guān)鍵字括號里的引用),,作為所要加鎖解鎖的鎖對象,。 ??關(guān)于 monitorenter 和 monitorexit 的作用,我們可以抽象地理解為每個鎖對象擁有一個鎖計數(shù)器和一個指向持有該鎖的線程的指針,。當(dāng)執(zhí)行 monitorenter 時,,如果目標(biāo)鎖對象的計數(shù)器為 0,那么說明它沒有被其他線程所持有,。在這個情況下,,Java 虛擬機會將該鎖對象的持有線程設(shè)置為當(dāng)前線程,并且將其計數(shù)器加 1,。在目標(biāo)鎖對象的計數(shù)器不為 0 的情況下,,如果鎖對象的持有線程是當(dāng)前線程,那么 Java 虛擬機可以將其計數(shù)器加 1,,否則需要等待,,直至持有線程釋放該鎖。當(dāng)執(zhí)行 monitorexit 時,,Java 虛擬機則需將鎖對象的計數(shù)器減 1,。當(dāng)計數(shù)器減為 0 時,那便代表該鎖已經(jīng)被釋放掉了,。HotSpot 虛擬機中具體的鎖實現(xiàn)分為: - 重量級鎖: Java 虛擬機中最為基礎(chǔ)的鎖實現(xiàn),。在這種狀態(tài)下,Java 虛擬機會阻塞加鎖失敗的線程,,并且在目標(biāo)鎖被釋放的時候,,喚醒這些線程,。Java 線程的阻塞以及喚醒,都是依靠操作系統(tǒng)來完成的開銷非常大,。為了盡量避免昂貴的線程阻塞,、喚醒操作,Java 虛擬機會在線程進入阻塞狀態(tài)之前,,以及被喚醒后競爭不到鎖的情況下,,進入自旋狀態(tài),在處理器上空跑并且輪詢鎖是否被釋放,。如果此時鎖恰好被釋放了,,那么當(dāng)前線程便無須進入阻塞狀態(tài),而是直接獲得這把鎖,。
- 輕量級鎖:對象頭中的標(biāo)記字段(mark word),。它的最后兩位便被用來表示該對象的鎖狀態(tài)。其中,,00 代表輕量級鎖,,01 代表無鎖(或偏向鎖),10 代表重量級鎖,,11 則跟垃圾回收算法的標(biāo)記有關(guān),。當(dāng)進行加鎖操作時,Java 虛擬機會判斷是否已經(jīng)是重量級鎖,。如果不是,它會在當(dāng)前線程的當(dāng)前棧楨中劃出一塊空間,,作為該鎖的鎖記錄,,并且將鎖對象的標(biāo)記字段復(fù)制到該鎖記錄中。然后,,Java 虛擬機會嘗試用 CAS(compare-and-swap)操作將鎖對象的標(biāo)記字段替換為一個指針,,指向當(dāng)前線程棧上的一塊空間,存儲著鎖對象原本的標(biāo)記字段,。
- 偏向鎖:在線程進行加鎖時,,如果該鎖對象支持偏向鎖,那么 Java 虛擬機會通過 CAS 操作,,將當(dāng)前線程的地址記錄在鎖對象的標(biāo)記字段之中,,并且將標(biāo)記字段的最后三位設(shè)置為 101。
編譯器橋接方法??對于 Java 語言中重寫而 Java 虛擬機中非重寫的情況,,編譯器會通過生成橋接方法來實現(xiàn) Java 中的重寫語義,。下機的圖可以通過字節(jié)碼看出是如何實現(xiàn)的: 方法內(nèi)聯(lián)方法內(nèi)聯(lián)是指:在編譯過程中遇到方法調(diào)用時,將目標(biāo)方法的方法體納入編譯范圍之中,,并取代原方法調(diào)用的優(yōu)化手段,。以 getter/setter 為例,,如果沒有方法內(nèi)聯(lián),在調(diào)用 getter/setter 時,,程序需要保存當(dāng)前方法的執(zhí)行位置,,創(chuàng)建并壓入用于 getter/setter 的棧幀、訪問字段,、彈出棧幀,,最后再恢復(fù)當(dāng)前方法的執(zhí)行。而當(dāng)內(nèi)聯(lián)了對 getter/setter 的方法調(diào)用后,,上述操作僅剩字段訪問,。 即時編譯??通常而言,代碼會先被 Java 虛擬機解釋執(zhí)行,,之后反復(fù)執(zhí)行的熱點代碼則會被即時編譯成為機器碼,,直接運行在底層硬件之上。即時編譯器有C1,C2,Grral,。 - C1:通過-client指定,,通常運用于執(zhí)行時間較短,對啟動性能有要求的程序,。
- C2:通過-server指定,,對峰值性能有要求的程序,C2比C1的執(zhí)行效率更快,,但是編譯時間更久,。
- Grral是一個實驗性質(zhì)的編譯器,通過參數(shù) -XX:+UnlockExperimentalVMOptions 啟用,。
- Java 7 引入了分層編譯(對應(yīng)參數(shù) -XX:+TieredCompilation)的概念,,綜合了 C1 的啟動性能優(yōu)勢和 C2 的峰值性能優(yōu)勢。從 Java 8 開始,,Java 虛擬機默認采用分層編譯的方式,。它將執(zhí)行分為五個層次,
1:0 層解釋執(zhí)行(也會收集程序的profiling),; 2:1 層執(zhí)行沒有 profiling 的 C1 代碼,; 3:2 層執(zhí)行部分 profiling 的 C1 代碼; 4:3 層執(zhí)行全部 profiling 的 C1 代碼,; 5: 4 層執(zhí)行 C2 代碼,。 其中profiling為運行時的程序的執(zhí)行狀態(tài)數(shù)據(jù),比如循環(huán)調(diào)用的次數(shù),,方法調(diào)用的次數(shù),分支跳轉(zhuǎn)次數(shù),,類型轉(zhuǎn)換次數(shù)等。 - 即時編譯是由方法調(diào)用計數(shù)器和循環(huán)回邊計數(shù)器觸發(fā)的。在使用分層編譯的情況下,,觸發(fā)編譯的閾值是根據(jù)當(dāng)前待編譯的方法數(shù)目動態(tài)調(diào)整的,。
- 基于分支 profile 的優(yōu)化以及基于類型 profile 的優(yōu)化都將對程序今后的執(zhí)行作出假設(shè)。這些假設(shè)將精簡所要編譯的代碼的控制流以及數(shù)據(jù)流,。在假設(shè)失敗的情況下,,Java 虛擬機將采取去優(yōu)化(從執(zhí)行即時編譯生成的機器碼切換回解釋執(zhí)行),退回至解釋執(zhí)行并重新收集相關(guān)的 profile,。
逃逸分析??逃逸分析將判斷新建的對象是否逃逸,。即時編譯器判斷對象是否逃逸的依據(jù),一是對象是否被存入堆中(靜態(tài)字段或者堆中對象的實例字段),,二是對象是否被傳入未知代碼中,。 當(dāng)發(fā)現(xiàn)一個對象只在某個方法里,或者這個方法的內(nèi)聯(lián)方法里,,則可以認為這個對象是逃逸的,。主要的優(yōu)化有:1:鎖消除,對逃逸的對象加鎖是沒有意義的,。2:采用標(biāo)量替換的技術(shù)將需要分配在堆上的對象直接在棧上采用變量的方式進行替換,。
|