免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
JVM學(xué)習(xí)筆記

本文所有內(nèi)容來于:http://stuq.com/a/100ww

java代碼是如何執(zhí)行的
  • java代碼是運行于java虛擬機上的,通過java虛擬機實現(xiàn)了跨平臺,并且java虛擬機幫助程序員處理了容易出錯的事務(wù),比如內(nèi)存管理。
  • java虛擬機會解釋執(zhí)行java字節(jié)碼,并且對于熱點代碼會采用即時編譯(Just-In-Time compilation,,JIT),即將一個方法中包含的所有字節(jié)碼編譯成機器碼后再執(zhí)行。如下圖所示:

    JVM如何執(zhí)行字節(jié)碼
  • Java 虛擬機將運行時內(nèi)存區(qū)域劃分為五個部分,分別為方法區(qū)、堆、PC 寄存器、Java 方法棧和本地方法棧。如下圖所示:

    JVM內(nèi)存劃分
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í)行的過程。分為驗證,準備,解析。
    1:驗證階段:主要是保證加載的類滿足JVM的約束,也是為了保證JVM的安全性。
    2:準備階段:為被加載類的靜態(tài)字段分配內(nèi)存。只是分配內(nèi)存,具體的初使化,則在初使化階段。在這個階段也會構(gòu)造類的方法表。
    3:解析階段(非必須):在 class 文件被加載至 Java 虛擬機之前,這個類無法知道其他類及其方法、字段所對應(yīng)的具體地址,甚至不知道自己方法、字段的地址。因此,每當(dāng)需要引用這些成員時,Java 編譯器會生成一個符號引用。在運行階段,這個符號引用一般都能夠無歧義地定位到具體目標上。解析階段的目的,正是將這些符號引用解析成為實際引用。如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那么解析將觸發(fā)這個類的加載(但未必觸發(fā)這個類的鏈接以及初始化。)
  • 初使化:初使化靜態(tài)變量(由static修飾的變量)并執(zhí)行static代碼塊。所有的static代碼塊會放到同一方法中,并命名為,這個方法會由JVM加鎖保證同步。類的初使化時機:
    1:當(dāng)虛擬機啟動時,初始化用戶指定的主類;
    2:當(dāng)遇到用以新建目標類實例的 new 指令時,初始化 new 指令的目標類;
    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 虛擬機能夠準確地識別目標方法。
??Java 虛擬機中關(guān)于方法重寫的判定同樣基于方法描述符。也就是說,如果子類定義了與父類中非私有、非靜態(tài)方法同名的方法,那么只有當(dāng)這兩個方法的參數(shù)類型以及返回類型一致,Java 虛擬機才會判定為重寫。對于 Java 語言中重寫而 Java 虛擬機中非重寫的情況,編譯器會通過生成橋接方法來實現(xiàn) Java 中的重寫語義。
??Java 虛擬機中的靜態(tài)綁定指的是在解析時便能夠直接識別目標方法的情況,而動態(tài)綁定則指的是需要在運行過程中根據(jù)調(diào)用者的動態(tài)類型來識別目標方法的情況。
??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)方法。
??在編譯過程中,我們并不知道目標方法的具體內(nèi)存地址。因此,Java 編譯器會暫時用符號引用來表示該目標方法。這一符號引用包括目標方法所在的類或接口的名字,以及目標方法的方法名和方法描述符。符號引用存儲在 class 文件的常量池之中。根據(jù)目標方法是否為接口方法,這些引用可分為接口符號引用和非接口符號引用。如果虛方法(invokevirtual)調(diào)用指向一個標記為 final 的方法,那么Java虛擬機也可以靜態(tài)綁定該虛方法調(diào)用的目標方法。
??Java 虛擬機中采取了一種用空間換取時間的策略來實現(xiàn)動態(tài)綁定。它為每個類生成一張方法表(類加載的鏈接階段實現(xiàn)),用以快速定位目標方法。方法表分為虛方法表(invokevirtual調(diào)用)與接口方法表(invokeinterface)調(diào)用。方法表本質(zhì)上是一個數(shù)組,每個數(shù)組元素指向一個當(dāng)前類及其祖先類中非私有的實例方法。方法表滿足兩個特質(zhì):

  • 子類方法表中包含父類方法表中的所有方法;
  • 子類方法在方法表中的索引值,與它所重寫的父類方法的索引值相同。

??方法調(diào)用指令中的符號引用會在執(zhí)行之前解析成實際引用。對于靜態(tài)綁定的方法調(diào)用而言,實際引用將指向具體的目標方法。對于動態(tài)綁定的方法調(diào)用而言,實際引用則是方法表的索引值(實際上并不僅是索引值)。在執(zhí)行過程中,Java 虛擬機將獲取調(diào)用者的實際類型,并在該實際類型的虛方法表中,根據(jù)索引值獲得目標方法。這個過程便是動態(tài)綁定。Java 虛擬機中的即時編譯器會使用內(nèi)聯(lián)緩存來加速動態(tài)綁定。Java 虛擬機所采用的單態(tài)內(nèi)聯(lián)緩存將紀錄調(diào)用者的動態(tài)類型,以及它所對應(yīng)的目標方法。當(dāng)碰到新的調(diào)用者時,如果其動態(tài)類型與緩存中的類型匹配,則直接調(dià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í)行路徑的出口中。

finally字節(jié)碼

對象的內(nèi)存布局

??通過 new 指令新建出來的對象(分存在堆中),它的內(nèi)存其實涵蓋了所有父類中的實例字段。也就是說,雖然子類無法訪問父類的私有實例字段,或者子類的實例字段隱藏了父類的同名實例字段,但是子類的實例還是會為這些父類實例字段分配內(nèi)存的。
??在 Java 虛擬機中,每個 Java 對象都有一個對象頭(object header),這個由標記字段(Mark Word)和類型指針所構(gòu)成。其中,標記字段用以存儲 Java 虛擬機有關(guān)該對象的運行數(shù)據(jù),如哈希碼、GC 信息以及鎖信息,而類型指針則指向該對象的類。
??在 64 位的 Java 虛擬機中,對象頭的標記字段占 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ā),探索所有能夠被該集合引用到的對象,并將其加入到該集合中,這個過程我們也稱之為標記(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ū)。如下圖所示:

堆內(nèi)存劃分
堆空間是線程共享的,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ù),大致地標出可能存在老年代到新生代引用的內(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)的異常信息,會標記出來和永久代相關(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基本類型

基本類型如下圖:

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)用目標方法。動態(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 時,如果目標鎖對象的計數(shù)器為 0,那么說明它沒有被其他線程所持有。在這個情況下,Java 虛擬機會將該鎖對象的持有線程設(shè)置為當(dāng)前線程,并且將其計數(shù)器加 1。在目標鎖對象的計數(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 虛擬機會阻塞加鎖失敗的線程,并且在目標鎖被釋放的時候,喚醒這些線程。Java 線程的阻塞以及喚醒,都是依靠操作系統(tǒng)來完成的開銷非常大。為了盡量避免昂貴的線程阻塞、喚醒操作,Java 虛擬機會在線程進入阻塞狀態(tài)之前,以及被喚醒后競爭不到鎖的情況下,進入自旋狀態(tài),在處理器上空跑并且輪詢鎖是否被釋放。如果此時鎖恰好被釋放了,那么當(dāng)前線程便無須進入阻塞狀態(tài),而是直接獲得這把鎖。
  • 輕量級鎖:對象頭中的標記字段(mark word)。它的最后兩位便被用來表示該對象的鎖狀態(tài)。其中,00 代表輕量級鎖,01 代表無鎖(或偏向鎖),10 代表重量級鎖,11 則跟垃圾回收算法的標記有關(guān)。當(dāng)進行加鎖操作時,Java 虛擬機會判斷是否已經(jīng)是重量級鎖。如果不是,它會在當(dāng)前線程的當(dāng)前棧楨中劃出一塊空間,作為該鎖的鎖記錄,并且將鎖對象的標記字段復(fù)制到該鎖記錄中。然后,Java 虛擬機會嘗試用 CAS(compare-and-swap)操作將鎖對象的標記字段替換為一個指針,指向當(dāng)前線程棧上的一塊空間,存儲著鎖對象原本的標記字段
  • 偏向鎖:在線程進行加鎖時,如果該鎖對象支持偏向鎖,那么 Java 虛擬機會通過 CAS 操作,將當(dāng)前線程的地址記錄在鎖對象的標記字段之中,并且將標記字段的最后三位設(shè)置為 101。
編譯器橋接方法

??對于 Java 語言中重寫而 Java 虛擬機中非重寫的情況,編譯器會通過生成橋接方法來實現(xiàn) Java 中的重寫語義。下機的圖可以通過字節(jié)碼看出是如何實現(xiàn)的:

java class 實現(xiàn)橋接方法
方法內(nèi)聯(lián)

方法內(nèi)聯(lián)是指:在編譯過程中遇到方法調(dià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:采用標量替換的技術(shù)將需要分配在堆上的對象直接在棧上采用變量的方式進行替換。

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
JVM概述
資深架構(gòu)師技術(shù)分享:JVM內(nèi)存結(jié)構(gòu)與內(nèi)存模型
JVM內(nèi)存結(jié)構(gòu)
JVM 詳解,大白話帶你認識 JVM
JVM總結(jié) ----JVM體系結(jié)構(gòu)
面試阿里高級架構(gòu)師之JVM篇
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服