深入理解 Jvm 讀書筆記(二)

Jvm 類加載及執行引擎相關

知識包括:

  • jvm類加載機制
    • 類加載時機及過程
    • 類加載器 雙親委派模型及如何破壞;
  • jvm字節碼執行引擎
    • 運行時棧幀結構
    • 方法調用等分析
    • 基於棧,基於寄存器指令集
  • 類加載及執行子系統的案例與實戰介紹
  • 程序編譯與代碼優化介紹

Jvm類加載機制

代碼編譯的結果是從本地機器碼轉變爲字節碼,jvm把描述類的數據從Class文件(二進制字節流)加載到內存,並對數據進行校驗,解析和初始化,最終可以被jvm直接使用的java類型;

類加載的時機

類的生命週期

加載(Loading) -> [驗證(Verification) -> 準備(Preparation) -> 解析(Resolution)] -> 初始化 (Initialization) -> 使用(Using) -> 卸載(Unloading)

  • 加載,驗證,準備,初始化,卸載的順序確定的;解析階段不一定,爲了支持java的運行時綁定(動態綁定,晚期綁定); 這些階段通常都是相互交叉混合式的進行的;

  • 有且只有以下情況,沒有類初始化需要進行類的初始化,簡稱對一個類的主動引用:

    • 遇到 new,getstatic,putstatic或invokestatic 字節碼指令;分別對應 實例化對象,讀取設置一個類的靜態字段(被final修飾,已在編譯器吧結果放入常量池的靜態字段除外,因爲使用的是ConstantValue初始化而不是方法),以及調用類的靜態方法;
    • 使用java.lang.reflect 包的方法對類進行反射調用的時候
    • 當初始化一個類時,發現其父類還沒有進行過初始化,需要先觸發器父類的初始化;
    • 當jvm啓動時,用戶需要指定一個要執行的主類(包含main方法的類),jvm會先初始化這個主類;
    • 使用jdk 1.7動態語言支持時,一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄;
  • 被動引用不會導致類初始化;

    • 子類引用父類的靜態字段,不會導致子類初始化;
    • 通過數組定義來引用類,不會觸發此類的初始化;
      • 不過jvm會生成一個直接繼承於java.lang.Object的子類,創建動作由字節碼指令newarray觸發(可否理解爲可加載解析,不會初始化);
    • 常量(final , ConstantValue屬性)在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,不會觸發定義常量類的初始化;
  • 接口的加載過程與類加載過程稍有不同

    • 接口在初始化時,並不要求父接口全部都完成了初始化,只有在真正使用到父接口(引用接口中定義的常量)採用初始化;

類加載的過程

  • 加載
    • 通過一個類的全限定名獲取定義此類的二進制字節流;
    • 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;
    • 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口;

加載階段完成後,jvm外部的二進制字節流就按照jvm所需格式存儲在方法區之中,然後在內存中(hotspot->方法區)實例化一個Class類對象,作爲程序訪問方法區中的這些類型數據的外部接口;


	- 實際上,jvm規範的這3條是非常靈活的
	
	非數組類的加載過程(加載階段獲取類的二進制字節流的動作)是開發人員可控性最強的;
	因爲加載階段既可以使用系統提供的引導類加載器完成,也可以使用用戶自定義的類加載器去完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方法(重寫一個類加載器的loadClass方法)

	數組類而言,數組類本身不通過類加載器創建,由jvm直接創建的;但是數組類的元素類型(ElementType 數組去掉所有維度的類型)最終是要靠類加載器去創建;數組類的創建: 
		- 如果數組的組件類型(Component Type 數組去掉一個維度的類型)是引用類型,遞歸加載組件類型;數組C在加載改組件類型的類加載器的類名稱空間上被標誌;
		(一個類必須與類加載器一起確定唯一性)
		- 如果數組的組件類型不是引用類型(int[]數組),jvm會把數組C標記爲與引導類加載器關聯;
		- 數組類的可見性與它的組件類型的可見性一致,組件類型不是引用類型,數組類的可見性默認爲public;
  • 驗證 鏈接階段的第一步,確保Class文件的字節流中包含的信息符合jvm的要求;

    • 文件格式驗證
      • 驗證字節流是否符合Class文件格式的規範,並且能被jvm處理;通過此階段後,字節流纔會進入內存的方法區中進行存儲;
    • 元數據驗證
      • 對字節碼描述信息進行語義分析,符合java語言規範;
    • 字節碼驗證
      • 確定程序語義是合法的,符合邏輯的,jdk1.6後Code屬性添加StackMapTable節省時間;
    • 符號引用驗證
      • 驗證jvm將符號引用轉換爲直接引用的時候,動作在解析階段中發生;對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗;
      • 無法通過,會拋出IncompatibleClassChangeError異常的子類,如IllegalAccessError,NoSuchFieldError,NoSuchMethodError;
  • 準備

    • 正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配;
      • 內存分配的僅包括類變量,不包括實例變量;
      • 通常情況下賦初值指的是java的默認初始值,但是類字段的字段屬性表中存在ConstantValue(final標記)屬性,準備階段的變量會被初始化ConstantValue屬性所指定的值;

	public static int value = 123;

	準備階段初始值爲0,而不是123,賦值123是putstatic字節碼指令被編譯後,存放在類構造器<clinit>方法之中,所以賦值123是在初始化階段纔會執行;

	public static final int value =123;

	準備階段初始值即爲123,因爲final修飾的字段,字段表中存在ConstantValue屬性,在編譯時javac將會爲value生成ConstantValue屬性,在準備階段jvm根據ConstantValue的值將value賦值爲123;
  • 解析

    • jvm將常量池內的符號引用替換爲直接引用的過程;
    • 引用區分:
      • 符號引用 (Symbolic References): 以一組符號來描述所引用的目標,可以是任何形式的字面量,與jvm實現的內存佈局無關;
      • 直接引用 (Direct References): 直接引用可以是直接指向目標的指針,相對偏移量或者一個能間接定位到目標的句柄; 如果有直接引用,則引用目標必定在內存中存在;
    • jvm規範中並未規定解析發生的具體時間,要求在執行anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield,putstatic16個用於操作符號引用的字節碼指令之前,先對他們所使用的符號引用進行解析;
    • invokedynamic 指令必須等到程序實際運行到這條指令的時候,解析動作才能進行,且不緩存;其餘符號指令可以在完成加載階段,還沒開始執行就進行解析,也可對第一次解析結果進行緩存,避免解析重複進行;
    • 解析動作主要針對接口,字段,類方法,接口方法,方法類型,方法句柄和調用點限定符7種符號引用,對應於常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info,CONSTANT_MethodType_info,CONSTANT_MethodHandle_info,CONSTANT_invokeDynamic_info7中常量類型;
      • 類或接口的解析 (D:當前代碼所處的類; N:從未解析過的符號引用;C:類或接口的直接引用;)
        • 如果不是一個數組類型,jvm將N的全限定名傳遞給D的類加載器加載這個類C;
        • 是一個數組類型, N的描述符類型添加[,按照1加載數組元素類型;
      • 字段解析 (C: 字段所屬的類或接口) 先解析字段表;
        • C本身包含簡單名稱和字段描述符都匹配,返回字段直接引用,查找結束;
        • 如果C中實現了接口,按照繼承關係從下往上遞歸搜索各個接口和它的父接口,接口中包含簡單名稱和字段描述符都匹配的字段,返回字段直接引用,查找結束
        • 否則,C不是java.lang.Object,按照繼承關係從下往上遞歸搜索其父類,找到簡單名稱和描述符都匹配的字段,返回字段直接引用,查找結束;
        • NoSuchFieldError
      • 類方法解析 (C: 類) 先解析類方法表;
        • 類方法和接口方法符號引用的常量類型是分開定義的,發現定義不同拋出IncompatibleClassChangeError異常;
        • 在C中查找簡單名稱和描述符都匹配的方法,如果有返回直接引用,查找結束;
        • 在C的父類中遞歸查找…
        • 在C實現的接口列表及父接口中遞歸查找匹配的方法,存在說明C爲抽象類,拋出AbstractMethodError異常(接口中存在static方法,不支持);
        • NoSuchMethodError
      • 接口方法解析 (C:類) 接口方法表;
        = 與類方法解析1相同;
        • 在接口C中查找是否有簡單名稱和描述符都匹配的方法,如果有返回直接引用,查找結束;
        • 在接口C的父接口遞歸查找,直到java.lang.Object類爲止,有則返回,結束查找;
        • NoSuchMethodError
  • 初始化

類加載的最後一步,初始化階段是執行類構造器<clinit>方法的過程;

  • <clinit> 方法特點
    • <clinit>方法 是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併生成的,收集順序是由語句在源文件中出現的順序決定;
      • 靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊中可以賦值,但是不能訪問;
    • <clinit>()方法與類的構造函數(實例構造器<init>()方法) 不同,不需要顯示的調用父類構造器;jvm會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢;
      • 因此 jvm中第一個被執行的<clinit>()方法的類肯定是java.lang.Object;
      • 由於父類的<clinit>()方法先執行,父類中定義的靜態語句塊要優先於子類的變量賦值操作;
    • <clinit>()方法對於類或接口不是必須的;
    • 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,接口和類都會生成<clinit>方法; 其中,執行接口的<clinit>方法不需要先執行父接口的<clinit>方法,只有到父接口中定義的變量使用時,父接口才會初始化;
    • jvm保證一個類的<clinit>()方法在多線程環境中被正確的加鎖,同步,多個線程同時去初始化一個類,只有一個線程去執行這個類的<clinit>方法,其他線程阻塞等待,當線程退出<clinit>方法後,其他線程喚醒後也不會再次進入到<clinit>方法,因爲同一個類加載器下,一個類型只會初始化一次;

類加載器

加載過程中 通過一個類的全限定名獲取類的類的二進制字節流 可以讓應用程序決定如何去獲取所需的類 ,實現這個動作的代碼模塊爲類加載器;

  • 類名稱空間: 對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在jvm中的唯一性; 每一個類加載器都擁有一個獨立的類名稱空間;

    • 即兩個類相等,只有兩個類由同一個類加載器加載的前提下才有意義;
    • 相等指的是代表類Class對象的equals,isAssignableFrom,isInstance,instanceOf等方法;
  • 雙親委派模式 Parents Delegation Model

    • 在jvm的角度說,只存在兩種類加載器
      • 啓動類(引導類)加載器(Bootstrap ClassLoader);
      • 其他類加載器(繼承於java.lang.ClassLoader)
    • java開發人員的角度,分爲3中
      • 啓動類(引導類)加載器(Bootstrap ClassLoader)
        • C++實現,負責加載 <JAVA_HOME>\lib目錄 或者被-Xbootclasspath參數所指定路徑,並且是jvm識別的類庫(僅按照文件名識別,如rt.jar)加載到jvm內存中;
        • 不可被java直接引用,用戶在編寫自定義類加載器,需要把加載請求委派給引導類加載器,直接使用null即可;
      • 擴展類加載器 (Extension ClassLoader)
        • sun.misc.Launcher$ExtClassLoader實現,負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量指定的路徑中的所有類庫;
        • 可被直接使用;
      • 應用程序類(系統類)加載器 (Application ClassLoader)
        • sun.misc.Launcher$AppClassLoader實現,負責加載用戶類路徑(ClassPath)上所指定的類庫;
        • 可直接使用,如果沒有自定義類加載器,一般就是程序默認類加載器;
      • BootstrapClassLoader <- ExtensionClassLoader <- ApplicationClassLoader <- CustomClassLoader 組合關係;
      • 如果一個類加載器收到類加載的請求,先委派給父類加載器完成,只有到父類加載器無法完成加載請求(搜索範圍中沒有找到所需的類),子加載器嘗試自己加載;
  • 破壞雙親委派模型

雙親委派模型並不是一個強制性的約束模型,雙親委派的破壞:

  • 第一次破壞: jdk 1.2之後添加 findClass方法;自定義類加載器邏輯寫入findClass中,在loadClass方法的邏輯如果父類加載失敗,則會調用自己的findClass方法完成加載,保證新寫出來的類加載器是符合雙親委派規則的;
  • 第二次破壞: 模型自身的缺陷,雙親委派雖然能很好的解決各個加載器基礎類的統一問題(越基礎的類由越上層的加載器加載);但是問題是基礎類調回用戶的代碼時, 比如JNDI由啓動類加載器加載(rt.jar),Jndi調用某些需要由獨立廠商實現並部署在應用程序的代碼,啓動類加載器加載的不能認識這些代碼(還有JDBC等,因爲是不同的類加載器加載,低層的可以認識頂層的,但頂層的不認識低層的);
    • 使用線程上下文類加載器兼容(Thread Context ClassLoader),通過Thread類setContextClassLoader方法設置,如果創建線程時還未設置,將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過,默認類加載器就是應用程序類加載器;
    • JNDI服務使用線程上下文類加載器加載所需要的SPI代碼(jndi接口提供者),也就是父類加載器請求子類加載器完成類加載的動作(我的理解是原本是啓動類加載器加載jndi,spi由應用類加載器加載,導致不能訪問;現在是設置上下文類加載器,jndi也是使用應用類加載器加載,spi由應用類加載器加載,同一個類加載器加載,可以訪問;)
  • 第三次破壞: 用戶對程序動態性的追求導致;如hotswap,熱部署等;
    • OSGi java模塊化標準,關鍵在於自定義的類加載器機制; 每一個程序模塊(OSGi成爲Bundle)都有一個自己的類加載器,需要更換一個Bundle時,把Bundle連同類加載器一起換掉實現代碼的熱部署;
    • OSGi爲網狀結構,不同於雙親的樹狀結構;

jvm字節碼執行引擎

運行時棧幀結構

  • 棧幀 Stack Frame
    • 用於支持jvm進行方法調用和方法執行的數據結構,是jvm運行時數據區中的jvm棧的棧元素; 存儲方法的局部變量表,操作數棧,動態連接,方法返回地址和一些額外的附加信息等信息;
    • 每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在jvm棧裏面從入棧到出棧的過程;
    • 在編譯程序代碼時,棧幀中需要多大的局部變量表,多深的操作數棧都已經完全確定了,並且寫入到方法表的Code屬性中,一個棧幀需要分配多少內存,僅僅取決於具體的jvm實現;
    • 在活動線程中,位於棧頂的棧幀纔是有效的,稱爲當前棧幀(CurrentStackFrame),與這個棧幀相關聯的方法稱爲當前方法(CurrentMethod),執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作;

在這裏插入圖片描述

  • 局部變量表 Local Variable Table

    • 一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量,寫入Code屬性確定最大容量max_locals;
    • 以變量槽(Variable Slot) 爲最小單位;每個slot都應該能存放boolean,char,byte,short,int,float,reference,returnAddress類型的數據;
      • reference 表示對一個對象實例的引用,直接或間接查找對象在java堆中數據存放的起始地址索引和在方法區中的存儲的類型信息;
      • returnAddress 爲jsr,jsr_w,ret服務,執行字節碼指令的地址,實現異常跳轉,現已經被異常表代替;
    • 對於64位的數據類型,jvm以高位對齊的方式分配兩個連續的Slot空間; long和double都是64位數據類型(reference可能是32位也可能是64位),不能單獨方位其中的某一個slot;
    • jvm 使用索引定位的方式使用局部變量表,索引值的範圍從0開始至局部變量表的最大slot數量;
    • 在方法執行時,jvm使用局部變量表完成參數值到參數變量列表的傳遞過程的,如果執行的是實例方法(非static方法),局部變量表中第0位索引的slot默認是用於傳遞方法所屬對象實例的引用,可以使用this來訪問到這個隱含的參數;
    • slot是可以重用的,方法體中定義的變量,作用域並不一定覆蓋整個方法體,如果當前字節碼pc計數器的值已經超出了某個變量的作用域,那這個變量對應的slot就可以交給其他變量使用;
      • 不使用的對象手動賦值null 此處注意局部變量表slot如果在變量所處作用域之後,手動對對象設置null值並不是一個無意義的操作,因爲可以去除slot對對象的引用,使對象提前被GC回收,而不是等到其他變量重用slot時在回收; 但是實際開發中並不需要賦值null;
  • 操作數棧 Operand Stack

    • 操作棧,後入先出(LIFO)棧,最大的寫入Code屬性確定最大容量max_stacks;
    • 操作數棧的每一個元素可以是任意的java數據類型,包括long和double;32位數據類型佔棧容量爲1,64位佔有棧容量爲2;
    • 當方法剛剛開始執行的時候,操作數棧是空的,執行過程中會有各種字節碼指令往操作數棧中出棧/入棧; eg: 整數加法的字節碼指令iadd運行時操作數棧最接近棧頂的兩個元素已經存入了兩個int型的數值,當執行這個指令時,會將兩個int值出棧並相加,然後將相加結果入棧;
    • jvm的解釋執行引擎稱爲基於棧的執行引擎,棧就是操作數棧;

在這裏插入圖片描述

  • 動態鏈接 Dynamic Linking

    • 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接;
    • Class文件的常量池存在大量符號引用,字節碼中的方法調用指令以常量池中指向方法的符號引用作爲參數;
      • 靜態解析: 這些符號引用一部分會在類加載階段或者第一次使用的時候轉化爲直接引用
      • 動態連接: 另外一部分將在每一次運行期間轉化爲直接引用;
  • 方法返回地址

    • 當一個方法執行後,只有兩種方法可以退出這個方法;

      • 執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口(Normal Method Invocation Completion);
      • 在方法執行過程遇到了異常,並且這個異常在方法體內沒有得到處理; 無論是jvm內部產生的異常還是代碼中使用athorw字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,稱爲異常完成出口(Abrupt Method Invocation Completion);
    • 方法退出的過程實際上就等同於把當前棧幀出棧,因此退出時可能執行的操作是: 恢復上層方法的局部變量表和操作數棧,把返回值(如果有)壓入調用者棧幀的操作數棧中,調整pc計數器的值以指向方法調用指令後面的一條指令;

  • 附加信息 (實際開發中,一般將動態連接,方法返回地址,其他附加信息統稱爲棧幀信息;)

方法調用

方法調用不等同於方法執行,方法調用階段的唯一任務是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部的具體運行過程; java中Class文件存儲的是符號引用,而不是具體地址,需要到類加載甚至到運行期間才能確定目標方法的直接引用;

  • 解析

    • 在類加載的解析階段,會將一部分符號引用轉化爲直接引用,這種解析成立的前提是: 方法在程序真正運行之前就有一個可確定的調用版本,並且這個方法的調用版本在運行期是不可改變的; 也就是說,調用目標在程序代碼寫好,編譯器進行編譯時就必須確定下來的這類調用稱爲解析;
    • java提供5種方法調用字節碼指令:
      • invokestatic: 調用靜態方法;
      • invokespecial: 調用實例構造器<init>方法,私有方法,父類方法;
      • invokevirtual: 調用所有的虛方法(除final方法);
      • invokeinterface: 調用接口方法,會在運行時在確定一個實現此接口的對象;
      • invokedynamic: 先在運行時動態解析出調用點限定符所引用的方法,然後在執行該方法,此條指令時由用戶所設定的引導方法決定的,其他是固化在jvm內部;
      • 非虛方法有5類:
        • invokestatic,invokespecial 指令調用的方法,都可以在解析階段中確定唯一的調用版本,符合這個條件的有靜態方法,私有方法,實例構造器,父類方法4類,在類加載的時候就會把符號引用解析爲直接引用,這類方法稱爲非虛方法;
        • 非虛方法還有一類是被final修飾的方法,即使final方法是使用invokevirtual指令調用;其他的爲虛方法;
      • 解析調用一定是個靜態的過程,在編譯期間就完全確定,類加載的解析階段就會把符號引用轉爲直接引用,而分派可能是靜態的也可能是動態的;
  • 分派 Dispatch

    • 多態性(重載&重寫)
    • 靜態分派 [編譯階段編譯器的選擇過程]
      • 靜態類型&實際類型:
        • Human man = new Man() Human稱爲變量的靜態類型(Static Type),或者叫外觀類型(Apparent Type),後面的Man稱爲變量的實際類型(Actual Type);
        • 靜態類型的變化僅僅在使用時發生,變量本身的靜態類型不會被改變,並且最終的靜態類型是在編譯期間可知的; 而實際類型變化的結果在運行期纔可確定,編譯器在編譯時並不知道一個對象的實際類型是什麼;
      • 編譯器在重載選擇方法時,通過參數的靜態類型而不是實際類型作爲判定依據的;
      • 所有依賴靜態類型來定位方法執行版本的分派動作稱爲靜態分派,典型應用是方法重載(Overload); 靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是jvm執行的;
    • 動態分派 [運行階段jvm的選擇過程]
      • 重寫(Override) ,可通過字節碼指令invokevirtual執行多態方法,執行的第一步就是在運行期間確認接收者(將要執行方法的所有者)的實際類型,把常量池中的類方法符號引用解析到不同的直接引用上,這個過程就是java方法重寫的本質;
    • 單分派和多分派
      • 方法的接收者與方法的參數統稱爲方法宗量;根據分派基於多少種宗量,可劃分爲單分派和多分派兩種;
        • 單分派: 根據一個宗量對目標方法進行選擇;
        • 多分派: 根據多於一個宗量對目標方法進行選擇;
      • java是一門靜態多分派(重載時根據調用者和參數),動態單分派(重寫時根據調用者)的語言;
  • 動態類型語言支持 Dynamically Type Language

    • 動態類型語言 類型檢查的主體過程是在運行期間而不是編譯期(如js,python,kotlin);而編譯期間進行類型檢查過程的語言(java,C++)就是靜態類型語言;動態語言變量無類型而變量值纔有類型;
    • java.lang.invoke包支持動態編程:
      • MethodHandle;
      • MethodType :方法類型,包含方法的返回值(methodType()的第一個參數)和具體參數(methodType()第二個及以後的參數)
      • MethodHandles.lookup() : 在指定類中查找符合給定的方法名稱,方法類型,並且符合調用權限的方法句柄;
      • 與反射的區別是反射是重量級的;
    • invokedynamic 字節碼指令 爲了解決原有4條’invoke*'指令方法分派規則固化在jvm之中的問題,把如何查找目標方法的決定權從jvm中轉嫁到具體用戶代碼中,讓用戶有更高的自由度;invokedynamic的分派邏輯不是由jvm決定的,而是由程序員決定的;
    • 使用動態語言調用父類的父類的方法: 輸出爲: i am grandfather;
      在這裏插入圖片描述

基於棧的字節碼解釋執行引擎

  • 解釋執行,半獨立編譯;
  • 基於棧的指令集與基於寄存器的指令集
    • java編譯器輸出的指令流,基本上是一種基於棧的指令集架構(Instruction Set Architecture),指令流中的指令大部分都是零地址指令,依賴操作數棧進行工作;而x86的二地址指令集就是基於寄存器的指令集;
    • 區別 (1+1):
      • 基於棧 iconst_1 iconst_1 iadd istore_0
        • 可移植,代碼緊湊,編譯器實現簡單,但相同功能指令數量更多,更頻繁的內存訪問,執行速度慢;
      • 基於寄存器 mov eax, 1 add eax, 1
        • 性能好,實現簡單

類加載及執行子系統的案例與實戰

程序進行操作的主要是字節碼生成類加載器這兩部分的功能;

tomcat: 正統的類加載器架構

Common類加載器能加載的類都可以被Catalina和Shared使用,雙方可以相互隔離;各個WebApp類加載器實例之間相互隔離;Jsp類加載器就是爲了被丟棄實現HotSwap功能;

在這裏插入圖片描述

OSGi: 靈活的類加載器架構

Open Service Gateway Initiative 基於java語言的動態模塊化規範; 運行時才能確定的網狀結構;Eclipse IDE 就是OSGi的應用案例;

字節碼生成技術與動態代理的實現

javac,javassist,CGLib,ASM ,Proxy.newProxyInstance;動態代理的優勢實現了在原始類和接口還未知的時候,就確定了代理類的代理行爲,當代理類與原始類脫離直接聯繫後,就可以很靈活的重用於不同的應用場景中;


程序編譯與代碼優化

  • 編譯器分類:

    • 前端編譯器: 將*.java轉變爲 *.class文件的過程; sun的javac;
    • JIT編譯器(Just in Time 後端運行期編譯器): 把字節碼轉變爲機器碼的過程;hotspotVm的c1,c2編譯器;
    • AOT編譯器 (Ahead of Time 靜態提前編譯器): 直接把*.java文件編譯成本地機器碼的過程; GCJ;
  • java語法糖:

    • 泛型與類型擦除 參數化類型的應用,也就是說所操作的數據類型被指定爲一個參數;這種參數類型可以用在類,解口,方法的創建中,分別被稱爲泛型類,泛型接口,泛型方法;
      • java中的泛型只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型(Raw Type 裸類型),並且在相應的地方插入了強制轉型代碼; 因此在運行期間的java來說,ArrayList和ArrayList就是同一個類,所以是一個語法糖;java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型;
      • Signature,LocalVariableTypeTable等新屬性用於解決伴隨泛型而來的參數類型的識別問題;
    • 自動裝箱,拆箱,遍歷循環;
      • 自動拆箱的陷阱
        在這裏插入圖片描述
        • Integer 內有提供的數緩存只有-128 ~ 127,超過這個範圍重新創建新的空間存儲這個數;所以第一個第二個爲true,false;
        • == 判斷兩個類型的地址,在不遇到算術運算的情況下不會自動拆箱;所以第三個第四個都是返回true;
        • equals方法不處理數據轉型的關係;所以第五個第六個返回true,false;
  • 註解處理器

    • 實現的註解處理器需要繼承抽象類javax.annotation.processing.AbstractProcessor ,

      • 覆蓋抽象方法 public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 它是javac編譯器在執行註解處理器代碼時要調用的過程
        • 第一個參數 獲取到此註解器所要處理的註解集合;
        • 第二個參數 訪問到當前這個Round中的語法樹節點,每個語法樹節點再這裏表示爲一個Element;
        • JDK 1.6 javax.lang.model 定義了16類Element
          • 包 package;
          • 枚舉 enum;
          • 類 class;
          • 註解 annotation_type;
          • 接口 interface;
          • 枚舉值 enum_constant;
          • 字段 field;
          • 參數 parameter;
          • 本地變量 local_variable;
          • 異常 exception_parameter;
          • 方法 method;
          • 構造函數 constructor;
          • 靜態語句塊 static_init
          • 實例語句塊 instance_init
          • 參數化類型 type_parameter;
          • 其他語法樹節點 other;
      • 常用的實例變量 protected ProcessingEnvironment processingEnv; 初始化的時候創建,代表註解處理器框架提供的一個上下文環境,要創建新的代碼,向編譯器輸出信息,獲取其他工具類等都需要這個實例變量;
    • 註解處理器除了process()方法及其參數之外,還有兩個可以配合使用的Annotations

      • @SupportedAnnotationTypes 註解處理器對哪些註解感興趣,可以使用星號*通配對所有註解都感興趣;
      • @SupportSourceVersion 指出這個註解處理器可以處理哪些版本的java代碼;
    • 每一個註解處理器在運行時都是單例的,如果不需要改變或生成語法樹的內容,process()方法就可以返回一個值爲false的布爾值,通知編譯器這個round中的代碼未發生變化,無需構造新的javaCompiler實例;

  • 晚期(運行時)優化

  • java最初是通過解釋器進行解釋執行的,當jvm發現某個方法或者代碼塊執行頻繁,就會把這些代碼認定爲’熱點代碼(HotSpot)’,提高熱點代碼的效率,運行時,jvm將會把這些代碼編譯成與本地平臺相關的機器碼,並進行層次的優化,完成這個任務的編譯器爲即時編譯器(Jit)

  • java hotspot jvm是解釋器與編譯器並存的架構, 當程序需要快速啓動和執行時,解釋器可以首先發揮作用,省去編譯的時間,立即執行;當程序運行後,隨着時間得推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之後,可以獲取較高的執行效率;

  • hotspot jvm內置兩個即時編譯器,分別稱爲 Client Compiler,Server Compiler 簡稱 C1編譯器,C2編譯器; 分別分爲 混合模式,編譯模式,解釋模式;

  • 編譯優化技術 (太複雜,選幾點記錄一下)

    • 方法內聯 (Method Inlining) 去除方法調用的成本(如建立棧幀);爲其他優化建立良好的基礎;非虛方法可以直接內聯;
    • 冗餘訪問消除 ,公共子表達式消除;
    • 複寫傳播
    • 無用代碼消除;
    • 逃逸分析(如果一個對象不會逃逸到方法或線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個對象,可能進行一些高效的優化(是否說明少用形式參數?))

這塊挺複雜的,只是粗淺的看了下,有興趣的可以看原書;


發佈了46 篇原創文章 · 獲贊 3 · 訪問量 5092
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章