JVM_2

虛擬機(jī)類(lèi)加載機(jī)制

虛擬機(jī)類(lèi)加載過(guò)程:Java虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可用被虛擬機(jī)直接使用的Java類(lèi)型

類(lèi)的生命周期

解析階段在某些情況可以在初始化階段之后開(kāi)始,為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定特性(也稱(chēng)為動(dòng)態(tài)綁定或晚期綁定)

這些階段通常是互相交叉地混合進(jìn)行,只不過(guò)會(huì)在一個(gè)階段執(zhí)行地過(guò)程中調(diào)用、激活另一個(gè)階段

主動(dòng)引用:必須立即對(duì)類(lèi)進(jìn)行“初始化”的有且僅有六種情況(加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開(kāi)始)

  1. 遇到new、getstatic、putstatic或invokestatic這四條字節(jié)碼指令時(shí),如果類(lèi)型沒(méi)有進(jìn)行初始化過(guò),則需要先觸發(fā)初始化階段。生成這四個(gè)關(guān)鍵字的場(chǎng)景有:
    • 使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候
    • 讀取或設(shè)置一個(gè)類(lèi)型的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)
    • 調(diào)用一個(gè)類(lèi)型的靜態(tài)方法的時(shí)候
  2. 調(diào)用java.lang.reflect包的方法對(duì)類(lèi)型進(jìn)行反射調(diào)用的時(shí)候,如果類(lèi)型沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化
  3. 當(dāng)初始化類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類(lèi)的初始化
  4. 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含main()方法的哪個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)類(lèi)
  5. 當(dāng)使用JDK7新加入的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類(lèi)型的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化
  6. 當(dāng)一個(gè)接口中定義了JDK8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí),如果有這個(gè)接口的實(shí)現(xiàn)類(lèi)發(fā)生了初始化,那該接口要在其之前被初始化

被動(dòng)引用

  • 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,不會(huì)導(dǎo)致子類(lèi)初始化
    package chapter7;
    
    /**
     * VM args:-XX:+TraceClassLoading 
     * 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,子類(lèi)不會(huì)初始化
     */
    
    public class NotInitialization1{
      public static void main(String[] args) {
          System.out.println(Subclass.value);
      }
    }
    
    class SuperClass {
      static {
          System.out.println("SuperClass init!");
      }
      public static int value = 123;
    }
    class Subclass extends SuperClass{
      static {
          System.out.println("SubClass init!");
      }
     }
    
    

子類(lèi)加載但沒(méi)有初始化

  • 通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化

    package chapter7;
    
    /**
     *通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化
     */
    
    public class NotInitialization2{
      public static void main(String[] args) {
          SuperClass[] sca = new SuperClass[10];
      }
    }
    
    

    沒(méi)有輸出“SuperClass init!”

  • 常量在編譯階段會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上沒(méi)有直接引用到定義常量的類(lèi),因此不會(huì)觸發(fā)定義常量的類(lèi)的初始化

    package chapter7;
    
    public class NotInitiallization3 {
      public static void main(String[] args) {
          System.out.println(ConstClass.HELLOWORLD);
      }
    }
    
    class ConstClass{
     static {
         System.out.println("ConstClass init!");
     }
     public static final String HELLOWORLD = "hello world";
    }
    
    

    沒(méi)有輸出“ConstClass init!”,其實(shí)在編譯階段通過(guò)常量傳播優(yōu)化,已經(jīng)將此常量的值直接存儲(chǔ)在NotInitiallization3的常量池,因此ConstClass.HELLOWORLD實(shí)際上是對(duì)自身常量池的引用。

接口的初始化與類(lèi)的初始化有所不同(有且僅有情況第三條),一個(gè)接口初始化時(shí),并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時(shí)候(如引用接口中定義的常量)才會(huì)初始化

類(lèi)加載過(guò)程

加載

加載階段要完成的三件事情

  • 通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取定義此類(lèi)的二進(jìn)制字節(jié)流
  • 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
  • 在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪問(wèn)入口

非數(shù)組類(lèi)型的加載階段(準(zhǔn)確地說(shuō),是加載階段中獲取類(lèi)的二進(jìn)制字節(jié)流的動(dòng)作)是開(kāi)發(fā)人員可控性最強(qiáng)的階段。加載階段既可以使用Java虛擬機(jī)里內(nèi)置的引導(dǎo)類(lèi)加載器來(lái)完成,也可以由用戶(hù)自定義的類(lèi)加載器去完成,開(kāi)發(fā)人員通過(guò)定義自己的類(lèi)加載器去控制字節(jié)流的獲取方式(重寫(xiě)一個(gè)類(lèi)加載器的findClass()或loadClass()方法),實(shí)現(xiàn)根據(jù)自己的想法來(lái)賦予應(yīng)用程序獲取運(yùn)行代碼的動(dòng)態(tài)性

加載階段和連接階段的部分動(dòng)作(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開(kāi)始,這兩個(gè)階段的開(kāi)始時(shí)間仍然保持著固定的先后順序

驗(yàn)證

確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全

驗(yàn)證四階段:

  1. 文件格式驗(yàn)證
    驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理

    • 是否以魔數(shù)0xCAFEBABE開(kāi)頭
    • 主、次版本號(hào)是否在當(dāng)前Java虛擬機(jī)接受范圍之內(nèi)
    • 常量池的常量中是否有不被支持的常量類(lèi)型(檢查常量tag標(biāo)志)
    • 指向常量的各種索引值中是否有指向不存在的常量或不符合類(lèi)型的常量
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數(shù)據(jù)
    • Class文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息
    • .......

    該驗(yàn)證階段是基于二進(jìn)制字節(jié)流進(jìn)行的,的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類(lèi)型信息的要求

    通過(guò)此階段后這段字節(jié)流進(jìn)入方法區(qū)存儲(chǔ),后面的三個(gè)階段都是基于方法去的存儲(chǔ)結(jié)構(gòu)進(jìn)行,不會(huì)再讀取、操作字節(jié)流

  2. 元數(shù)據(jù)驗(yàn)證
    對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,保證其描述的信息符合《Java語(yǔ)言規(guī)范》的要求

    • 這個(gè)類(lèi)是否有父類(lèi)(除了java.lang.Object外,所有的類(lèi)都應(yīng)當(dāng)有父類(lèi))
    • 這個(gè)類(lèi)是否繼承了不允許被繼承的類(lèi)(被final修飾的類(lèi))
    • 如果這個(gè)類(lèi)不是抽象類(lèi),是否實(shí)現(xiàn)類(lèi)其父類(lèi)或接口之中要求實(shí)現(xiàn)的所有方法
    • 類(lèi)中的字段、方法是否與父類(lèi)產(chǎn)生矛盾(例如覆蓋了父類(lèi)的final字段,或者出現(xiàn)不符合規(guī)則的方法重載,例如方法參數(shù)都一致,但返回值類(lèi)型卻不同等)
    • .......

    主要目的是對(duì)類(lèi)的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn),保證不存在與《Java語(yǔ)言規(guī)范》定義相悖的元數(shù)據(jù)信息

  3. 字節(jié)碼驗(yàn)證
    對(duì)類(lèi)的方法體(Class文件中的Code屬性)進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類(lèi)的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為

    • 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類(lèi)型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)類(lèi)似于“在操作棧放置了一個(gè)int類(lèi)型的數(shù)據(jù),使用時(shí)卻按long類(lèi)型來(lái)加載入本地變量表中”這樣的情況
    • 保證任何跳轉(zhuǎn)指令都不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
    • 保證方法體中的類(lèi)型轉(zhuǎn)換總是有效的,例如可以把一個(gè)子類(lèi)對(duì)象賦值給父類(lèi)數(shù)據(jù)類(lèi)型,這是安全的,但是把父類(lèi)對(duì)象賦值給子類(lèi)數(shù)據(jù)類(lèi)型,甚至把對(duì)象賦值給與它毫無(wú)繼承關(guān)系、完全不相干的一個(gè)數(shù)據(jù)類(lèi)型,則是危險(xiǎn)和不合法的
    • .......

    主要目的是通過(guò)數(shù)據(jù)流分析和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的

  4. 符號(hào)引用驗(yàn)證
    發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段(解析)發(fā)生。檢查該類(lèi)是否缺少或者被禁止訪問(wèn)它依賴(lài)的某些外部類(lèi)、方法、字段等資源

    • 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類(lèi)
    • 在指定類(lèi)中是否存在符合方法的字段描述及簡(jiǎn)單名稱(chēng)所描述的方法和字段
    • 符號(hào)引用中的類(lèi)、字段、方法的可訪問(wèn)性(private、protected、public、<package>)是否可被當(dāng)前類(lèi)訪問(wèn)
    • ........

    主要目的是確保解析行為能正常執(zhí)行,如果不通過(guò),則拋出一個(gè)java.lang.IncompatibleClassChangeError(java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等)

    如果程序運(yùn)行的全部代碼都已經(jīng)被反復(fù)使用和驗(yàn)證過(guò),在生產(chǎn)環(huán)境的實(shí)施階段可以考慮-Xverify:none參數(shù)來(lái)關(guān)閉大部分的類(lèi)的驗(yàn)證措施

準(zhǔn)備

準(zhǔn)備階段是正式為類(lèi)中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段

這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量,而不包括實(shí)例變量,實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中

public static int value = 123;
變量value在準(zhǔn)備階段過(guò)后的初始值為0而不是123,因?yàn)檫@時(shí)還未執(zhí)行任何Java方法,而把value賦值為123的putstatic指令時(shí)程序被編譯后,存放于類(lèi)構(gòu)造器<clinit>()方法中,所以這個(gè)賦值動(dòng)作是要到類(lèi)的初始化階段才會(huì)完成

基本數(shù)據(jù)類(lèi)型的零值

數(shù)據(jù)類(lèi)型 零值 數(shù)據(jù)類(lèi)型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char '\u0000' reference null
byte (byte)0

如果類(lèi)字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量值就會(huì)被初始化為ConstantValue屬性所指定的初始值,public static final int value = 123;,在準(zhǔn)備階段value的值就為123

解析

Java虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程

  • 符號(hào)引用
    符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)無(wú)歧義地定位到目標(biāo)即可,與虛擬機(jī)內(nèi)存布局無(wú)關(guān),引用的目標(biāo)不一定已經(jīng)加載到虛擬機(jī)內(nèi)存
  • 直接引用
    直接引用時(shí)可以直接指向目標(biāo)的指針、相對(duì)偏移量或者是一個(gè)能間接定位到目標(biāo)的句柄,與虛擬機(jī)內(nèi)存布局相關(guān),引用的目標(biāo)必定已經(jīng)在虛擬機(jī)內(nèi)存存在

對(duì)方法或者字段的訪問(wèn),也會(huì)在解析階段中對(duì)它們的可訪問(wèn)性(private、protected、public、<package>)進(jìn)行檢查

解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用點(diǎn)限定符這7類(lèi)符號(hào)引用進(jìn)行

初始化

初始化階段,會(huì)根據(jù)程序員通過(guò)程序編碼指定的主觀計(jì)劃去初始化類(lèi)變量和其他資源。

初始化階段就是執(zhí)行類(lèi)構(gòu)造器<clinit>()方法的過(guò)程,<clinit>()是Javac編譯器的自動(dòng)生成物

  • <clinit>()方法是由編譯器自動(dòng)收集類(lèi)中的所有變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊(static{}塊)中的語(yǔ)句合并產(chǎn)生的,收集順序是由語(yǔ)句在源文件中出現(xiàn)的順序決定的,靜態(tài)語(yǔ)句塊中之內(nèi)訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,靜態(tài)語(yǔ)句塊可以賦值,但是不能訪問(wèn)
    public class Test {
        static {
            i = 0;  // 給變量賦值可以正常編譯通過(guò)
            System.out.print(i);  // 這句編譯器會(huì)提示“非法前向引用”
        }
        static int i = 1;
    }
    
  • <clinit>()方法與類(lèi)的構(gòu)造函數(shù)(即在虛擬機(jī)視角中的實(shí)例構(gòu)造器<init>()方法)不同,它不需要顯示地調(diào)用父類(lèi)構(gòu)造器,Java虛擬機(jī)會(huì)保證在子類(lèi)的<clinit>()方法執(zhí)行前,父類(lèi)的<clinit>()方法已經(jīng)執(zhí)行完畢,java.lang.Object的<clinit>()肯定是第一個(gè)執(zhí)行的
  • <clinit>()方法對(duì)于類(lèi)和接口來(lái)說(shuō)不是必須的,如果一個(gè)類(lèi)中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量的賦值操作,那么編譯器可以不生產(chǎn)這個(gè)類(lèi)的<clinit>()方法
  • 接口中不能使用靜態(tài)語(yǔ)句塊,但仍然有變量初始化賦值的操作,所以接口也會(huì)生成<clinit>()方法。與類(lèi)不同的是,執(zhí)行接口的<clinit>()方法不用先執(zhí)行父接口的<clinit>()方法,除非父接口中定義的變量被使用,以及接口的實(shí)現(xiàn)類(lèi)初始化時(shí)也不會(huì)執(zhí)行接口的<clinit>()方法
  • Java虛擬機(jī)必須保證一個(gè)類(lèi)的<clinit>()方法在多線程環(huán)境中被正確地加鎖同步,如果多個(gè)線程去初始化這個(gè)類(lèi),那么只有其中一個(gè)線程去執(zhí)行該類(lèi)的<clinit>()方法,其他線程需要阻塞等待。如果一個(gè)類(lèi)的<clinit>()方法中有耗時(shí)很長(zhǎng)的操作,可能導(dǎo)致多個(gè)進(jìn)程堵塞

類(lèi)加載的過(guò)程中,除了在加載階段用戶(hù)應(yīng)用程序可以通過(guò)自定義類(lèi)加載器的方式局部參與外,其余動(dòng)作都完全由Java虛擬機(jī)主導(dǎo),直到初始化階段,虛擬機(jī)才真正開(kāi)始執(zhí)行程序代碼,主導(dǎo)權(quán)移交給應(yīng)用程序。

類(lèi)加載器

用于實(shí)現(xiàn)類(lèi)的加載動(dòng)作。對(duì)于任意一個(gè)類(lèi),都要有加載它的類(lèi)加載器和這個(gè)類(lèi)本身一起共同確立其在Java虛擬機(jī)中的唯一性。每個(gè)類(lèi)加載器都擁有一個(gè)獨(dú)立的類(lèi)名稱(chēng)空間

比較兩個(gè)類(lèi)是否相等,看這兩個(gè)類(lèi)是否由同一個(gè)類(lèi)加載器加載。即使這兩個(gè)類(lèi)來(lái)源于同Class文件,但被不同加載器加載,那這兩個(gè)類(lèi)必定不相等

雙親委派模型

兩種不同的類(lèi)加載器

  • 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader),使用C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分
  • 其他所有的類(lèi)加載器,都由Java語(yǔ)言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,繼承自抽象類(lèi)java.lang.ClassLoader

三層類(lèi)加載器

  • 啟動(dòng)類(lèi)加載器(Bootstrap Class Loader)
    負(fù)責(zé)加載存放在<JAVA_HOME>\lib目錄或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且Java虛擬機(jī)能夠識(shí)別的(按照文件名識(shí)別,如rt.jar、tools.jar,名字不符合的即使放在lib目錄下也不會(huì)被加載)類(lèi)庫(kù)加載到虛擬機(jī)內(nèi)存中

    無(wú)法被Java程序直接引用,如果需要把加載請(qǐng)求委派啟動(dòng)類(lèi)加載器去處理,直接使用null代替即可

  • 擴(kuò)展類(lèi)加載器(Extension Class Loader)
    負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類(lèi)庫(kù)

    允許用戶(hù)將具有通用性的類(lèi)庫(kù)放置在ext目錄擴(kuò)展Java SE功能

  • 應(yīng)用程序類(lèi)加載器(Application Class Loader
    它是ClassLoader類(lèi)中的getSystemClassLoader()方法的返回值,又叫“系統(tǒng)類(lèi)加載器”

    負(fù)責(zé)加載用戶(hù)類(lèi)路徑(ClassPath)上的所有類(lèi)庫(kù),程序的默認(rèn)類(lèi)加載器


    雙親委派模型

雙親委派模型要求除了頂層的啟動(dòng)類(lèi)加載器外,其余的類(lèi)加載器都應(yīng)有自己的父類(lèi)加載器。這里類(lèi)加載器之間的父子關(guān)系一般不是繼承關(guān)系,而是通常使用組合關(guān)系來(lái)復(fù)用父加載器的代碼

工作過(guò)程
如果一個(gè)類(lèi)加載器收到了類(lèi)加載請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委托給父類(lèi)加載器完成,每一個(gè)層次的類(lèi)加載器都如此。因此所有的加載請(qǐng)求最終都應(yīng)該傳送到最頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己去加載

好處:Java中的類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。比如類(lèi)java.lang.Object,它處于rt.jar中,無(wú)論哪一個(gè)類(lèi)加載器請(qǐng)求加載這個(gè)類(lèi),都會(huì)委派給處于模型最頂端的啟動(dòng)類(lèi)加載器加載,所以在各種類(lèi)加載器環(huán)境中能保證Object類(lèi)是同一個(gè)類(lèi)

破壞雙親委派模型

雙親委派模型并不是一個(gè)具有強(qiáng)制性約束的模型

只要有明確的目的和充分的理由,突破就有原則無(wú)疑是一種創(chuàng)新,比如OSGi中的類(lèi)加載器

JDK9之后的類(lèi)加載器委派模型
JDK9之后的類(lèi)加載器委派模型
  • 啟動(dòng)類(lèi)加載器負(fù)責(zé)加載的模塊


  • 平臺(tái)類(lèi)加載器負(fù)責(zé)加載的模塊


  • 應(yīng)用程序類(lèi)加載器負(fù)責(zé)加載的模塊


虛擬機(jī)字節(jié)碼執(zhí)行引擎

“虛擬機(jī)”:執(zhí)行引擎由軟件自行實(shí)現(xiàn),不受物理?xiàng)l件制約
“物理機(jī)”:執(zhí)行引擎直接建立在處理器、緩存、指令集和操作系統(tǒng)層面上

所有的虛擬機(jī)輸入、輸出都是一致的:輸入的是字節(jié)碼二進(jìn)制流,處理過(guò)程是字節(jié)碼解析執(zhí)行的等效過(guò)程,輸出的是執(zhí)行結(jié)果

運(yùn)行時(shí)棧結(jié)構(gòu)

Java虛擬機(jī)以方法作為最基本的執(zhí)行單位,“棧幀”則是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行背后的數(shù)據(jù)結(jié)構(gòu)

每一個(gè)棧幀都包括了局部變量被、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址和一些額外的附加信息

只有位于棧頂?shù)姆椒ú攀窃谶\(yùn)行的,稱(chēng)為“當(dāng)前棧幀”,與之關(guān)聯(lián)的方法稱(chēng)為“當(dāng)前方法”,執(zhí)行引擎所運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作


棧幀的概念結(jié)構(gòu)
局部變量表

是一組變量值的存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量

在Java程序被編譯成Class文件時(shí),就在方法的Code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需分配的局部變量表的最大容量

局部變量表的容量以變量槽為最小單位,一個(gè)變量槽可以存放一個(gè)32位以?xún)?nèi)的數(shù)據(jù)類(lèi)型(boolean、byte、char、short、int、float、reference、returnAddress);對(duì)于64位的數(shù)據(jù)類(lèi)型,Java虛擬機(jī)會(huì)以高位對(duì)其的方式為其分配兩個(gè)連續(xù)的變量槽空間(long、double)

Java虛擬機(jī)通過(guò)索引定位的方式使用局部變量表,索引值范圍從0到局部變量表最大的變量槽數(shù)量。訪問(wèn)32位數(shù)據(jù)類(lèi)型的變量,則索引N代表使用了第N個(gè)變量槽;訪問(wèn)64位數(shù)據(jù)類(lèi)型的變量,則會(huì)同時(shí)使用第N和第N+1兩個(gè)變量槽

當(dāng)一個(gè)方法被調(diào)用時(shí),Java虛擬機(jī)會(huì)使用局部變量表來(lái)完成參數(shù)值到參數(shù)列表的傳遞過(guò)程,即實(shí)參到形參的傳遞。如果執(zhí)行的是實(shí)例方法(沒(méi)有被static修飾的),那局部變量表的第0位索引的變量槽是用于傳遞方法所屬對(duì)象實(shí)例的引用,可以通過(guò)“this”來(lái)訪問(wèn)這個(gè)隱含的參數(shù)

為了節(jié)省棧幀耗用的內(nèi)存空間,變量槽是可以重用的

局部變量不像內(nèi)部變量那樣存在“準(zhǔn)備階段”,如果一個(gè)局部變量定義了但沒(méi)有賦初值,那它是完全不能使用的

操作數(shù)棧

也被稱(chēng)為操作棧,被編譯成Class文件時(shí),就在方法的Code屬性的max_stacks數(shù)據(jù)項(xiàng)中確定了最大深度

32位數(shù)據(jù)類(lèi)型占棧容量為1,64位數(shù)據(jù)類(lèi)型占棧容量為2

當(dāng)一個(gè)方法剛開(kāi)始執(zhí)行時(shí),操作數(shù)棧是空的。執(zhí)行過(guò)程中,由各種字節(jié)碼指令往操作數(shù)棧中壓入和提取內(nèi)容

兩個(gè)不同棧幀在概念模型中是完全相互獨(dú)立的,但是大多數(shù)虛擬機(jī)的實(shí)現(xiàn)里會(huì)進(jìn)行優(yōu)化處理,讓下面棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊,不僅節(jié)約了空間,進(jìn)行方法調(diào)用的時(shí)候可以直接共用一部分?jǐn)?shù)據(jù)

Java虛擬機(jī)的解釋執(zhí)行引擎被稱(chēng)為“基于棧的執(zhí)行引擎”,其中的“?!睘椴僮鲾?shù)棧

兩個(gè)棧幀之間的數(shù)據(jù)共享
動(dòng)態(tài)連接

每個(gè)棧幀都包含一個(gè)指向常量池中該棧幀所屬方法的引用,持有這個(gè)引用時(shí)為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接

Class文件的常量池中存有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池里指向方法的符號(hào)引用作為參數(shù)

靜態(tài)解析:符號(hào)引用在類(lèi)加載階段或第一次使用的時(shí)候就被轉(zhuǎn)化為直接引用
動(dòng)態(tài)連接:符號(hào)引用在每一次運(yùn)行期間都轉(zhuǎn)化為直接引用

方法返回地址

當(dāng)方法執(zhí)行時(shí),兩種方式退出方法

  • “正常調(diào)用完成”
    執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法被稱(chēng)為調(diào)用者或主調(diào)方法)
  • “異常調(diào)用完成”
    在方法執(zhí)行的過(guò)程中遇到了異常,并且這個(gè)異常沒(méi)有在方法體內(nèi)得到妥善處理。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給它的上層調(diào)用者提供任何返回值的

無(wú)論采用何種退出方式,方法返回時(shí)可能需要在棧幀中保存一些信息,用來(lái)幫助恢復(fù)它的上層主調(diào)方法的執(zhí)行狀態(tài)

附加信息

《Java虛擬機(jī)規(guī)范》允許虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒(méi)有描述的信息到棧幀之中,比如與調(diào)試、性能收集相關(guān)的信息,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn)

方法調(diào)用

方法調(diào)用階段的唯一任務(wù)就是確定被調(diào)用的方法版本(即調(diào)用哪個(gè)方法)

解析

符合“編譯期可知,運(yùn)行期不可變”這個(gè)要求的方法的調(diào)用被稱(chēng)為解析

主要有靜態(tài)方法和私有方法兩大類(lèi),這兩種方不可能通過(guò)繼承或別的方式重寫(xiě)其他版本

解析調(diào)用一定是個(gè)靜態(tài)的過(guò)程,在編譯期間就完全確定,在類(lèi)加載的解析階段就會(huì)把涉及的符號(hào)引用全部轉(zhuǎn)變?yōu)槊鞔_的直接引用

調(diào)用不同的方法,字節(jié)碼指令設(shè)計(jì)不同的指令

  • invokestatic:用于調(diào)用靜態(tài)方法
  • invokespecial:用于調(diào)用實(shí)例構(gòu)造器<init>()方法、私有方法、父類(lèi)中的方法
  • invokevirtual:用于調(diào)用所有的虛方法
  • invokeinterface:用于調(diào)用接口方法
  • invokedynamic:先在執(zhí)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法,然后再執(zhí)行該方法,前面4條指令,分派邏輯都固化再Java虛擬機(jī)內(nèi)部,而這個(gè)指令的分派邏輯由用戶(hù)設(shè)定的引導(dǎo)方法來(lái)決定

虛方法和非虛方法

  • “非虛方法”
    靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類(lèi)方法、被final修飾的方法(盡管它使用invokevirtual調(diào)用)
  • “虛方法”
    其他方法
分派
  • 靜態(tài)分派

    package chapter8;
    
    /**
     * Human man = new Man()
     * Human:變量的“靜態(tài)類(lèi)型”
     * Man:變量的“實(shí)際類(lèi)型”
     * 虛擬機(jī)在常在時(shí)是通過(guò)參數(shù)的靜態(tài)類(lèi)型而不是實(shí)例類(lèi)型作為判定依據(jù)
     */
    public class StaticDispatch {
      static abstract class Human{
    
        }
    
        static class Man extends Human{
    
        }
        static class Woman extends Human{
    
      }
    
      public void sayHello(Human gay){
          System.out.println("hello,guy!");
      }
    
      public void sayHello(Man guy){
          System.out.println("hello,gentleman!");
      }
    
      public void sayHello(Woman guy){
          System.out.println("hello,lady!");
      }
    
      public static void main(String[] args) {
          Human man = new Man();
          Human woman = new Woman();
          StaticDispatch sr = new StaticDispatch();
          sr.sayHello(man);   // 輸出hello,guy
          sr.sayHello(woman); // 輸出hello,guy
      }
    }
    
    

    所有依賴(lài)靜態(tài)類(lèi)型來(lái)決定方法執(zhí)行版本的分派動(dòng)作,都稱(chēng)為靜態(tài)分派。靜態(tài)分派的最典型應(yīng)用表現(xiàn)就是方法重載

     package chapter8;
    /**
     * 重載方法匹配優(yōu)先級(jí)
     */
    
    import java.io.Serializable;
    
    public class Overload {
      public static void sayHello(Object arg){
          System.out.println("hello Object");
      }
    
      public static void sayHello(int arg){
          System.out.println("hello int");
      }
    
      public static void sayHello(long arg){
          System.out.println("hello long");
      }
    
      public static void sayHello(Character arg){
          System.out.println("hello Character");
      }
    
      public static void sayHello(char arg){
          System.out.println("hello char");
      }
    
      public static void sayHello(char... arg){
          System.out.println("hello char ...");
      }
    
      public static void sayHello(Serializable arg){
          System.out.println("hello Serializable");
      }
    
      public static void main(String[] args) {
          sayHello('a');  // 輸出hello char
      }
    }
    
    

    如果注釋掉sayHello(char arg)方法,那么會(huì)輸出hello int,這時(shí)發(fā)生了一次自動(dòng)類(lèi)型轉(zhuǎn)化char('a')-->int(97)
    如果再注釋掉sayHello(int arg),則會(huì)輸出hello long,發(fā)生了兩次次自動(dòng)轉(zhuǎn)型,char('a')-->int(97)-->long(97L)

    自動(dòng)轉(zhuǎn)型按照char > int > long > float > double是轉(zhuǎn)型順序進(jìn)行匹配,但不會(huì)匹配到byte和short,因?yàn)閏har到byte和short的轉(zhuǎn)型是不安全的

    sayHello(long arg)注釋后,會(huì)輸出hello Character,發(fā)生了一次自動(dòng)裝箱,'a'被包裝為它的封裝類(lèi)型java.lang.Character

  • 動(dòng)態(tài)分派
    動(dòng)態(tài)分派與重寫(xiě)有著密切的關(guān)聯(lián)

    運(yùn)行期根據(jù)實(shí)際類(lèi)型確定方法執(zhí)行版本的分派過(guò)程稱(chēng)為動(dòng)態(tài)分派

    這種多態(tài)性的根源在于虛方法調(diào)用指令invokevirtual

    package chapter8;
    
    public class DynamicDispatch {
      static abstract class Human{
          protected abstract void sayHello();
      }
      static class Man extends Human{
          @Override
          protected void sayHello(){
              System.out.println("man say hello");
          }
      }
    
      static class Woman extends Human{
    
          @Override
          protected void sayHello() {
              System.out.println("woman say hello");
          }
      }
    
      public static void main(String[] args) {
          Human man = new Man();
          Human woman = new Woman();
          man.sayHello(); // print:man say hello
          woman.sayHello();   // print:woman say hello
          man = new Woman();
          man.sayHello(); // print:woman say hello
      }
    }
    
    

    invokevirtual指令的運(yùn)行時(shí)解析過(guò)程
    1、找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類(lèi)型,記作C
    2、如果在類(lèi)型C中找到與常量中描述符和簡(jiǎn)單名稱(chēng)都相符的方法,則進(jìn)行訪問(wèn)權(quán)校驗(yàn),如果通過(guò)則返回這個(gè)方法的直接引用,查找過(guò)程結(jié)束;不通過(guò)則返回java.lang.IllegalAccessError異常
    3、否則,按照繼承關(guān)系從下往上依次對(duì)C的各個(gè)父類(lèi)進(jìn)行第二步的搜索和驗(yàn)證過(guò)程
    4、如果始終沒(méi)有找到合適的方法,則拋出java.lang.AbstractMethodError異常

  • 單分派與多分派
    方法的接收者與方法的參數(shù)統(tǒng)稱(chēng)為方法的宗量,根據(jù)分派基于多少種宗量,可以將分派劃分成單分派和多分派。單分派是根據(jù)一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇,多分派則是根據(jù)多于一個(gè)宗量對(duì)目標(biāo)方法進(jìn)行選擇

package chapter8;

/**
 * 單分派、多分派演示
 */
public class Dispatch {
    static class QQ{}
    static class _360{}

    public static class Father{
        public void hardChoice(QQ arg){
            System.out.println("father choose qq");
        }
        public void hardChoice(_360 arg){
            System.out.println("father choose 360");
        }

    }

    public static class Son extends Father{
        public void hardChoice(QQ arg){
            System.out.println("son choose qq");
        }
        public void hardChoice(_360 arg){
            System.out.println("son choose 360");
        }

    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());  // print:father choose 360
        son.hardChoice(new QQ());   // son choose qq
    }
}

編譯階段的選擇過(guò)程,也就是靜態(tài)分派的過(guò)程。選擇目標(biāo)方法的依據(jù)有兩點(diǎn):一是靜態(tài)類(lèi)型是Father還是Son,二是方法參數(shù)是QQ還是360。這次選擇結(jié)果的最終產(chǎn)物是產(chǎn)生了兩條invokevirtual指令,兩條指令的參數(shù)分別為常量池中指向Father::hardChoice(360)及Father::hardChoice(QQ)方法的符號(hào)引用。因?yàn)槭歉鶕?jù)兩個(gè)宗量進(jìn)行,所以靜態(tài)分派屬于多分派類(lèi)型

運(yùn)行階段中虛擬機(jī)的選擇,也就是動(dòng)態(tài)分派的過(guò)程。在執(zhí)行“son.hardChoice(new QQ())這行代碼時(shí),由于編譯期已經(jīng)決定目標(biāo)方法的簽名必須為hardChoice(QQ),所以參數(shù)的QQ是什么都不會(huì)構(gòu)成任何影響,唯一可以影響虛擬機(jī)選擇的因素只有該方法的接收者的實(shí)際類(lèi)型是Father還是Son。因?yàn)橹挥幸粋€(gè)宗量作為選擇依據(jù),所以動(dòng)態(tài)分派屬于單分派類(lèi)型

如今的Java語(yǔ)言(Java12和預(yù)覽版Java13)是一門(mén)靜態(tài)多分派、動(dòng)態(tài)單分派的語(yǔ)言

虛擬機(jī)動(dòng)態(tài)分派的實(shí)現(xiàn)

虛方法表(Virtual Method Table,簡(jiǎn)稱(chēng)vtable)
存放著各個(gè)方法的實(shí)際入口地址,如果方法在子類(lèi)沒(méi)有重寫(xiě),則子類(lèi)虛方法表中的地址入口和父類(lèi)相同方法的地址入口一致,都指向父類(lèi)的實(shí)現(xiàn)入口。如果重寫(xiě)了方法,則子類(lèi)虛方法表中的地址也會(huì)替換為指向子類(lèi)實(shí)現(xiàn)版本的入口地址

方法表的結(jié)構(gòu)

查虛方法表是分派調(diào)用的一種優(yōu)化手段,除此之外,虛擬機(jī)為了進(jìn)一步提高性能,還會(huì)使用類(lèi)型繼承關(guān)系分析、守護(hù)內(nèi)聯(lián)、內(nèi)聯(lián)緩存等多種非穩(wěn)定的激進(jìn)優(yōu)化來(lái)爭(zhēng)取更大的性能空間

動(dòng)態(tài)語(yǔ)言支持

動(dòng)態(tài)類(lèi)型語(yǔ)言

動(dòng)態(tài)語(yǔ)言的關(guān)鍵特征是它的類(lèi)型檢查的主體過(guò)程是在運(yùn)行期而不是編譯期進(jìn)行的

運(yùn)行時(shí)異常和連接時(shí)異常

  • 運(yùn)行時(shí)異常就是指只要代碼不執(zhí)行到這一行就不會(huì)出現(xiàn)問(wèn)題
  • 連接時(shí)異常就是即使導(dǎo)致連接時(shí)異常的代碼放在一條根本無(wú)法被執(zhí)行到的路徑分支上,類(lèi)加載時(shí)也照樣會(huì)拋出異常

obj.println("hello world")
Java語(yǔ)言在編譯期間就已將println(String)方法完整的符號(hào)引用生成出來(lái),并作為方法調(diào)用指令的參數(shù)存儲(chǔ)到Class文件中,例如下面這樣
invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

這個(gè)符號(hào)引用包含了該方法定義在哪個(gè)具體的類(lèi)型之中、方法的名字以及參數(shù)順序、參數(shù)類(lèi)型和方法返回值等信息,通過(guò)這個(gè)符號(hào)引用,Java虛擬機(jī)就可以翻譯出該方法的直接引用

而EMCAScript等動(dòng)態(tài)語(yǔ)言與Java有一個(gè)核心差異就是obj本身沒(méi)有類(lèi)型,而obj的值才具有類(lèi)型,所以編譯器在編譯期最多只能確定方法名稱(chēng)、參數(shù)、返回值這些信息,而不會(huì)去確定方法所在的類(lèi)型(即接收者不固定),“變量無(wú)類(lèi)型而變量值才有類(lèi)型”也是動(dòng)態(tài)語(yǔ)言的一個(gè)核心特征

靜態(tài)類(lèi)語(yǔ)言能夠在編譯期確定變量類(lèi)型,最顯著的好處是編譯器可以提供全面嚴(yán)謹(jǐn)?shù)念?lèi)型檢查,這樣與數(shù)據(jù)類(lèi)型相關(guān)的潛在問(wèn)題就能在編碼時(shí)被及時(shí)發(fā)現(xiàn),利于穩(wěn)定性及讓項(xiàng)目容易達(dá)到更大的規(guī)模
動(dòng)態(tài)類(lèi)語(yǔ)言在運(yùn)行期才確定類(lèi)型,這是可以為開(kāi)發(fā)人員提供極大的靈活性,某些在靜態(tài)類(lèi)語(yǔ)言中要花大量臃腫代碼來(lái)實(shí)現(xiàn)的功能,由動(dòng)態(tài)類(lèi)語(yǔ)言去做可能會(huì)很清晰簡(jiǎn)介,意味著開(kāi)發(fā)效率的提升

后端編譯與優(yōu)化

后端編譯:編譯器無(wú)論在何時(shí)、何種狀態(tài)下把Class文件轉(zhuǎn)換成與本地基礎(chǔ)設(shè)施(硬件指令集、操作系統(tǒng))相關(guān)的二進(jìn)制機(jī)器碼,它都可以視為整個(gè)編譯的后端

即時(shí)編譯器

“熱點(diǎn)代碼”:某個(gè)方法或代碼塊的運(yùn)行特別頻繁

即時(shí)編譯器:為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時(shí)虛擬機(jī)將會(huì)把這些代碼編譯成本地機(jī)器碼,并以各種手段盡可能地優(yōu)化,運(yùn)行時(shí)完成這個(gè)任務(wù)的后端編譯器

解釋器與編譯器

目前主流的商用Java虛擬機(jī),內(nèi)部都同時(shí)包含解釋器與編譯器。當(dāng)程序需要迅速啟動(dòng)和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用,省去編譯的時(shí)間,立即運(yùn)行。當(dāng)程序啟動(dòng)后,編譯器把越來(lái)越多的代碼編譯成本地代碼,減少解釋器的中間損耗,獲得更高的執(zhí)行效率

當(dāng)程序運(yùn)行環(huán)境中內(nèi)存資源限制較大,可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來(lái)提示效率

解釋器可以作為編譯器激進(jìn)優(yōu)化時(shí)后備的“逃生門(mén)”(情況允許,HotSpot虛擬機(jī)也會(huì)采用不進(jìn)行激進(jìn)優(yōu)化的客戶(hù)端編譯器充當(dāng)“逃生門(mén)”的角色)

讓編譯器選擇一些不能保證所有情況都正確,但大多數(shù)時(shí)候都能提升運(yùn)行速度的優(yōu)化手段,當(dāng)激進(jìn)優(yōu)化的假設(shè)不成立,如加載了新類(lèi)后類(lèi)型基礎(chǔ)結(jié)構(gòu)出現(xiàn)變化、出現(xiàn)“罕見(jiàn)陷阱”時(shí)可以通過(guò)逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行

解釋器與編譯器的交互

HotSpot虛擬機(jī)內(nèi)置了兩個(gè)(或三個(gè))即時(shí)編譯器

  • “客戶(hù)端編譯器”(Client Compiler):簡(jiǎn)稱(chēng)C1編譯器
  • “服務(wù)端編譯器”(Server Compiler):簡(jiǎn)稱(chēng)C2編譯器,也叫Opto編譯器
  • Graal編譯器

HotSpot虛擬機(jī)會(huì)根據(jù)自身版本與宿主機(jī)器的硬件性能自動(dòng)選擇運(yùn)行模式,用戶(hù)也可以使用“-client”或“-server”參數(shù)強(qiáng)制指定虛擬機(jī)運(yùn)行在客戶(hù)端還是服務(wù)端

三種模式

  • “混合模式”:編譯器與解釋器搭配使用
  • “解釋模式”:使用參數(shù)“-Xint”強(qiáng)制虛擬機(jī)運(yùn)行于“解釋模式”,編譯器完全不介入工作
  • “編譯模式”:使用參數(shù)“-Xcomp”強(qiáng)制虛擬機(jī)運(yùn)行于“編譯模式”,優(yōu)先采用編譯方式執(zhí)行程序,但解釋器仍要在編譯無(wú)法進(jìn)行的情況下介入執(zhí)行過(guò)程

分層編譯
編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,編譯出優(yōu)化程度越高的代碼越耗時(shí),解釋器可能還要替代編譯器收集性能監(jiān)控信息,影響解釋執(zhí)行階段速度

為了在程序啟動(dòng)響應(yīng)速度于運(yùn)行效率之間達(dá)到最佳平衡,加入了分層編譯

解釋器、客戶(hù)端編譯器和服務(wù)端編譯器同時(shí)工作,用客戶(hù)端編譯器獲取更高的編譯速度,用服務(wù)端編譯器獲取更好的編譯質(zhì)量;解釋執(zhí)行的時(shí)候也無(wú)須額外承擔(dān)收集性能監(jiān)控信息的任務(wù);服務(wù)端編譯器采用高復(fù)雜度的優(yōu)化算法時(shí),客戶(hù)端編譯器可先采用簡(jiǎn)單優(yōu)化來(lái)為它爭(zhēng)取更多的編譯時(shí)間

  • 0層:程序純解釋執(zhí)行,并且解釋器不開(kāi)啟性能監(jiān)控功能
  • 1層:使用客戶(hù)端編譯器將字節(jié)碼編譯為本地代碼運(yùn)行,進(jìn)行簡(jiǎn)單可靠的優(yōu)化,不開(kāi)啟性能監(jiān)控功能
  • 2層:仍使用客戶(hù)端編譯器執(zhí)行,僅開(kāi)啟方法及回邊次數(shù)統(tǒng)計(jì)等有限的性能監(jiān)控功能
  • 3層:仍使用客戶(hù)端編譯器執(zhí)行,開(kāi)啟全部性能監(jiān)控,除了2層的統(tǒng)計(jì)信息,還會(huì)收集分支跳轉(zhuǎn)、虛方法調(diào)用版本等全部的統(tǒng)計(jì)信息
  • 4層:使用客戶(hù)端編譯器將字節(jié)碼編譯為本地代碼,相比客戶(hù)端編譯器,服務(wù)端編譯器會(huì)啟用更多耗時(shí)更長(zhǎng)的優(yōu)化,還會(huì)根據(jù)性能監(jiān)控信息進(jìn)行一些不可靠的性能優(yōu)化
分層編譯的交互關(guān)系
編譯對(duì)象與觸發(fā)條件

熱點(diǎn)代碼主要有兩類(lèi)

  • 被多次調(diào)用的方法
  • 被多次執(zhí)行的循環(huán)體

這兩種情況編譯的目標(biāo)對(duì)象都是整個(gè)方法體,而不會(huì)是單獨(dú)的循環(huán)體。第一種情況會(huì)以整個(gè)方法作為編譯對(duì)象(標(biāo)準(zhǔn)的即時(shí)編譯方式);后一種情況,也是以整個(gè)方法作為編譯對(duì)象,但執(zhí)行入口(從方法第幾條字節(jié)碼指令開(kāi)始執(zhí)行)稍有不同,編譯時(shí)會(huì)出入執(zhí)行入口點(diǎn)字節(jié)碼序號(hào)(棧上替換)。

“棧上替換”(On Stack Replacement,OSR):編譯發(fā)生在方法執(zhí)行的過(guò)程中,即方法的棧幀還在棧上,方法就被替換了。

熱點(diǎn)探測(cè)

  • 基于采樣點(diǎn)的熱點(diǎn)探測(cè)
    采用這種方法的虛擬機(jī)會(huì)周期性地檢查各個(gè)線程的調(diào)用棧頂,如果發(fā)現(xiàn)某個(gè)(或某些)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是“熱點(diǎn)方法”。
    優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單高效,很容易地獲取方法調(diào)用關(guān)系(將調(diào)用對(duì)展開(kāi)即可)
    缺點(diǎn):很難精確地確認(rèn)一個(gè)方法地?zé)岫?,容易因?yàn)榫€程阻塞或別的外界因素的影響而擾亂熱點(diǎn)探測(cè)
  • 基于計(jì)數(shù)器的熱點(diǎn)探測(cè)
    虛擬機(jī)會(huì)為每個(gè)方法(甚至是代碼塊)建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過(guò)一定的閾值就認(rèn)為它是“熱點(diǎn)方法”
    優(yōu)點(diǎn):統(tǒng)計(jì)結(jié)果相對(duì)來(lái)說(shuō)更加精確嚴(yán)謹(jǐn)
    缺點(diǎn):實(shí)現(xiàn)起來(lái)更麻煩一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系

HotSpot虛擬機(jī)使用的是第二種熱點(diǎn)探測(cè)方法

兩類(lèi)計(jì)數(shù)器

  • 方法調(diào)用計(jì)數(shù)器
    統(tǒng)計(jì)方法被調(diào)用的次數(shù),默認(rèn)閾值客戶(hù)端模式下為1500次,服務(wù)端模式下為10000次(-XX:CompilerThreshold人為設(shè)定)

    方法調(diào)用計(jì)數(shù)器觸發(fā)即時(shí)編譯

    默認(rèn)設(shè)置下。方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的并不是方法被調(diào)用的絕對(duì)次數(shù),而是相對(duì)的執(zhí)行頻率,即一段時(shí)間之內(nèi)方法被調(diào)用的次數(shù)。當(dāng)超過(guò)一定時(shí)間限度,方法調(diào)用次數(shù)未達(dá)到閾值,則該方法的調(diào)用計(jì)數(shù)器減少一半,這個(gè)過(guò)程為方法調(diào)用計(jì)數(shù)器熱度的衰減(在收集收集時(shí)順便進(jìn)行,可用-XX:-UseCounterDecay關(guān)閉),這段時(shí)間為此方法統(tǒng)計(jì)的半衰周期(-XX:CounterHalfLifeTime設(shè)置,單位為秒)

  • 回邊計(jì)數(shù)器
    統(tǒng)計(jì)方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令就稱(chēng)為“回邊”,建立回邊計(jì)數(shù)器統(tǒng)計(jì)的目的時(shí)為了觸發(fā)棧上的替換編譯

    回邊計(jì)數(shù)器閾值計(jì)算公式

    • 客戶(hù)端模式下:方法調(diào)用計(jì)數(shù)器閾值(-XX:CompilerThreshold) * OSR比率(-XX:OnStackReplacePercentage,默認(rèn)值為933) / 100 ,都取默認(rèn)值,那客戶(hù)端模式下回邊計(jì)數(shù)器閾值為13995
    • 服務(wù)端模式下:方法調(diào)用計(jì)數(shù)器閾值(-XX:CompilerThreshold) * (OSR比率(-XX:OnStackReplacePercentage,默認(rèn)值為140) - 解釋器監(jiān)控比率(-XX:InterpreterProfilePercentage,默認(rèn)值為33)) / 100,都取默認(rèn)值,那服務(wù)端模式下回邊計(jì)數(shù)器閾值為10700
    回邊計(jì)數(shù)器觸發(fā)即時(shí)編譯

    回邊計(jì)數(shù)器沒(méi)有計(jì)數(shù)熱度衰減,這個(gè)計(jì)數(shù)器統(tǒng)計(jì)的就是該方法循環(huán)執(zhí)行的絕對(duì)次數(shù),計(jì)數(shù)器溢出時(shí),把方法計(jì)數(shù)器的值也調(diào)整到溢出狀態(tài),這樣下次再進(jìn)入該方法的時(shí)候就會(huì)執(zhí)行標(biāo)準(zhǔn)編譯過(guò)程

編譯過(guò)程

默認(rèn)條件下,虛擬機(jī)在未完成編譯前,都仍按照解釋方式執(zhí)行代碼,編譯動(dòng)作則在后臺(tái)進(jìn)行

禁止后臺(tái)編譯(-XX:-BackgroundCompilation),執(zhí)行線程將會(huì)一直阻塞等待,直到編譯過(guò)程完成再執(zhí)行編譯器輸出的本地代碼

  • 客戶(hù)端編譯器
    主要關(guān)注點(diǎn)在于局部性?xún)?yōu)化,放棄了許多耗時(shí)較長(zhǎng)的全局優(yōu)化收到

    高級(jí)中間代碼(High-Level Intermediate Representation,HIR):與目標(biāo)機(jī)器指令集無(wú)關(guān)的中間表示
    靜態(tài)單分配(Static Single Assignment,SSA)形式:代表碼值,一些在HIR的構(gòu)造過(guò)程之中和之后進(jìn)行的優(yōu)化動(dòng)作更容易實(shí)現(xiàn)
    低級(jí)中間代碼(Low-Level Intermediate Representation,LIR):與目標(biāo)機(jī)器指令集相關(guān)的中間表示

    客戶(hù)端編譯器架構(gòu)
  • 服務(wù)端編譯器
    專(zhuān)門(mén)面向服務(wù)端的典型應(yīng)用場(chǎng)景,并為服務(wù)端的性能配置針對(duì)性調(diào)整的編譯器,一個(gè)能容忍很高優(yōu)化復(fù)雜度的高級(jí)編譯器

    執(zhí)行大部分經(jīng)典的優(yōu)化動(dòng)作:無(wú)用代碼消除、循環(huán)展開(kāi)、循環(huán)表達(dá)式外提、消除公共子表達(dá)式、常量傳播、基本塊重排序等

    實(shí)施與Java語(yǔ)言特性密切相關(guān)的優(yōu)化技術(shù):范圍排除消除、空值檢查消除、等

    可能根據(jù)解釋器或客戶(hù)端編譯器提供的性能監(jiān)控信息進(jìn)行一些不穩(wěn)定的預(yù)測(cè)性激進(jìn)優(yōu)化:守護(hù)內(nèi)聯(lián)、分支頻率預(yù)測(cè)等

編譯器優(yōu)化技術(shù)

方法內(nèi)聯(lián)

最重要的優(yōu)化手段,被稱(chēng)為優(yōu)化之母。它除了消除方法調(diào)用的成本之外,更重要的意義是為其他優(yōu)化手段建立良好的基礎(chǔ)。沒(méi)有內(nèi)聯(lián),多數(shù)其他優(yōu)化都無(wú)法有效進(jìn)行

把目標(biāo)方法的代碼原封不動(dòng)地“復(fù)制”到發(fā)起調(diào)用的方法之中,避免發(fā)生真實(shí)的調(diào)用

對(duì)于虛方法,編譯器靜態(tài)地去做內(nèi)聯(lián)的時(shí)候很難確定應(yīng)該使用哪個(gè)方法版本。為了解決這個(gè)問(wèn)題,引入了類(lèi)型繼承關(guān)系分析(Class Hierarchy Analysis,CHA),這是整個(gè)應(yīng)用程序范圍內(nèi)的類(lèi)型分析技術(shù),用于確定在目前已加載的類(lèi)中,某個(gè)接口是否有多于一種的實(shí)現(xiàn)、某個(gè)類(lèi)是否存在子類(lèi)、某個(gè)子類(lèi)是否覆蓋率父類(lèi)的某個(gè)虛方法等信息

通過(guò)內(nèi)聯(lián)緩存調(diào)用比用不內(nèi)聯(lián)的非虛方法調(diào)用,僅多了一次類(lèi)型判斷的開(kāi)銷(xiāo)而已

逃逸分析

最前沿的優(yōu)化技術(shù)之一,并不是直接優(yōu)化代碼的手段,而是為其他優(yōu)化措施提供依據(jù)的分析技術(shù)

逃逸分析的基本原理為分析對(duì)象動(dòng)態(tài)作用域,對(duì)象由低到高的不同逃逸程度

  • 從不逃逸
  • 方法逃逸:一個(gè)對(duì)象在方法里面被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他方法中
  • 線程逃逸:甚至可能被外部線程訪問(wèn)到,譬如賦值給可以在其他線程中訪問(wèn)到的實(shí)例變量

通過(guò)不同逃逸程度,給對(duì)象實(shí)例采取不同的優(yōu)化

  • 棧上分配
    如果確定一個(gè)對(duì)象不會(huì)逃出線程之外,那讓這個(gè)對(duì)象在棧上分配內(nèi)存,對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷(xiāo)毀,垃圾收集子系統(tǒng)的壓力將會(huì)下降很多。棧上分配可以支持方法逃逸,不支持線程逃逸

  • 標(biāo)量替換
    一個(gè)數(shù)據(jù)已經(jīng)無(wú)法再分解成更小的數(shù)據(jù)來(lái)表示,Java虛擬機(jī)中的原始數(shù)據(jù)類(lèi)型(int、long等數(shù)值類(lèi)型及reference類(lèi)型等)都不能再進(jìn)一步分解了,這些數(shù)據(jù)就可以稱(chēng)為標(biāo)量

    如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,則稱(chēng)為聚合量(Java中的對(duì)象)

    標(biāo)量替換:把一個(gè)Java對(duì)象拆散,根據(jù)程序訪問(wèn)情況,將其用到的成員變量恢復(fù)為原始類(lèi)型來(lái)訪問(wèn)

    一個(gè)對(duì)象不會(huì)被方法外部訪問(wèn),并且這個(gè)對(duì)象可以被拆散,那么程序真正執(zhí)行的時(shí)候?qū)⒖赡懿辉賱?chuàng)建這個(gè)對(duì)象,而是創(chuàng)建它的若干個(gè)被這個(gè)方法使用的成員變量來(lái)代替。

    將對(duì)象拆分后,除了可以讓對(duì)象的成員在棧上分配和讀寫(xiě)之外,還可以為后續(xù)進(jìn)一步優(yōu)化手段創(chuàng)建條件。它不允許對(duì)象逃逸出方法范圍內(nèi)

  • 同步消除
    如果能夠確定一個(gè)變量不會(huì)逃逸出線程,無(wú)法被其他線程訪問(wèn),那么這個(gè)變量的讀寫(xiě)肯定就不會(huì)有競(jìng)爭(zhēng),對(duì)這個(gè)變量實(shí)施的同步措施也就可以安全地消除掉

-XX:+DoEscapeAnalysis:開(kāi)啟逃逸分析
-XX:+PrintEscapeAnalysis:查看逃逸分析結(jié)果
-XX:+EliminateAllocations:開(kāi)啟標(biāo)量替換
+XX:+EliminateLocks:開(kāi)啟同步消除
-XX:+PrintEliminateAllocations:查看標(biāo)量替換情況

公共子表達(dá)式消除

非常經(jīng)典的、普遍用于各種編譯器的優(yōu)化技術(shù)

如果一個(gè)E之前已經(jīng)被計(jì)算過(guò)了,并且先前計(jì)算到現(xiàn)在的E中所有變量的值都沒(méi)有發(fā)生變化,那么E的這次出現(xiàn)就稱(chēng)為公共子表達(dá)式

  • 局部公共子表達(dá)式消除:優(yōu)化僅限于程序基本快內(nèi)
  • 全局公共子表達(dá)式消除:優(yōu)化的范圍涵蓋了多個(gè)基本塊

int d = (c * b) * 12 + a + (a + b * c)
進(jìn)入虛擬機(jī)即時(shí)編譯后,檢測(cè)到b * c與c * b是一樣的表達(dá)式,且b、c在計(jì)算期間值不變
int d = E * 12 + a + (a + E)
編譯器還能來(lái)進(jìn)行另外一種優(yōu)化——化數(shù)為簡(jiǎn)ba
int d = E * 13 + a + a

數(shù)組邊界檢查消除

是即時(shí)編譯器的一項(xiàng)語(yǔ)言相關(guān)的經(jīng)典優(yōu)化技術(shù)

如果有一個(gè)數(shù)組foo[i],則i必須滿足“i >= 0 && i < foo.length”的訪問(wèn)條件,對(duì)于虛擬機(jī)的執(zhí)行子系統(tǒng)來(lái)說(shuō),每次數(shù)組元素的讀寫(xiě)都帶有一次隱含的條件判定操作,對(duì)擁有大量數(shù)組訪問(wèn)的程序代碼,這必定是一種性能負(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)在區(qū)間 [0 , foo.length] 之間,那么在循環(huán)中就可以把整個(gè)數(shù)組的上下界檢查消除掉

數(shù)組邊界檢查優(yōu)化盡可能把運(yùn)行期檢查提前到編譯期完成

Java內(nèi)存模型與并發(fā)

Java內(nèi)存模型

主要目的:定義程序中各種變量的訪問(wèn)規(guī)則,即關(guān)注在虛擬機(jī)中把變量值存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細(xì)節(jié)

此處的變量包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量與方法參數(shù)

主內(nèi)存與工作內(nèi)存

Java內(nèi)存模型規(guī)定了所有變量都存儲(chǔ)在主內(nèi)存中。

每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存保存了該線程使用的變量的主內(nèi)存副本,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的數(shù)據(jù)

內(nèi)存間交互操作

主內(nèi)存拷貝到工作內(nèi)存、工作內(nèi)存同步回主內(nèi)存

  • lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程的獨(dú)占狀態(tài)
  • unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定
  • read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中
  • load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中
  • use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作
  • assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作
  • store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中
  • write(寫(xiě)入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中

在執(zhí)行上述8種基本操作時(shí)必須滿足如下規(guī)則

  • 不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者工作內(nèi)存發(fā)起回寫(xiě)了但主內(nèi)存不接受的情況出現(xiàn)
  • 不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回內(nèi)存
  • 不允許一個(gè)線程無(wú)原因地(沒(méi)有發(fā)生過(guò)任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中
  • 一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,換句話說(shuō)就是對(duì)一個(gè)變量實(shí)施use、store操作之前,必須執(zhí)行assign和load操作
  • 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖
  • 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,那將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行assign和load操作
  • 如果一個(gè)變量事先沒(méi)有被lock操作鎖定,那就不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定的變量
  • 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)
對(duì)于volatile型變量的特殊規(guī)則

volatile是Java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制

當(dāng)一個(gè)變量被定義成volatile后,具備兩項(xiàng)特性

  • 保證此變量對(duì)所有線程的可見(jiàn)性
    即一條線程修改了這個(gè)變量的值,那么新值對(duì)于其他線程來(lái)說(shuō)是立即可知的,不代表它是線程安全的

    不符合以下兩條規(guī)則,仍然要通過(guò)加鎖(synchronized、java.util.concurrent中的鎖或原子類(lèi))來(lái)保證原子性

    • 運(yùn)算結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值
    • 變量不需要與其他的狀態(tài)的變量共同參與不變約束
  • 禁止指令重排序化
    指令重排序是指處理器采用了允許將多條指令不按程序規(guī)定的順序分開(kāi)發(fā)送給各個(gè)相應(yīng)的電路單元進(jìn)行處理

Java內(nèi)存中對(duì)volatile變量定義的特殊規(guī)則
T:線程、V,W:volatile變量

  • 在工作內(nèi)存中,每次使用V前都必須從主內(nèi)存刷新最新的值,用于保證能看見(jiàn)其他線程對(duì)變量V所做的修改
  • 每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對(duì)變量V所做的修改
  • volatile修飾的變量不會(huì)被指令重排序優(yōu)化,從而保證代碼的執(zhí)行順序與程序的順序相同
針對(duì)long和double型變量的特殊規(guī)則

允許虛擬機(jī)將沒(méi)有被volatile修飾的64位數(shù)據(jù)的讀寫(xiě)操作劃分為兩次32位的操作來(lái)進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)自行選擇是否要保證64位數(shù)據(jù)類(lèi)型的load、store、read和write這四個(gè)操作的原子性,“l(fā)ong和double的非原子協(xié)定”

原子性、可見(jiàn)性和有序性
  • 原子性
    由Java內(nèi)存模型來(lái)直接保證的原子性變量操作包括read、load、assign、use、store和write這六個(gè),基本數(shù)據(jù)類(lèi)型的訪問(wèn),讀寫(xiě)都是具備原子性的(除了long和double)

    synchronized之間的操作也具備原子性

  • 可見(jiàn)性
    可見(jiàn)性是指一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知這個(gè)修改

volalite、synchronized、final能實(shí)現(xiàn)可見(jiàn)性

  • 有序性
    如果在本線程內(nèi)觀察,所有的操作都是有序的;如果一個(gè)線程中觀察另一個(gè)線程,所有的操作都是無(wú)序的

volatite、synchronized保證線程之間操作的有序性

Java與線程

目前線程時(shí)Java里面進(jìn)行處理器資源調(diào)度的最基本單位

線程的實(shí)現(xiàn)
  • 使用內(nèi)核線程實(shí)現(xiàn)(1:1實(shí)現(xiàn))
    直接由操作系統(tǒng)內(nèi)核支持的線程

    這種線程由內(nèi)核來(lái)完成線程切換,內(nèi)核通過(guò)操縱調(diào)度器對(duì)線程進(jìn)行調(diào)度,并負(fù)責(zé)將線程的任務(wù)映射到各個(gè)處理器上

    程序一般不會(huì)直接使用內(nèi)核線程,而是使用內(nèi)核線程(KLT)的一種高級(jí)接口——輕量級(jí)進(jìn)程(LWP),輕量級(jí)進(jìn)程與內(nèi)核線程之間1:1的關(guān)系稱(chēng)為一對(duì)一的線程模型

  • 使用用戶(hù)線程實(shí)現(xiàn)(1:N實(shí)現(xiàn))
    一個(gè)線程只要不是內(nèi)核線程,都可以認(rèn)為是用戶(hù)線程(UT)的一種

  • 使用用戶(hù)線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)(N:M實(shí)現(xiàn))
    內(nèi)核線程與用戶(hù)線程一起使用


Java線程調(diào)度

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過(guò)程

  • 協(xié)同式線程調(diào)度
    線程的執(zhí)行時(shí)間由線程本身來(lái)控制,線程執(zhí)行完畢,通知系統(tǒng)切換線程

    優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,一般沒(méi)有線程同步問(wèn)題
    缺點(diǎn):線程執(zhí)行時(shí)間不可控制

  • 搶占式線程調(diào)度
    每個(gè)線程由系統(tǒng)來(lái)分配執(zhí)行時(shí)間,線程的切換不由線程本身來(lái)決定

    可以通過(guò)設(shè)置線程優(yōu)先級(jí)來(lái)“建議”系統(tǒng)給某些線程多分配些時(shí)間。在兩個(gè)線程同時(shí)處于Ready狀態(tài)時(shí),優(yōu)先級(jí)越高的線程越容易被系統(tǒng)選擇執(zhí)行,但線程調(diào)度最終還是由操作系統(tǒng)說(shuō)了算

    在Windows下設(shè)置線程優(yōu)先級(jí)為1和2、3和4、6和7、8和9的效果是完全相同的

狀態(tài)轉(zhuǎn)換

六種線程狀態(tài):

  • 新建(New):創(chuàng)建后尚未啟動(dòng)的線程
  • 運(yùn)行(Runnable):包括操作系統(tǒng)線程狀態(tài)中的Running(正在執(zhí)行)和Ready(等待操作系統(tǒng)為它分配執(zhí)行時(shí)間)
  • `無(wú)限期等待(Waiting):不會(huì)被分配處理器執(zhí)行時(shí)間,等待被其他線程顯示喚醒。以下方法會(huì)陷入Waiting狀態(tài)
    • 沒(méi)有設(shè)置Timeout參數(shù)的Object::wait()方法
    • 沒(méi)有設(shè)置Timeout參數(shù)的Thread::join()方法
    • lockSupport::park()方法
  • 限期等待(Timed Waiting):不會(huì)被分配處理器執(zhí)行時(shí)間,不過(guò)無(wú)需等待被其他線程顯示喚醒,在一定時(shí)間之后它們由系統(tǒng)自動(dòng)喚醒,以下方法會(huì)陷入限期等待狀態(tài):
    • Threed::sleep()方法
    • 設(shè)置了Timeout參數(shù)的Object::wait()方法
    • 設(shè)置了Timeout參數(shù)的Thread::join()方法
    • LockSupport::parkNanos()方法
    • LockSupport::partUntil()方法
  • 阻塞(Blocked):線程被阻塞了,阻塞狀態(tài)在等待著獲取到一個(gè)排它鎖,在程序等待進(jìn)入同步區(qū)域的時(shí)候,線程將進(jìn)入這種狀態(tài)
  • 結(jié)束(Terminated):已終止線程的線程狀態(tài),線程已結(jié)束執(zhí)行

Java與協(xié)程

最初多數(shù)的用戶(hù)線程是被設(shè)計(jì)成協(xié)同式調(diào)度,所以也叫“協(xié)程”,它的主要優(yōu)勢(shì)是輕量

線程安全與鎖優(yōu)化

當(dāng)多個(gè)線程同時(shí)訪問(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é)果,那就稱(chēng)這個(gè)對(duì)象是線程安全的

線程安全

安全程度
  • 不可變
    不可變的對(duì)象一定是線程安全的

    如果多線程共享的數(shù)據(jù)是一個(gè)基本數(shù)據(jù)類(lèi)型,那么只要在定義時(shí)使用final關(guān)鍵字修飾它就可以保證它是不變的;如果是一個(gè)對(duì)象,那就需要對(duì)象自行保證其行為不會(huì)對(duì)其狀態(tài)產(chǎn)生任何影響(String類(lèi)、Long、Double等)

  • 絕對(duì)線程安全
    不管運(yùn)行時(shí)環(huán)境如何,調(diào)用者都不需要任何額外的同步措施

  • 相對(duì)線程安全
    這個(gè)對(duì)象單次的操作是線程安全的

  • 線程兼容
    線程兼容指對(duì)象本身并不是線程安全的,但是可以通過(guò)在調(diào)用端正確地使用同步手段來(lái)保證對(duì)象在并發(fā)環(huán)境中可以安全地使用

  • 線程對(duì)立
    不管調(diào)用端是否采取了同步措施,都無(wú)法在多線程環(huán)境中并發(fā)使用代碼

線程安全實(shí)現(xiàn)方法
互斥同步

最常見(jiàn)也是最主要的并發(fā)正確性保障手段

同步是指多個(gè)線程并發(fā)訪問(wèn)共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一個(gè)時(shí)刻只被一條(或者是一些,當(dāng)使用信號(hào)量的時(shí)候的時(shí)候)線程使用。

互斥是實(shí)現(xiàn)同步的手段,臨界區(qū)、互斥量和信號(hào)量是常見(jiàn)的互斥實(shí)現(xiàn)方式

互斥手段:

  • synchronized關(guān)鍵字

    • 被synchronized修飾的同步塊對(duì)同一條線程來(lái)說(shuō)是可重入的。這意味著同一線程反復(fù)進(jìn)入同步塊也不會(huì)出現(xiàn)自己把自己鎖死的情況
    • 被synchronized修飾的同步塊在持有鎖的線程執(zhí)行完畢并釋放鎖之前,會(huì)無(wú)條件地阻塞后面其他線程的進(jìn)入。無(wú)法強(qiáng)制已獲取鎖的線程釋放鎖;也無(wú)法強(qiáng)制正在等待鎖的線程中斷等待或超時(shí)退出
  • 重入鎖ReentrantLock
    是Lock接口最常見(jiàn)的一種實(shí)現(xiàn),它與synchronized一樣可重入

與synchronized的區(qū)別

  • 等待可中斷
    當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,改為處理其他事情
  • 公平鎖
    多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來(lái)一次獲得鎖;而非公平鎖則在鎖被釋放時(shí),任何一個(gè)等待鎖的線程都有機(jī)會(huì)獲得鎖。默認(rèn)時(shí)非公平的,但可以通過(guò)帶布爾值的構(gòu)造函數(shù)要求使用公平鎖
  • 鎖綁定多個(gè)條件
    一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定多個(gè)Condition對(duì)象,多次調(diào)用new Condition()方法即可
非阻塞同步

互斥同步面臨的主要問(wèn)題是進(jìn)行線程阻塞和喚醒所帶來(lái)的性能開(kāi)銷(xiāo),因此也叫阻塞同步

非阻塞同步:基于沖突檢測(cè)的樂(lè)觀并發(fā)策略,不管風(fēng)險(xiǎn)先操作,有風(fēng)險(xiǎn)再進(jìn)行其他補(bǔ)償措施

無(wú)同步方案

如果一個(gè)方法不涉及共享數(shù)據(jù),那就不需要任何同步措施

鎖優(yōu)化

自旋鎖與自適應(yīng)鎖

自旋鎖:如果兩個(gè)或以上的線程同時(shí)并行執(zhí)行,可以讓后面請(qǐng)求鎖那個(gè)線程“稍等一會(huì)”,但不放棄處理器的執(zhí)行時(shí)間。為了讓線程等待,我們只須讓線程執(zhí)行一個(gè)忙循環(huán)(自旋)

自選雖然避免了線程切換的開(kāi)銷(xiāo),但是占用了處理器執(zhí)行時(shí)間,如果等待時(shí)間短,那自旋等待的效果就會(huì)非常好。自旋的等待時(shí)間必須要有一定的限度,超過(guò)了限定次數(shù)沒(méi)有成功獲得鎖,就要掛起線程。自旋次數(shù)默認(rèn)值是10,可以用-XX:PreBlockSpin來(lái)自行修改

自適應(yīng)自旋:由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定

鎖消除

虛擬機(jī)即使編譯器在運(yùn)行時(shí)檢測(cè)到某段需要同步的代碼根本不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)而實(shí)施的一種對(duì)鎖進(jìn)行消除的優(yōu)化策略

鎖粗化

如果虛擬機(jī)探測(cè)到有這樣一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部

輕量級(jí)鎖
偏向鎖

這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果接下來(lái)的執(zhí)行過(guò)程中,該鎖一直沒(méi)有被其他的線程獲取,則持有偏向鎖是線程將永遠(yuǎn)不需要再進(jìn)行同步

筆記來(lái)源于《深入理解Java虛擬機(jī)》周志明 著

?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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