《深入理解Java虛擬機》讀書筆記

此文為我在學習《深入理解Java虛擬機:JVM高級特性與最佳實踐》時所做的筆記,把我認為是重點、面試時可能會被問到的知識點給記錄了下來,自認為是《深入理解Java虛擬機》這本書的精華。關于這些知識點,有的在我理解以后并沒有進行展開敘述。可當做是一個JVM知識點提綱來看,有不懂的地方大家可針對知識點展開學習。

Java內存區域與內存溢出異常
  • Java內存區域分為Java堆,虛擬機棧,方法區,本地方法棧,程序計數器;其中,程序計數器、虛擬機棧和本地方法棧屬于線程私有,Java堆和方法區線程共享;
  • 方法區用于存儲虛擬機加載的類信息、常量、靜態變量、即使編譯器編譯后的代碼等數據“永久代”),這一區域內存回收的目標主要是針對常量池的回收和對類型的卸載;
  • 運行時常量池是方法區的一部分。Class文件中除了有類的版本信息、字段、方法、接口等描述信息外,還有一項是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池進行存放;運行時常量時相對于Class文件常量池具備動態性,運行期間也可能將新的常量放入池中(String.intern()方法,檢測常量池中是否有特定字符串,如果有,返回常量引用,如果沒有,加入常量池并返回該常量引用);
  • 在堆中分配內存時,有“指針碰撞”和“空閑列表”兩種分配方式,具體哪種,需要看垃圾收集器具體采用何種GC算法;
  • 對象的內存有三部分構成:對象頭、示例數據和對齊填充;
  • 對象頭有兩部分構成:第一部分用于存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標志、線程持有鎖、偏向線程ID、偏向時間戳等;對象頭的另一部分是類型指針、即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的示例。另外,如果這個對象是一個數組,對象頭中海油一塊用于記錄數組長度的數據。
  • 對象的示例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容,無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。
  • 對象的訪問定位,通過棧上的reference數據來操作堆上的具體對象。目前主流的訪問方式有使用句柄和直接指針兩種方式。如果使用句柄訪問的話,那么Java堆中會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象示例數據與類型數據各自的地址信息;如果使用直接指針訪問,那么Java堆對象的布局就必須考慮如果安置訪問類型的相關信息,而reference中存儲的直接就是對象地址。使用句柄的最大好處是在對象被移動(垃圾收集時)時,只會改變句柄中的示例數據指針,而不用修改reference本身;使用直接指針方式最大的好處在于速度更快,省去了一次指針定位的時間開銷;HotSpot使用的是直接指針方式進行對象定位的。

垃圾收集器與內存分配策略
  • 程序計數器、虛擬機棧和本地方法棧,隨線程生,隨線程滅。虛擬機棧、本地方法棧中的各棧幀需要分配多大內存,在編譯器就可確定下來,故這部分內存回收具有可確定性;
  • 確定對象是否存活:引用計數法(有循環引用問題)和可達性分析。可達性分析算法的基本思路: 通過一系列稱為“GC Roots”的對象作為起點,從這些節點開始向下搜索,搜索走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
  • 在Java中,可作為GC Roots的對象包括: 虛擬機棧(棧幀中的本地變量表)中引用的對象、方法區中類靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中JNI引用的對象。
  • 如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址,稱這塊內存代表一個引用。
  • 引用分為:強引用(只要引用關系還在,就永遠不會回收掉被引用的對象)、軟引用(用來描述一些還有用但并非必須的對象,內存溢出之前會把他們進行二次GC)、弱引用(也是用來描述非必須對象,強度比軟引用還弱一些、被弱引用關聯的對象只能生存到下一次垃圾收集發生之前)、虛引用(引用關系中最弱,不會對對象的生存時間構成影響,也無法通過虛引用來取得一個對象的示例,為一個對象設置虛引用的唯一目的就是能在這個對象被垃圾收集器回收之前收到一個系統通知)。
  • 對象真正的死亡,需要經過兩次標記:第一次經過可達性分析以后發現沒有與GC Roots相連接的引用鏈,將會進行第一次標記并且進行一次篩選,依據是是否覆寫了finilize()方法或該方法是否已經執行過一次,如果發現需要執行finilize()方法,便會將對象放置到F-Queue隊列中,并在稍后由虛擬機自動創建的、低優先級的Finalizer線程去執行它。虛擬機觸發這個方法,并不能保證這個方法會執行完,如finilize()方法中出現死循環的時候虛擬機不會等待這個方法做完。如果在執行finilize()方法的時候,該對象重新與引用鏈上的任何一個對象建立關聯,在這第二次標記的時候會被移除出“即將回收”的集合;
  • finilize()方法只會被系統執行一次,如果對象面臨下一次回收,它的finilize()方法不會再次執行。
  • 回收方法區:廢棄常量和無用類。判斷無用類需要滿足三點:該類的所有示例都已經被回收,也就是Java堆中不存在該類的任何引用;加載該類的ClassLoader已經被回收;該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法;
  • 垃圾收集算法:標記-清理,缺點:效率低+產生大量不連續空間,導致大對象無法再次分配;復制算法,缺點,存在空間浪費,不能百分之百的利用空間去給對象分配內存;標記-整理,標記完以后讓所有存活的對象都向一端進行移動;分代收集:年輕代,朝生夕死,采用復制算法,老年代,存活時間比較久,采用標記-清理或者標記-整理算法;
  • 大對象直接進入老年代。大對象是指:需要大量連續空間的Java對象,比如很長的字符串和數組;
  • 長期存活的對象將進入老年代。在GC過程中,年輕代中達到年齡閾值(默認為15)的對象會被移入老年代;
  • 動態對象年齡判定,虛擬機并不是永遠要求對象的年齡必須達到了MaxTrenuringThreshold才能晉升老年代,如果在Sorvivor空間中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或者等于該年齡的對象就可以直接進入老年代,無需等到達到MaxTenuringThreshold中要求的年齡;

類文件結構
  • 實現語言無關性的基礎仍然是虛擬機和字節碼存儲格式。
  • Class文件中的常量池主要存放兩大類常量:字面量和符號引用。
  • 字面量比較接近Java語言層面的常量的概念,如文本字符串、聲明為final的常量值等。
  • 符號引用包含了三類常量:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符。
  • 動態鏈接,在虛擬機加載Class文件的時候才進行“鏈接”操作,所以Class文件中不會保存各個方法、字段的最終的內存布局信息。這些字段、方法的符號引用不經過運行期轉換的話,無法得到真正的內存入口地址,也就無法直接被虛擬機使用。
  • Class文件中的方法、字段都是要引用CONSTANT_Uft8_info型常量來描述名稱,所以CONSTANT_Utf8_info型常量的最大長度也就是Java中方法、字段名的最大長度。
  • 分析Class文件字節碼的工具:javap
  • 字段表集合中不會列出從超類或者父接口中繼承而來的字段,但是有可能列出原本Java代碼中不存在的字段,比如在內部類中為了保持對外部類的訪問性,會西東添加指向外部類示例的字段。
  • Java語言中的字段是無法重載的,兩個字段的數據類型、修飾符不管是不是相同,都必須使用不一樣的名稱。但對于字節碼來說,如果兩個字段的描述符不同,那字段重名就是合法的;
  • 與字段表集合相對應,如果父類方法在子類中沒有被重寫,方法表集合中就不會出現來自父類的方法信息。
  • 在Java語言中,要重載一個方法,除了要與原來的方法具有相同的簡單名稱之外,還必須要有不同的方法特征簽名。特征簽名就是一個方法中各個參數在常量池中的字段符號引用的集合;
  • 在Java代碼中的方法的特征簽名只包括了方法名稱、參數順序和參數類型;但在字節碼中方法的的特征簽名還包括方法返回值和受查異常表。但是在Java代碼中,即使兩個方法特征簽名完全一樣,但返回值不一樣,這兩個方法也是可以合理的共存于一個Class文件中的。
  • 在任何示例方法里面,都可以通過“this”關鍵字訪問到此方法所屬的對象,所以在示例方法的局部變量表中至少會存在一個指向當前對象示例的局部變量。
  • 編譯器使用異常表而不是簡單的跳轉命令來實現Java異常及finally處理機制。
  • 對于非static類型的變量(示例變量)的賦值,是在示例構造器<init>方法中進行的;而對于類變量,則有兩種方式可以選擇:在類構造器<clinit>方法中或者使用ConstantValue屬性。如果同時使用final和static來修飾一個變量,并且這個變量的數據類型為基本類型或者java.lang.String的話,就生成ConstantValue屬性來進行初始化,如果這個變量沒有被final修飾,或者并非基本類型及字符串,則會選擇<clinit>方法中進行初始化。之所以要求必須是基本類型或者String類型,是因為ConstantValue的值只是常量池的索引,而常量池存儲的是字面量和符號引用,符號引用需要在類加載的時候才能轉化為直接內存地址,所以在生成Class階段,即使ConstantValue屬性想支持別的類型也無能為力;
  • Java泛型的實現采用的是擦除法,在字節碼中,泛型信息編譯之后會被通通擦除掉。Signature屬性就是為了彌補這個缺陷而增設的,Java反射能夠獲取泛型類型,最終的數據來源,也是根據這個屬性。
  • Java虛擬機的指令由一個字節長度的、代表這某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其后的零至多個代表此操作所需的參數(稱為操作數,Operands)而構成。Java虛擬機是面向操作數棧而不是寄存器架構,所以大多數的指令都不包含操作數,只有一個操作碼。
  • 處理浮點數運算時,不會拋出任何運行時異常,如果一個操作產生溢出,將會使用有符號的無窮大來表示;如果一個操作結果沒有明確的數學定義的話,將會用NaN的值來表示,所有使用NaN的值作為才作數的算術操作,結果都會返回NaN。
  • 虛擬機的實現主要有兩種方式:
    • 將輸入的Java虛擬機代碼在加載或執行時翻譯成另外一種虛擬機的指令集;
    • 將輸入的Java虛擬機代碼在加載或執行時翻譯成宿主機CPU的本地指令集(即JIT代碼生成技術);

虛擬機類加載機制
  • 虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制;
  • 類從被加載到虛擬機內中開始,到卸載出內存為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備和解析三個階段統稱為連接。
  • 虛擬機規定了5種情況下必須立即對類進行“初始化”:
    • 遇到new、getstatic、putstatic或invokestatic這4條指令時,如果類沒有進行過初始化,則需要先觸發其初始化操作。常見的常見是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果寫在常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
    • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化;
    • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化;
    • 當虛擬機啟動的時候,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類;
    • 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle示例最后的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
  • 通過子類引用父類的靜態字段,不會導致子類初始化。對于靜態字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
  • 通過數組定義來引用類,不會觸發此類的初始化。
    public class Test{
        public static void mian(String[] args){
            Clazz[] array = new Clazz[10]; // 此時不會觸發Clazz類的初始化
         }
    }
  • 字面常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
    public class ConstClass{
        static {
            System.out.println("ConstClass init!");
        }
        
        public static final String HELLOWORLD = " hellow world ";
    }
    
        public class Test{
        public static void mian(String[] args){
            System.out.println(ConstClass.HELLOWORLD); // 此時不會觸發ConstClass類的初始化,不會輸出"ConstClass init!"
         }
    }
  • 接口初始化時機與類不同的在于第3種:當一個類在初始化時,要求其父類全部都已經經過初始化了,但是一個接口在初始化時,并不要求其父接口全部都已經完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。
  • 在加載階段,虛擬機需要完成以下3件事情:
    • 通過一個類的全限定名來獲取定義此類的二進制字節流;
    • 將整個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;
    • 在內存中生成一個代表這個類的java.lang.Class對象,作為方法去這個類的各種數據的訪問入口;
  • 對于數組類而言,本身不通過類加載器創建,它是有java的虛擬機創建的。
  • 加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需格式存儲在方法區之中,然后在方法區中實例化一個java.lang.Class類的對象,它將作為程序訪問方法區中的這些類型數據的外部接口。
  • 驗證階段完成4個階段的校驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證;
  • 文件格式驗證,驗證字節流是否符合Class文件格式規范,并且能被當前版本的虛擬機處理;
  • 元數據驗證,對類的元數據信息進行語義分析,保證其描述的信息符合Java語言規范的要求;
  • 字節碼驗證,對類的方法體進行校驗分析,保證被校驗的類的方法在運行時不會做出危害虛擬機安全的事件。
  • 符號引用驗證,該驗證過程發生在講符號引用轉化為直接引用的時候-也就是解釋階段,該階段保證解析動作能夠正常執行;
  • 準備階段是正式為類變量分配內幕才能并設置類變量初始化的階段,這些變量所使用的內存都將在方法區中進行分配。這個時候分配的變量僅僅包括類變量(被static修飾的變量),不包括示例變量。
public static int value = 123;//在準備階段以后,初始值是0,而不是123;而賦值為123的動作發生在初始化階段
  • 如果類字段屬相表中存在ConstantValue屬性,將在準備階段變量value就會被初始化為ConstantValue屬性所指定的值。
public static final int value = 123; //編譯時會為該字段生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue屬性將value初始化為123;
  • 解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
  • 符號引用,以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用的字面量形式明確定義在Java虛擬機規范的Class文件格式中。
  • 直接引用,直接指向目標的指針、相對偏移量或一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。
  • 在需要操作符號引用的字節碼在執行之前,必須先對他們所使用的符號引用進行解析。
  • 虛擬機對同一個符號引用解析的結果會進行緩存,在運行時常量池中記錄直接引用,并把常量標識為已解析狀態,從而避免解析動作重復進行。
  • 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
  • 類或接口符號引用的解析,如果該類從未被解析過:如果對象不是數組,則會把該類的全限定名交給調用類的類加載器進行加載,此時可能會觸發元數據驗證、字節碼驗證以及觸發其他相關的類加載動作;如果是數組,并且數組的元素類型是對象,則會把元素類型按照上述進行加載解析。解析完后會驗證訪問權限。
  • 字段的符號引用的解析,首先會解析字段表內class_index項中索引的CONSTANT_Class_info所代表的類進行符號引用解析。解析完字段所代表的類以后,按照先類本身、再按照繼承關系從下往上搜索接口、最后按照繼承關系從下往上遞歸搜索其父類,在此過程中,如果搜索到簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。如果查找失敗,拋出java.lang.NoSuchFieldError異常。最后還會對權限進行驗證。
  • 類方法符號引用的解析,與字段解析一樣,首先會解析字段表內class_index項中索引的CONSTANT_Class_info所代表的類進行符號引用解析。在解析的收,如果發現class_index中索引的類是個接口,則會拋出java.lang.IncompatibleClassChangeError異常。如果解析出了方法所述的類或接口C,會按照先C類本身、再類C的父類中遞歸的順序進行查找,如果找到了簡單名稱和描述符都與目標方法相匹配的方法,則返回這個方法的直接引用,查找結束;否則在類C的接口列表及它們的父接口之中遞歸查找,如果簡單名稱和描述符都與目標相匹配的方法,則說明這個方法在類C中沒有實現,拋出java.lang.AbstractMethodError異常。如果上述過程都沒有找到,則拋出java.lang.NoSuchMethodError。同樣,如果解析出了方法的直接引用,同樣要檢驗權限。
  • 接口方法符號引用的解析,首先解析出接口方法表class_index項中索引的方法所屬的類或接口的符號引用,如果解析成功,會先校驗解析出的接口C,如果發現是類而不是接口,會拋出java.lang.IncompatibleClassChangeError異常。然后會按照先接口C、再遞歸查找C的父接口(直到Object類),如果找到簡單名和描述符都與目標相同的方法,則會直接返回該方法的直接引用,否則,會拋出java.lang.NoSuchMethod異常。由于接口中所有的方法默認都是public的,所以不存在訪問權限問題,所以不會拋出java.lang.IllegalAccessError異常。
  • 初始化階段, 是執行類構造器<clinit>方法的過程:
    • 自動給類變量(static修飾的變量)和靜態語句塊(static {}塊),順序按照語句在源文件出現的順序。靜態語句塊只能訪問到定義在靜態語句塊之前的變量,但卻可以給定義在靜態語句塊之后的靜態變量進行賦值,但卻不能訪問。
    • <clinit>方法與類的構造器(示例構造器<init>)不同,它不需要顯式的調用父類構造器,虛擬機會保證子類的<clinit>方法執行前,父類的<clinit>方法已經執行,所以虛擬機中第一個被執行的類構造器<clinit>方法的類肯定是java.lang.Object。
    • 由于父類的<clinit>方法要先執行,也意味著父類的靜態語句塊和變量的賦值操作要優先于子類的靜態語句塊先執行。
    • <clinit>方法并不是必須的,如果沒有靜態語句塊也沒有對變量的賦值操作,編譯器可以不生成該方法。
    • 接口的<clinit>方法對于接口而言(接口不可以有靜態語句塊,但可以定義變量,有變量的初始化的賦值操作),不像類那樣,執行接口的<clinit>方法,并不需要先執行父接口的<clinit>方法。只有當父類中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時,也不會執行接口的<clinit>方法。
    • 一個類的<clinit>方法在多線程環境下,會被正確的加鎖、同步,多個線程同時執行一個類的構造器方法,則只有一個線程會執行,其他的線程會被阻塞等待。
  • 類加載器的作用:通過一個類的全限定名來獲取描述此類的二進制字節流。
  • 每一個類加載器,都擁有一個獨立的名稱空間,所以比較兩個類是否“相等”,只有兩個類是由同一個類加載器加載的前提下才有意義。兩個類相等,包括類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回接口,也包括instanceof關鍵字做對象所屬關系的判定等情況。
  • 類加載器,一般分為三種:啟動類加載器(可以理解為加載java系統等被虛擬機識別的類,用戶不能直接使用)、擴展類加載器(加載用戶導入的jar等)和應用程序加載器(負責加載用戶路徑ClassPath上指定的類庫)。
  • 類加載器的模型是雙親委派模型,除了頂層的啟動類加載器,其余的類加載器都要有自己的父類加載器,但是他們之間的關系一般不會是繼承,而是使用組合的關系來復用父加載器的代碼。
  • 雙親委派模型,如果一個類加載器接收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給自己的父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

虛擬機字節碼執行引擎
  • 在不同的虛擬機實現里面,執行引擎在執行Java代碼的時候可能會有解釋執行(通過解釋器執行)和編譯執行(通過即時編譯器產生本地代碼執行)兩種選擇,也可能兩者兼備。
  • 運行時棧幀,是用于支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操作數棧、動態廉潔和方法返回地址等信息。每一個方法從調用開始至執行完成的過程,都對應著一個棧幀在虛擬機棧里面從入棧到出棧的過程。
  • 在編譯程序代碼的時候,每一個棧幀需要多大的局部變量表,多深的操作數棧都已經完全確定,寫在方法的Code屬性之中。
  • 在活動的線程中,只有位于虛擬機棧頂的棧幀才是有效的,稱為當前棧幀(Current Stack Frame),與這個棧幀相關聯的方法稱為當前方法(Current Method)。執行引擎運行的所有的字節碼指令都只針對當前棧幀進行操作。
  • 局部變量表是一組變量值的存儲空間,用于存放方法參數和方法內定義的局部變量。編譯時,局部變量表的最大空間寫在了Code屬性的max_locals中,單位是Slot。
  • 虛擬機的實現要求通過一個引用變量要能做到兩點:
    • 從此引用中直接或間接的查找到對象在Java堆中的數據存放的起始地址;
    • 次引用中直接或間接查找到對象所屬的數據類型在方法區中存儲的類型信息;
  • 由于局部變量表在虛擬機棧幀中,是屬于線程私有數據,所以對于long和double來說,連續讀寫兩個Slot,不會引起數據安全問題。
  • 虛擬機通過索引定位的方式使用局部變量表,索引值的范圍從0開始至局部變量表最大的Slot數量。
  • 在方法執行時,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的。
  • 局部變量表可以復用,有時會造成某些時候對象不能被回收的情況出現。而一個對象能否被回收的根本原因是:局部變量表的Slot是否還存有關于該對象的引用。
  • 類變量有兩次附初始值的過程,一次是在準備階段,賦予系統的初始值;另外一次是在初始化階段,賦予程序員定義的初始值。但是一個局部變量表定義了但沒有賦予初始值是不能使用的。
  • 操作數棧的最大深度在編譯的時候寫在了Code屬性的max_stacks數據項中。每一個元素可以使任意的java數據類型,在方法執行的時候,操作數棧的最大深度不會超過max_stacks數據項中設定的最大值。
  • 當一個方法剛剛開始執行的時候,這個方法的操作數棧是空的,在方法的執行過程中,會有各種字節碼指令往操作數棧中寫入和提取內容,也就是出棧/入棧操作。
  • 操作數棧中的元素的數據類型必須與字節碼指令的序列嚴格匹配。
  • Java虛擬機的解釋執行引擎稱之為“基于棧的執行引擎”,這個棧指的就是操作數棧。
  • 每個棧幀都包含一個執行運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接。Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中執行方法的符號引用作為參數。這些符號引用一部分會在類的加載階段或者第一次使用的時候就轉化為直接引用,這個轉化成為靜態解析。另一部分將在每一次運行期間轉化為直接引用,這部分成為動態連接。
  • 當一個方法開始執行時,有兩種方法可以退出這個方法:遇到任意一個方法返回的字節碼指令,稱之為正常完成出口;另一種是遇到了異常(虛擬機內部的異常或者athrow字節碼異常指令),稱之為異常完成出口。
  • 方法退出的過程實際上就等同于把當前棧幀出棧,因此可能執行的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向方法調用指令后邊的一條指令等。
  • 方法調用并不等同于方法執行,方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪個方法),暫時還不涉及方法內部的具體運行過程。
  • 一切方法調用在Class文件里邊存儲的都只是符號引用,而不是方法在實際運行時內存布局中的入口地址(直接引用)。需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。
  • 所有的方法調用中的目標方法在Class文件里面都是一個常量池中的符號引用,在類加載的解析階段,會有一部分符號引用轉化為直接引用,即靜態解析,它成立的前提是:方法在真正運行之前就有一個可確定的調用版本,并且這個方法的調用版本在運行期間是不可變的。
  • 在java語言中符合“編譯器可知,運行期不可變”這個要求的方法,主要包括“靜態方法”和“私有方法”兩大類。
  • java提供了5條方法調用字節碼指令:
    • invokestatic:調用靜態方法;
    • invokespecial:調用示例構造器<init>方法、私有方法和父類方法。
    • invokevirtual:調用所有的虛方法。
    • invovkeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。
    • invokedynamic:先在運行時動態解析出調用點限定符所引用的方法,然后再執行該方法。
  • 只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段中確定唯一的調用版本,符合這個條件的有靜態方法、私有方法、實力構造器、父類方法4類,它們在類加載的時候就會把符號引用解析為該方法的直接引用。這些方法稱之為非虛方法。
  • java中的非虛方法除了使用invokestatic\invokespecial調用方法之外還有一種,就是被final修飾的方法。雖然fianl方法是使用invokevirtual指令來調用的,但它無法被覆蓋,也無需對方法接收者進行多態選擇。
  • 解析調用是一個靜態的過程,在編譯器期間就可以完全確定,在類加載的解析階段就會把涉及的符號引用全部轉變為可確定的直接引用,不會延遲到運行期再去完成。而分派調用則可能是靜態的也可能是動態的,根據分派依據的宗量數可分為單分派和多分派。
  • 靜態類型(外觀類型)與實際類型,靜態類型的變化僅僅在使用時發生,變量本身的靜態類型不會被改變,并且最終的靜態類型在編譯器是可知的;而實際類型變化的結果在運行期才可確定,編譯器在編譯程序的時候并不知道一個對象的實際類型是什么。
 //Human稱為變量的靜態類型或外觀類型
 //Man稱為變量的實際類型
 Human man = new Man();

 //實際類型的變化
  Human man = new Man();
  man = new Woman();
  //靜態類型變化
  sr.sayHello((Man)man);
  sr.sayHello((Woman)man);
  • 在有重載的情況下的方法調用,使用哪個重載版本,完全取決于傳入參數的數量和數據類型。虛擬機(準確說是編譯器)在重載時是通過參數的靜態類型而不是實際類型作為判斷依據的。并且靜態類型是編譯器可知的,因此,在編譯階段,Javac編譯器就會根據參數的靜態類型決定使用哪個重載版本。
  • 所有依賴靜態類型來定位方法之行版本的分派動作稱之為靜態分派。靜態分派的典型應用是重載。
  • invokevirtual指令運行時解析過程大致分為以下幾個步驟:
    • 找到操作數棧頂的第一個元素所指向對象的實際類型,記作C。
    • 如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,通過則返回這個方法的直接引用,查找過程結束。如果不通過,返回非法訪問異常。
    • 否則,按照繼承關系從下往上依次對C的各個父類進行上述步驟的搜索和驗證。
    • 如果始終沒有找到合適的方法,拋出java.lang.AbstractMethodError異常。
  • 把在運行期根據實際類型確定方法執行版本的分派過程稱為動態分派。典型的應用是方法重寫。重寫的本質是:invokevirtual指令把常量池中類方法符號引用解析到了不同的直接引用上。
  • 方法的接收者和方法的參數統稱為方法的宗量。
  • Java語言的動態分派屬于單分派類型。所以在Java發展到1.8之前,屬于靜態多分派,動態單分派的語言。
  • 由于動態分派是非常頻繁的動作,基于性能的考慮,最常用的“穩定優化”手段就是在類的方法區中建立一個虛方法表(與此對應的,也會有接口方法表),使用虛方法表索引來代替元數據查找以提高性能。
  • 虛方法表中存放著各個方法的實際入口地址,如果某個方法在子類中沒有被復寫,那子類的虛方法表里邊的地址入口和父類相同方法的地址是一致的,都指向父類的實現入口。如果子類復寫了這個方法,子類方法表的地址會替換為指向子類實現版本的入口地址。
  • 為了程序實現上的方便,具有相同簽名的方法,在父類、子類的虛方法表中都應當具有一樣的索引序號,這樣當類型變換時,僅僅需要變更查找的方法表,就可以從不同的虛方法表中按索引轉換出所需方法的入口地址。
  • 方法表一般在類加載的連接階段進行初始化,準備了類的變量初始值后,虛擬機會把該類的方法表也初始化完畢。
  • MethodHanle的使用方法和效果與Reflection的區別有如下幾點:
    • 從本質上講,Reflection和MethodHandle機制都在模擬方法調用,但是Reflection是在模擬Java代碼層次的方法調用,而MethodHandle是在模擬字節碼層次的方法調用。
    • Reflection中的java.lang.reflect.Method對象遠比MethodHandle機制中的java.lang.invoke.MethodHandle對象包含的信息多。前者是方法在Java一端的全面映射,包含了方法簽名、描述符以及方法屬性表中各種屬性的Java端的表現方式,還包含執行權限等運行期信息。后者僅僅包含與執行方法相關的信息。Reflection是重量級的,MethodHandle是輕量級的。
    • 由于MethodHandle是對字節碼的方法指令的模擬,所以理論上虛擬機在這方面做的各種優化在MethodHandle上是支持的,但是java反射則不行。
  • 現代的高級語言,在執行前先對程序源碼進行詞法分析和語法分析處理,把源碼轉化為抽象語法樹。
  • 基于棧的指令集主要優點就是可移植,寄存器由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免的要受到硬件的約束。還有其他的優點如diamante相對更加緊湊(字節碼中的每個字節就對應一條指令,而多地址指令集中還需要存放參數)、編譯器實現更加簡單(不需要考慮空間分配問題,所需空間都是在棧上操作)等。主要的缺點就是執行起來相對會稍慢一些。

早期(編譯期)優化
  • Java語言的“編譯期”其實是一段“不確定”的操作過程,它可能包含幾個個過程:
    • 可能是指一個前端編譯器把.jva文件轉變成.class文件的過程;
    • 也可能是指虛擬機的后端運行期編譯器(JIT編譯器)把字節碼轉變成機器碼的過程;
    • 還可能指使用靜態提前編譯器(AOT編譯器)直接把*.java文件編譯成本地機器代碼的過程。
  • 虛擬機運行時不支持一些語法,他們在編譯階段還原回簡單的基礎語法結構,這個過程稱為解語法糖。Java中最常用的語法糖主要有:泛型、變長參數、自動裝箱/拆箱等、內部類、枚舉類、斷言語句、枚舉、字符串的switch;
  • 泛型是JDK1.5的一項新增特性,它的本質是參數化類型的應用,也就是說所操作的數據類型被指定為一個參數。Java泛型只存在于源碼中,編譯后的字節碼文件中,已經替換為原生類型了,并在相應的地方插入了強制轉型代碼,所以這種采用擦除法實現的泛型是一種偽泛型,只是一個語法糖而已。
  • 僅僅是泛型的類型參數不同的方法是無法重載的,因為編譯成字節碼的時候把類型參數進行擦除導致方法簽名一樣。但是有不同的返回值缺可以正常編譯,因為:在Java代碼中的方法的特征簽名只包括了方法名稱、參數順序和參數類型;但在字節碼中方法的的特征簽名還包括方法返回值和受查異常表。但是在Java代碼中,即使兩個方法特征簽名完全一樣,但返回值不一樣,這兩個方法也是可以合理的共存于一個Class文件中的。
  • Signature屬性存儲一個方法在字節碼層面的特正確簽名,這個屬性中板寸的參數類型并不是原生類型,而是包括了參數化類型信息。
  • 自動裝箱、拆箱,在編譯之后被轉化成了對應的包裝和還原方法,如Integer.valueOf()與Integer.intValue()方法;
  • 遍歷循環,則把代碼還原升了迭代器的實現。
  • 變長參數,它在調用的時候變成了一個數組類型的參數。
  • 要實現C++中的條件編譯,在Java中只要使用條件為常量的if語句即可,編譯器會把條件不成立的代碼塊擦除掉。
if(true) {
    //此處會被編譯,生成字節碼
} else {
    //此處不會被編譯,不會生成對應的字節碼
}
  • 如果想在編譯器做一些事情,可以考慮采用注解處理器來實現,需要繼承抽象類javax.annotation.processing.AbstractProcessor。

晚期(運行期)優化
  • Java程序最初是通過解釋器進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為“熱點代碼”,為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優化,完成這個任務的編譯器稱為即使編譯器(Just In Time Compiler)。
  • 解釋器與編譯器并存的的架構的優點:當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯時間,立即執行。當程序運行后,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲得更高的效率。當程序運行環境中內存資源的限制較大,可以使用解釋器執行節約內存,反之可以使用編譯執行來提升效率。同時,解釋器還可以作為編譯器激進優化時的一個逃生門。
  • 在運行過沉重,會被即使編譯器編譯的“熱點代碼”有兩類,即:
    • 被多次調用的方法
    • 被多次實行的循環體
  • 判斷一段代碼是不是熱點代碼,是不是需要觸發即使編譯,這樣的行為被稱為“熱點探測”,目前主要的熱點探測判定方式有兩種:
    • 基于采樣的熱點探測:虛擬機周期性的檢查各線程中虛擬機棧的棧頂,如果發現某個方法經常出現在棧頂,則判定為“熱點方法”;
    • 基于計數器的熱點探測:采用這種方法的虛擬機會為每個方法(甚至是代碼塊)建立計數器,統計方法的執行次數,如果執行次數超過一定的閾值就認為是“熱點方法”。
  • 被即時編譯的方法和循環體(循環體多次調用也會導致整個方法被進行編譯)發生在方法執行的過程中,因此形象的稱之為棧上替換(On Stack Replacement)。
  • 在HotSpot虛擬機中使用的是基于計數器的熱點探測方法,每個方法有兩類計數器:方法調用計數器和回邊計數器。
    • 方法調用計數器,統計方法被調用的次數(方法調用計數器熱度的衰減)。
    • 回邊計數器,統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向后跳轉的指令稱為“回邊”。
  • 在默認情況下,無論是方法調用產生的即使編譯請求,還是OSR編譯請求,虛擬機在代碼編譯器還未完成之前,都仍然將按照解釋方尺繼續執行,而編譯動作則在后臺的編譯線程中進行。
  • 方法內聯優化:把目標方法代碼“復制”到發起調用的方法之中,避免發生真實的方法調用而已。
  • 方法內聯的目的:去除方法調用成本(如建立棧幀等)和為其他優化建立良好基礎,方法內聯膨脹之后可以便于在更大范圍上采取后續的優化手段。
  • 公共子表達式消除:如果一個表達式E已經計算過了,并且從先前計算到現在E中所有變量的值都沒有發生變化,那么E這次出現就成為了公共子表達式,對于E,直接用前面計算過的結果就可以了。
  • 數組邊界檢查消除:除了盡可能把運行期檢查提到編譯期完成完成的思路之外,還有一種避免思路,隱式異常處理。
  • 編譯期在進行內聯時,如果是非虛方法,那么直接進行內聯就可以了。如果遇到虛方法,則會向“類型繼承關系分析”查詢此方法在當前程序下是否有多個目標版本可供選擇,如果只有一個版本,也可以進行內聯。不過這種內聯屬于激進優化,需要預留一個“逃生門”,稱為守護內聯。如果程序的后續執行過程中,虛擬機一直沒有加載到會令這個方法的接收者的繼承關系發生變化的類,那這種內聯優化的代碼就可以一直使用下去。但如果加載了導致繼承關系發生變化的新類,那就需要拋棄已經編譯的代碼,退回到解釋狀態執行,或者重新進行編譯。如果通過“類型繼承關系分析(CHA)”查詢有多個版本的目標方法可供選擇,會使用內聯緩存來完成內聯。
  • 逃逸分析:逃逸分析的基本行為就是分析對象動態作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他方法中,稱為方法逃逸。甚至還可能被外部線程訪問到,比如賦值給類變量或者可以在其他線程中訪問的實例變量,稱為線程逃逸。
  • 如果能證明一個對象不會逃逸到方法或者線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個對象,則可以對其進行如下的優化:
    • 棧上分配:Java堆中的對象對于各個線程都是共享和可見的,依賴于垃圾回收。如果一個對象不會逃逸到方法之外,那讓這個對象在棧上分配內存,這個對象就會隨著方法的結束而自動銷毀了;
    • 同步消除:線程同步本身相對比較耗時,如果逃逸分析確定一個變量不會逃逸出線程,無法被其他線程訪問,則可以進行同步消除;
    • 標量替換:標量是指一個數據已經無法再被分解成更小的數據來表示了,如原始數據類型;相對的是聚合量。如果逃逸分析證明一個對象不會被外部訪問,并且這個對象可以被拆散的話,那程序真正執行的時候可能不創建這個對象,而改為直接創建它的若干個被這個方法使用到成員變量來代替。將對象拆分后,除了可以讓對象的成員變量在棧上分配和讀寫之外,還可以為后續進一步優化手段創建條件;

Java 內存模型與線程
  • Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與Java編程中所說的變量有所區別,它包括了示例字段、靜態字段和構成數組對象的元素,不包括局部變量和方法參數,他們是線程私有。
  • Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之前也無法直接訪問對方工作內存中的變量,線程間的變量值的傳遞均需要通過主內存來完成。
  • Java內存模型中定義了以下8種操作來完成工作內存與主內存之間數據讀取和同步的操作:
    • lock(鎖定):作用于主內存的變量,它把一個變量標識為一條線程獨占的狀態;
    • unlock(解鎖):作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放出來后的變量才可以被其他線程鎖定;
    • read(讀取):作用于主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用;
    • load(載入):作用于工作內存的變量,它把read操作從主內存得到的變量值放入工作內存的變量副本中;
    • use(使用):作用于工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
    • assign(賦值):作用于工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
    • store(存儲):作用于工作內存的變量,它把工作內存的一個變量的值傳送到主內存中,以便以后的write操作使用。
    • write(寫入):作用于主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存變量中。
  • 如果要把一個變量從主內存復制到工作內存,就要順序的執行read和load操作,如果要將一個變量從工作內存同步回主內存,就要順序的執行store和write操作。
  • volatile是Java虛擬機提供的最輕量級的同步機制,當一個變量定義為volatile之后,它將具備兩種特性:
    • 第一是保證次變量對所有線程的可見性(一個線程修改了變量的值,其他線程可以立馬得知,是由于各個線程在使用該變量之前,都會對其工作內存中的該變量進行刷新)。由于解釋執行或者編譯執行時,只有用到變量的字節碼會觸發刷新變量的操作,所以會導致不一致的問題存在。在不符合以下兩條規則的運算場景中,仍然要通過加鎖來保證原子性:
      • 運算結果并不依賴變量的當前值,或者能夠確保只有單一線程修改變量的值;
      • 變量不需要與其他的狀態變量共同參與不變約束。
    • 使用volati變量的第二個語義是禁止指令重排序優化。
  • Java內存模型的三大特性:
    • 原子性:基本數據類型的訪問讀寫具備原子性,但是long和double有非原子性協定。synchronized關鍵字可以保證代碼塊之間的原子性;
    • 可見性:可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。除了volatile保證多線程變量的可見性,synchronized和final兩個關鍵字也能實現可見性。被final修飾的字段在構造器中一但初始化完成,并且構造器沒有把“this”的引用傳遞出去,那再其他線程中就能看見final字段的值(如果發生了this引用逃逸,可能在初始化的時候有其他線程也在初始化這個變量)。
    • 有序性:如果在本線程內觀察,所有操作都是有序的:如果在一個線程中觀察另一個線程,所有的操作都是無序的。Java余元提供了volatile和synchronized兩個關鍵字來保證線程之間的有序性。
  • 線程的主要實現方式:
    • 使用內核線程實現
    • 使用用戶線程實現
    • 使用用戶線程加輕量級進程混合實現
  • 線程調度是指系統未線程分配處理器使用權的過程,主要調度方式有兩種,分別是:
    • 協同式線程調度,線程的執行時間由線程本身來控制,線程工作完了主動通知系統切換到另外一個線程上;
    • 搶占式線程調度,每個線程將由系統來分配執行時間,線程的切換不由線程本身來決定;但可以通過設置優先級來讓系統多分配一些執行時間,但線程優先并不太靠譜;
  • 一個線程有且只有以下的一種狀態:
    • 新建(New):創建后尚未啟動的線程狀態
    • 運行(Runable):包括了操作系統線程狀態中的Running和Ready,可能正在執行,也可能正在等待CPU為它分配執行時間;
    • 無限期等待(Waiting):出于這種狀態的線程不會被分配CPU執行時間,他們要等待其他線程顯示的喚醒。以下方法會讓線程陷入無限期的等待狀態:
      • 沒有設置TimeOut參數的Object.wait()方法。
      • 沒有設置TimnOut參數的Thread.join()方法。
      • LockSupport.park()方法。
    • 限期等待(Timed Waiting):處于這種狀態的線程不會被分配CPU執行時間,不過無須等待其他線程顯示的喚醒,在一定時間后他們由系統自動喚醒。以下方法會讓線程陷入限期等待狀態:
      • 設置了TimeOut參數的Object.wait()方法。
      • 設置了TimnOut參數的Thread.join()方法。
      • LockSupport.parkNanos()方法。
      • LockSupport.parkUnitl()方法。
    • 阻塞(Blocked):線程阻塞與線程等待的區別是:阻塞狀態在等待這獲取到一個排他鎖,這個時間將在另外一個線程放棄這個鎖的時候發生;等待狀態是在等待一段時間或者喚醒動作的發生。在程序等待進入同步區域的時候,線程會進入阻塞狀態;
    • 結束(Terminated):線程已經結束執行的狀態;

線程安全與鎖優化
  • 線程安全的實現方法:
    • 互斥同步:同步是指在多個線程并發訪問共享數據時,保證共享數據在同一時刻只被一個(或者是一些,使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式。因此,在這4個字里邊,互斥是因,同步是果;互斥是方法,同步是目的。
      • 在Java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字在編譯以后,會在同步塊前后分別形成monitorenter和monitorexit這兩個指令,這兩個指令都需要一個reference類型的參數來指明要鎖定和解鎖的對象。
      • synchronized同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題。同步塊在已進入的線程執行完之前,會阻塞后面的其他線程進入。
      • Java的線程是映射到操作系統的原生線程之上的,如果要喚醒或阻塞一個線程,都需要操作系統來幫忙完成,這就需要從用戶態切到核心態中,因此狀態轉換需要耗費很多處理器時間(這也是為什么會出現偏向鎖等鎖優化措施);
      • java.utile.concurrent包中的重入鎖(ReentrantLock)也可以實現同步,與synchronized,ReentrantLock增加了一些高級功能,如:等待可中斷,可實現公平鎖,以及鎖可以綁定多個條件等
    • 非阻塞同步:互斥同步最主要的問題就是進行線程的阻塞和喚醒帶來的性能問題,因此也可以被稱之為阻塞同步。互斥同步屬于一種悲觀的并發策略,而非阻塞同步屬于基于沖突檢測的樂觀并發策略,通俗的說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了,如果共享數據有爭用,產生了沖突,那就再采取其他的補償措施(最常見的補償措施是不斷的重試,直到成功為止)。
      • 這種策略需要操作和沖突檢測這兩個步驟具備原子性,需要靠硬件來完成。
    • 無同步方案:以下兩種情況不需要進行同步操作來保證線程安全
      • 可重入代碼:純代碼,在代碼中任何時刻中斷它,等控制權返回后,原來的程序不會出現任何錯誤;可重入代碼有以下特征:不依賴存儲在堆上的數據和公共的系統資源、用到的狀態量都是由參數中傳入、不調用非可重入的方法等。判斷原則:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的數據,它就能返回相同的結果,它就滿足可重入性的要求,當然也就是線程安全的。
      • 線程本地存儲:如果一段代碼中所需的數據必須與其他代碼共享,如果能保證這些共享數據的代碼在同一個線程中執行,我們就可以把共享數據的可見范圍限制在同一個線程之內。
  • 鎖優化:
    • 自旋鎖:如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時并行執行,我們就可以讓后面的請求鎖的那個線程“稍等一下”,但不放棄處理器的執行時間(即不進行阻塞),看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。
    • 自適應自旋鎖:自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
    • 鎖消除:鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定依據來源于逃逸分析的數據支持。如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,就可以把它們當成棧上數據對待,認為它們是線程私有的,同步鎖自然就無需進行。
    • 鎖粗化:如果虛擬機探測到有一串零碎的操作都對同一個對象加鎖,將會把鎖的同步范圍擴展到整個操作序列的外部。
    • 輕量級鎖:由對象頭配合實現的一種機制,它并不是來代替重量級鎖的,它的本意是指在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。輕量級鎖能提升程序同步性能的依據是“對于絕大部分的鎖,在整個同步周期內都是不存在競爭的。”
    • 偏向鎖:如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下,把整個不同都消除掉,連CAS操作也不做了。偏向鎖,它會偏向第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠都不需要在進行同步。但是當一個線程獲得對象的偏向鎖以后,當有另外一個線程去嘗試獲取這個鎖時,偏向模式宣告結束。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容