讀書筆記-深刻理解Java虛擬機

概述

深刻理解Java虛擬機:JVM高級特性與最佳實踐(第三版)周志明 讀書筆記選取了書中部份內容,Java虛擬機更多的是一種規範,具體的Java虛擬機實現是有不少的。做者提到本文多數是以Hotspot虛擬機做爲講解。html

第二部分 自動內存管理

第2章 Java內存區域與內存溢出異常

運行時數據區:計數器,Java虛擬機棧(局部變量表,操做數棧)方法區(類型信息,運行時常量池
等), Java堆,直接內存。可能產生內存溢出場景。虛擬機對象建立,內存佈局與訪問定位,前端

第3章 垃圾收集與內存分配策略

強引用,軟引用,弱引用,虛引用,分代收集,併發可達性分析,標記-清除,標記-複製,標記-整理,java

第三部分 虛擬機執行子系統

第6章 類文件結構

Class類文件結構,字節碼指令集程序員

第7章 虛擬機類加載機制

加載,連接,初始化,類加載器,雙親委派模型算法

第8章 虛擬機字節碼執行引擎

運行時棧結構,方法解析分派,動態類型語言支持,基於棧的字節碼執行數據庫

第四部分 程序編譯與代碼優化

第10章 前端編譯與優化

從Java文件到字節碼階段:Java註解處理器,泛型,裝拆箱,插入式註解處理器編程

第11章 後端編譯與優化

從字節碼到機器碼:AOT,JIT , Android Dalvik,Android ART後端

第2章 Java內存區域與內存溢出異常

2.1 概述

對於C/C++程序員來講,擔負每個對象生命從開始到終結的維護責任。而對於Java程序員來講,Java幫助程序員自動管理內存,不須要寫顯式的代碼去釋放內存。但虛擬機不是萬能的,一旦出現內存泄漏和溢出問題,若是不瞭解虛擬機怎樣使用內存,將難以排查錯誤和修正問題。 本章從概念上介紹Java虛擬機內存的各個區域,及其可能產生的問題。數組

2.2 運行時數據區域

Java虛擬機在執行Java程序時,會將它管理的內存分紅功能不一樣的運行時數據區域。這些區域有着不一樣的用戶,不一樣的建立和銷燬時間。 有的區域隨着虛擬機進程生命週期,有的區域則依賴用戶線程的啓動和結束而創建和銷燬。根據《Java虛擬機規範》規定,Java虛擬機管理的內存包括如下運行時數據區。緩存

截屏2020-08-04下午8.07.42.png Java虛擬機基於棧的方式去執行程序。每個線程都會有相應的虛擬機棧,而虛擬機棧的棧幀對應於Java方法。

2.2.1 程序計數器

程序計數器(Progrom Counter Register),記錄當前線程執行的字節碼行號指示器。一般來講,字節碼解釋器工做時就是經過改變計數器的值來選取下一條須要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等都依賴計數器完成。每一個線程都有獨立的計數器,互不影響。計數器分配在線程私有的內存空間中。此區域不會出現OOM的狀況。
複製代碼

2.2.2 Java虛擬機棧

Java虛擬機棧也是線程私有的,它的生命週期與線程相同,描述的是Java方法執行的線程內存模型,即每一個方法被執行時,都會同步建立一個棧幀(Stack Frame) 存儲局部變量表,操做數棧,動態鏈接等。方法的調用與執行完畢,對應一個棧幀的入棧和出棧。
複製代碼

截屏2020-08-05下午9.03.19.png 在Java源碼編譯成字節碼時,一個棧幀須要多大的局部變量表,須要多深的操做數棧就已經被分析計算出來,即編譯事後就已經可以須要多大內存,內存取決於源碼和具體的虛擬機棧內存佈局形式。 在《Java虛擬機規範》中提到:若是線程請求的棧深度大於虛擬機所容許的深度,將會拋出StackOverflowError異常;若是虛擬機棧容量能夠動態擴展,無限擴展,內存不足會拋出OutOfMemoryError異常。 Hotspot虛擬機棧容量不可動態擴展,但若是線程申請棧空間失敗,仍然會OOM。

2.2.3 本地方法棧

相較於Java虛擬機棧執行Java方法,本地方法棧執行Native方法。做用是類似的,也會有一樣的異常問題。在HotSpot虛擬機中,本地方法棧與Java虛擬機棧合二爲一。
複製代碼

2.2.4 Java堆

Java堆是全部線程共享的內存區域,在虛擬機啓動時建立。用來存放對象實例。數組也是一種對象實例。
 Java堆是垃圾收集器管理的內存區域。基於分代收集理論設計,多數虛擬機的堆內存能夠分爲新生代、老年代、永久代,Eden,Survivor等。隨之垃圾收集器技術的發展,也出現了不採用分代設計的新垃圾收集器,那就不存在上述所謂的代劃分。![截屏2020-08-05下午3.27.29.png](https://cdn.nlark.com/yuque/0/2020/png/1305846/1596612457012-03fbf28c-2cf4-43be-a24f-7144ada446a4.png#align=left&display=inline&height=155&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2020-08-05%E4%B8%8B%E5%8D%883.27.29.png&originHeight=166&originWidth=729&size=15139&status=done&style=none&width=680)
複製代碼

Java堆中,能夠劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer ,TLAB)。咱們說TLAB是現成獨享的,但只是分配是獨享的,讀操做和垃圾回收等動做上是線程共享的。TLAB一般是在Eden區分配,由於Eden區自己不大,TLAB實際內存也很是小,默認佔Eden空間的1%,因此必然存在一些大對象沒法在TLAB直接分配。 截屏2020-08-05下午9.02.34.png 不管怎麼劃分,都不會改變堆存放對象實例的做用。各類劃分是爲了更好的分配和回收內存。 《Java虛擬機規範》規定,邏輯上連續的內存空間,在物理上能夠不連續。但多數虛擬機實現出於實現簡單和存儲高效,也會要求連續的物理內存空間。 主流Java虛擬機的堆內存空間都是可擴展的,但有上限值。當對象實例沒法被分配內存,且堆達到上限。Java虛擬機便會拋出OutOfMemoryError異常。

2.2.5/2.2.6 方法區(包含運行時常量池)

方法區(Method Area),運行時常量池(Runtime Constant Pool)
複製代碼

方法區,線程共享,存儲已被虛擬機加載的類型信息,常量,靜態變量,即時編譯器編譯後的代碼緩存等數據。 JDK8,再也不使用永久代(Permanent Generation Space)實現方法區,而是在本地內存中實現的元空間(Metaspace)來代替。而字符串常量移到Java堆。這部分的內存回收目標主要針對常量池的回收和對類型的卸載。 截屏2020-08-05下午3.22.27.png 運行時常量池,存放常量池表(Constant Pool Table),即Class文件編譯期生成的各類字面量與符號引用。 根據《Java虛擬機規範》規定,若是方法區沒法知足新的內存分配,將會拋出OutOfMemoryError異常。

2.2.7 直接內存

直接內存(Direct Memory). NIO(New input/output)是 JDK1.4新加入的類,引入了一種基於通道(channel)和緩衝區(buffer)的I/O方式,它可使用Native函數直接分配堆外內存,而後經過堆上DirectByteBuffer對象對這塊內存進行引用和操做。直接內存的大小不受JVM的限制,但一樣可能會OutOfMemoryError異常。 截屏2020-08-05下午9.01.53.png

2.3 HotSpot虛擬機對象探祕

以HotSpot爲例,講述在Java堆中對象的建立、結構和訪問。

2.3.1 對象建立截屏2020-08-05下午9.00.56.png

分配堆內存 根據內存是否規整,分爲兩種:指針碰撞(Bump The Pointer)和空閒列表(Free List).前者內存規整。 截屏2020-08-05下午8.59.51.png 解決併發狀況下的線程安全問題的兩種方式

  • 對分配內存的動做進行同步處理,實際採用CAS(CompareAndSwap)配上失敗重試保證操做的原子性
  • 本地線程緩衝區(Thread Local Allocation Buffer,TLAB,線程預分配私有寫內存區域。

2.3.2對象結構

  • 對象頭(Header)
    • 對象自身運行時數據:哈希碼、GC分代年齡、鎖狀態標誌、線程持有鎖、偏向線程ID,偏向時間戳等。這部分的數據在32位虛擬機和64位虛擬機上的大小分別也是32bit和64bit,稱之爲Mark Word。
    • 類型指針,指向其類型元數據。
  • 實例數據(Instance Data)
    • 對象真正存儲的有效信息
  • 對齊填充(Padding)
    • 保證對象內存大小爲8字節的整數倍,對齊填充補全。

2.3.3對象的訪問定位

在《Java虛擬機規範》中規定,棧上的reference類型數據只是一個指向對象的引用。實際經過引用訪問對象有兩種方式:

  • 句柄訪問,reference存儲的是句柄地址,對象被移動,改變句柄中的指針就好,reference自己不會被修改。
  • 直接指針訪問,少一次指針定位的開銷,對象訪問在Java中很是頻繁。HotSpot採用直接指針訪問。

截屏2020-08-06上午11.40.49.png截屏2020-08-06下午1.14.42.png

2.4 實戰:OutOfMemoryError異常

模擬Java堆、虛擬機棧、本地方法棧、方法區、運行時常量池,本地直接內存的溢出。

2.4.1 Java堆溢出

/***Intellij IDEA 配置 Run Configgurations * VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * 限制Java堆的大小爲20MB,不可擴展(將堆的最小值-Xms參數與最大值-Xmx參數設置爲同樣) */ 
public class HeapOOM {
    static class OOMObject{}
    public static void main(String[] args) {
        List<OOMObject> list=new ArrayList<OOMObject>();
        while (true){
            list.add(new OOMObject());
        }
    }
}
複製代碼

運行結果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid58651.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [27770272 bytes in 0.159 secs]
複製代碼

2.4.2 虛擬機棧和本地方法棧溢出

HotSpot不區分虛擬機棧和本地方法棧,須要設置 -Xss。 -Xoss(設置本地方法棧)沒有效果。
複製代碼

兩種異常:

  • 棧內存不可動態擴展,請求棧深度大於容許最大深度,則 StackOverflowError。
  • 棧內存可動態擴展,當內存不足,沒法申請,則 OutOfMemoryError。

HotSpot棧內存不容許動態擴展,咱們使用-Xss參數減小棧內存容量。

// VM Args:-Xss160k
public class JavaVMStackOF {
    private int stackLength = 1;
    
    public void stackLeak() {
        stackLength++;
        System.out.println("stack length:" + stackLength);
        stackLeak();
    }
    public static void main(String[] args) throws Exception {
        JavaVMStackOF oom = new JavaVMStackOF();
        try {
            oom.stackLeak();
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }
}
複製代碼

運行結果

...
stack length:754
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:9)
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:10)
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:10)
...
複製代碼

2.4.3 方法區和運行時常量池溢出

截屏2020-08-06下午4.11.39.png

在JDK6及以前版本中運行String::intern()

在JDK6及之前的HotSpot中,常量池分配在永久代中,經過 -XX:PermSize 和 -XX:MaxPermSize限制永久代的大小。String::intern() 能夠將一個字符串對象添加到常量池,若是常量池中不包此字符串對象。 在JDK6的HotSpot中

/*** VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M */ 
public class RuntimeConstantPoolOOM { 
    public static void main(String[] args) {
        // 使用Set保持着常量池引用,避免Full GC回收常量池行爲 
        Set<String> set = new HashSet<String>(); 
        // 在short範圍內足以讓6MB的PermSize產生OOM了 
        short i = 0; 
        while (true) { 
            set.add(String.valueOf(i++).intern()); 
        } 
    } 
}
複製代碼

運行結果,也驗證了字符串常量在永久代中

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 
at java.lang.String.intern(Native Method) 
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18
複製代碼

在JDK7及之後版本中運行String::intern()

因爲字符串常量轉移到了Java堆中, 因此在JDK7中設置 -XX:MaxPermSize, 或者在JDK8中設置 --XX:MaxMeta-spaceSize都不會出現JDK6中的溢出問題。但咱們能夠限制最大堆內存空間-Xmx6m,從而產生OOM。

/*** VM Args:-Xmx6m */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }
}
複製代碼

運行結果,也驗證了字符串常量在堆中

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.HashMap.put(HashMap.java:611)
	at java.util.HashSet.add(HashSet.java:219)
	at oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:27)
複製代碼

2.4.4 本機直接內存溢出

直接內存(Direct Memory)的容量大小可經過 -XX:MaxDirectMemorySize指定。

/*** VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M */
public class DirectMemoryOOM {

    public static void main(String[] args) throws Exception {
        int count=1;
        Field unsafeFiled=Unsafe.class.getDeclaredFields()[0];
        unsafeFiled.setAccessible(true);
        Unsafe unsafe= (Unsafe) unsafeFiled.get(null);
        while (true){
            unsafe.allocateMemory(1024*1024*1024);
            System.out.println(count++);
        }
    }
}
複製代碼

運行結果

Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at oom.DirectMemoryOOM.main(DirectMemoryOOM.java:17)
複製代碼

2.5 小結

到目前爲止,明白了虛擬機中內存的劃分,以及出現內存溢出的場景。下一章將詳細講解Java垃圾收集機制如何避免內存溢出。

第3章 垃圾收集與內存分配策略

3.1概述

垃圾收集(Garbage Collection,GC),1960年麻省理工Lisp語言,使用動態內存分配和垃圾收集技術。 當Lisp胚胎時期,其做者John McCarthy就思考過GC須要完成的三件事情:

  • 哪些內存須要回收
  • 何時回收
  • 如何回收

Java虛擬機內存運行時區間中,程序計數器、虛擬機棧、本地方法棧隨線程而生,隨線程而滅。在編譯器其大小基本肯定。而GC關注的是線程共享的區域 Java堆和方法區。

3.2對象已死?

GC回收堆以前,如何斷定哪些對象須要被回收,或者說這些對象已死?截屏2020-08-12下午8.27.10.png

3.2.1 引用計數法(Reference Counting)

3.2.2可達性分析算法(Reachability Analysis)

可固定做爲GC Roots的對象包括:

  • 虛擬機棧幀中的本地變量表引用的對象
  • 方法區中類靜態屬性引用對象
  • 方法區中常量引用對象
  • 本地方法棧中JNI引用對象
  • 虛擬機內部的引用,如Class對象,常駐異常對象(NullPointException),類加載器等等
  • 被同步鎖持有的對象
  • 反映Java虛擬機內部狀況的JMXBean,JVMTI中註冊的回調,本地代碼緩存等。

除固定外,還有臨時性的其餘對象等。

3.2.3再談引用

Java將引用分爲四種:

  • 強引用(Strongly Reference),引用賦值(即Object obj=new Object),永不回收
  • 軟引用(Soft reference),存活到即將發生內存溢出異常前的二次回收
  • 弱引用(Weak Reference),存活到下一次垃圾收集
  • 虛引用(Phantom Reference),沒法經過虛引用獲取對象實例,只是感知對象被回收

3.2.4 生存仍是死亡

要宣告一個對象死亡,至少要經歷兩次不可達標記過程。重載對象的finalize()方法,能夠再第一次標記後執行,從新掛上引用鏈避免被回收,但finalize()只會被執行一次。

3.2.5回收方法區

3.3垃圾收集算法

  • 引用計數式垃圾收集(Reference Counting GC)
  • 追蹤式垃圾收集(Tracing GC)

這裏咱們討論的是追蹤式垃圾收集。

3.3.1 分代收集理論

分代收集(Generational Collection)的理論假說基礎:

  • 弱分代假說(Weak Generational Hypothesis)

絕大多數對象都是朝生夕滅的

  • 強分代假說(Strong Generational Hypothesis):

熬過越屢次垃圾收集過程的對象越難以消亡

  • 跨代引用假說(Intergenerational Reference Hypothesis):

跨代引用相對同代引用來講,僅佔極少數

基於弱分代/強分代假說,通常Java虛擬機至少會把Java堆劃分爲

  • 新生代(Young Generation)
  • 老年代(Old Generation)

每次垃圾收集,新生代大量死去的對象會被回收,存活的對象將會逐步晉升到老年代中。大對象直接進入老年代。在新生代中創建一個全局的數據結構(記憶集,Remebered Set),把老年代分紅多個小塊,標識出某些塊存在跨代引用。 針對不一樣級別的分代的收集,咱們定義一下名詞:

  • 部分收集(Partial GC)
    • 新生代收集(Minor GC/Young GC)
    • 老年代收集(Major GC/Old GC)
    • 混合收集(Mixed GC),收集新生代和部分老年代
  • 整堆收集(Full GC)

3.3.2 標記-清除算法

標記-清除(Mark-Swap) 1960 Lisp John McCarthy,基礎性算法。 缺點:

  • 執行效率不穩定,隨對象數量增加而下降
  • 內存碎片化問題

截屏2020-08-13下午3.26.28.png

3.3.3 標記-複製算法

1969 Fenichel "半區複製" SemisSpace Copying 解決內存碎片化和執行效率問題,缺點明顯:浪費一半內存。

截屏2020-08-13下午4.36.59.png IBM研究發現98%的新生代對象熬不過第一輪收集,所以不用1:1分配內存 1989年 Andrew Appel ,提出更優化的半區複製分代策略。 能夠看到始終有一個 Survivor(10%新生代內存)做爲保留,用來存放回收後存活對象,若果Survivor空間不夠,則須要老年代進行分配擔保(Handle Promotion) 截屏2020-08-13下午5.17.17.png 標記-複製算法適用於新生代,即大量對象會被回收,須要複製的對象不多。老年代對象存活率高,就不適用了。

3.3.4 標記-整理算法

標記-整理(Mark-Compact) 1974年 Edward Lueders。 移動式回收算法,標記-清除是非移動式的。 移動對象則回收時複雜,不移動對象則分配內存時複雜。截屏2020-08-13下午5.47.06.png

3.4 HotSpot垃圾收集算法細節

3.4.1 根節點枚舉

全部垃圾回收算法在根節點枚舉時,都須要暫停用戶線程,即 Stop The World! HotSpot採用準確式(Exact)垃圾回收,使用稱爲OopMap的數據結構,記錄棧上本地變量到堆上對象的引用關係。 從而減小根節點枚舉耗費的大量時間。 找出棧上的指針/引用 介紹了保守式,半自動式,準確式垃圾回收,同時也引出了OopMap。

3.4.2 安全點

根節點枚舉須要暫停線程,總不能在每條指令後都去中斷線程,因此有些固定的指令位置,做爲中斷的點,稱之爲 safe point。採用主動式中斷,即達到安全點,檢查是否要執行中斷線程。 安全點的位置通常爲:

  • 循環的末尾
  • 方法返回前/調用方法的call指令後
  • 可能拋出異常的位置

3.4.3 安全區域

safe region。安全區域指在某個代碼片斷中,引用關係不會發生變化,在這個區域內能夠安全的開始垃圾收集。截屏2020-08-17下午11.26.17.png

3.4.4 記憶集與卡表

RememberSet,記錄非收集區到收集區的引用,避免把整個非收集區加入到GC Root掃描。好比說收集新生代對象時,避免整個老年代加入GCRoot掃描。
複製代碼

從精度上來看,記憶集能夠分爲

  • 字長精度
  • 對象精度
  • 卡精度,每一個記錄精確到一塊內存區域,記錄該區域內是否含有跨代指針。

卡表,即爲常見的卡精度的記憶集。 卡表簡單來講,能夠只是一個字節數組(Card Table)。每一個數組元素都對應一個卡頁(Card Page),卡頁是某塊特定大小的內存塊,通常來講大小爲2的N次冪字節數,HotSpot中爲512字節。只要卡頁中有對象存在跨代引用,則對應卡表元素標記爲1,即元素變髒(Dirty)截屏2020-08-18上午11.22.24.png

3.4.5 寫屏障與AOP

什麼時候去記錄RememberSet? 寫屏障(Write Barrier),虛擬機層面對"引用類型字段賦值"動做的AOP切面,虛擬機爲賦值操做生成相應指令。 環形(Around)通知,提供寫前屏障(Pre-Write Barrier)和寫後屏障(Post-Write Barrier)。

假設處理器的緩存行大小爲64字節,因爲一個卡表元素佔1個字節,64個卡表元素將共享同一個緩 存行。卡表在高併發下的僞共享(False Sharing)問題, 寫髒前,先判斷是否已髒。 在JDK 7以後,HotSpot虛擬機增長了一個新的參數-XX:+UseCondCardMark,用來決定是否開啓卡表更新的條件判斷

3.4.6 併發的可達性分析與三色標記

前面咱們經過OopMap、安全區域、RememberSet等手段,提高了根節點枚舉的速度。根節點枚舉帶來的停頓已經至關短暫和固定了,而從GC Roots繼續往下遍歷對象的停頓時間與堆容量成正比。 可達性分析(標記)算法要求全過程在一個一致性的快照中分析,勢必要凍結所有用戶線程,且凍結的時間徹底不可控。在堆容量過大狀況下,凍結時間是沒法接受的。所以,可達性分析過程,若是能與用戶線程併發執行,是最好不過了。 咱們先來看併發可達性分析過程 即三色標記 截屏2020-08-18下午3.54.48.png 併發可達性分析又會引發兩類問題:

  • 1.該回收的沒有被標記(浮動垃圾,Floating Garbage),這個可接受,大不了下次回收時,再回收。
  • 2.不應回收的被標記(對象消失),這個不可接受,由於用戶線程須要的對象沒了。

當且僅當同時知足下面兩個條件,會產生對象消失問題,即本來應當爲黑色的對象被誤標爲白色(Wilson,1994年證實):

  • 賦值器插入了一條黑色到白色的引用。
  • 同時賦值器刪除了所有灰色到該白色的直接或間接引用。

截屏2020-08-18下午3.55.45.png

  • 增量更新,記錄新增的引用,併發掃描結束後,從新以黑色爲根掃描,即黑色變爲灰色
  • 原始快照,記錄刪除的引用,併發掃描結束後,從新以灰色爲根掃描。

3.5 經典垃圾收集器

3.5.0 概述截屏2020-08-18下午5.49.49.png

在介紹以前咱們先明確幾個概念:

  • 並行,能夠有多個垃圾收集線程同時運行。 串行則同時只能有一個垃圾收集線程運行。
  • 併發,垃圾收集線程能夠與用戶線程同時運行。
  • 高吞吐量,垃圾收集時間/(用戶線程運行時間+垃圾收集時間)
  • 低延遲,快速響應。能夠容忍總的收集時間增長,下降平均每次收集時間。

3.5.1 Serial收集器

新生代,無並行,無併發,標記複製,簡單高效,額外內存消耗(Memory Footprint)最小,適用於單核/少核,JDK1.3.1以前。截屏2020-08-19下午1.17.22.png

3.5.2 ParNew收集器

新生代,並行,Serial的多線程版本,標記複製, [JDK1.3 - JDK8)

3.5.3 Parallel Scavenge收集器

新生代,並行 ,無併發,JDK1.4,標記複製,注重吞吐量截屏2020-08-19下午1.17.40.png

3.5.4 Serial Old 收集器

老年代,無並行,無併發,Serial老年代版本,標記整理

3.5.5 Parallel Old 收集器

老年代,並行,無併發,Parallel Scavenge 老年代版本,標記整理,注重吞吐量

3.5.6 CMS收集器

Concurent Mark Sweep, 老年代,並行,併發, [JDK5 - JDK8],標記清除,注重低延遲, 併發標記使用增量更新。四個步驟:

    1. 初始標記(CMS initial mark),僅標記GC Roots直接關聯對象,短STW
  • 2)併發標記(CMS concurrent mark),遍歷整個對象圖,耗時長,能夠與用戶線程併發
  • 3)從新標記(CMS remark),增量更新,避免對象消失問題,短STW
  • 4)併發清除(CMS concurrent sweep),不須要移動對象,能夠與用戶線程併發

截屏2020-08-18下午10.58.20.png CMS的三個明顯缺點:

  • 回收線程佔用處理器資源,CMS默認啓動的回收線程數 (處理器核心數量+3)/4. 併發階段,應用程序會變慢,吞吐量下降
  • 浮動垃圾 Floating Garbage, 併發過程失敗,須要啓用Serial Old,作一次老年代收集。
  • 內存碎片,標記清除算法帶來的問題,進行若干次標記清除後,會執行一次碎片整理。由於整理須要移動對象,沒法併發。

3.5.7 G1收集器

Garbage First,JDK7完善, JDK9開始成爲默認垃圾收集器。 新生代,老年代。Region分區,局部標記-複製,總體標記-整理,注重低延遲,併發標記使用原始快照。 大對象(內存超過Region內存的一半)直接進入 Humongous Region區域。Region是回收的最小單元。每個Region均可以根據須要扮演Eden空間,Survivor空間或者老年代空間。可預測時間停頓模型。 四個步驟:

    1. 初始標記( Initial Marking),僅標記GC Roots直接關聯對象,短STW
  • 2)併發標記(Concurrent Marking),遍歷整個對象圖,耗時長,能夠與用戶線程併發
  • 3)最終標記(Final Marking),併發標記使用原始快照,避免對象消失問題,短STW
  • 4)篩選回收(Live Data Counting and Evacuation),對各個Region回收的價值和成本排序,根據指望停頓時間,組合任意多個Region回收。待回收的Region存活對象複製到空Region中,回收舊Region,涉及對象移動,須要STW

截屏2020-08-18下午10.59.27.png G1是垃圾收集器技術發展歷史上的里程碑式的結果,開創了面向局部手機的設計思路和基於Region的內存佈局形式。從G1開始,垃圾收集器的設計導向變爲追求應付內存分配速率(Allocation Tate),而不追求一次把整個Java堆清理乾淨。G1的更多介紹

3.6 低延遲垃圾收集器

3.6.1 Shenandoah收集器

略,實在不會

3.6.2 ZGC收集器

略,實在不會

第6章 類文件結構

 代碼編譯的結果從本地機器碼變爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。

6.1 概述

程序語言 -->  字節碼 -->  二進制本地機器碼

6.2 無關性的基石

無關性的基石  --  字節碼(Byte Code) 平臺無關性,語言無關性截屏2020-08-19下午4.11.19.png

6.3 Class類文件的結構

Class文件以8個字節爲單位的二進制流,各數據項嚴格按照順序緊湊排列在文件中,中間沒有任何分隔符。 Class文件結構中只有兩種數據類型:

  • 無符號數,基本數據類型,u一、u二、u四、u8分別表明1個字節 、2個字節、4個字節 、8個字節的無符號數,用來描述數字,引用,數量值或者UTF-8編碼字符串。
  • 表,多個無符號數組成的符合數據類型,命名通常以「_info」結尾。
類型 名稱 數量 解釋
u4 magic 1 4字節魔數 0xCAFEBABE
u2 minor_version 1 次要版本號
u2 major_version 1 主要版本號
u2 constant_pool_count 1 常量池計數值
cp_info constant_pool constant_pool_count-1 常量池
u2 access_flags 1 訪問標誌
u2 this_class 1 類索引
u2 super_class 1 父類索引
u2 interfaces_count 1
u2 interfaces interfaces_count 接口索引集合
u2 fields_count 1
field_info fields fields_count 字段表集合,類變量
u2 methods_count 1
method_info methods methods_count 方法表集合
u2 attributes_count 1
attribute_info attributes attributes_count 屬性表集合

先來一段代碼 TestClass.Java

package clazz;
public class TestClass {
    public static void main(String[] args) { }
    private int m;
    public int inc(){return m+1;}
}
複製代碼

經過編譯獲得二進制字節碼文件 TestClass.class 截屏2020-08-19下午5.20.20.png

javap -v TestClass.class獲得字節碼中包含的類信息。咱們如今要作的就是模擬javap這個解析的過程。

Last modified 2020-7-29; size 483 bytes
  MD5 checksum ad62060802ee27c385e20042d24e8b38
  Compiled from "TestClass.java"
public class clazz.TestClass minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref          #4.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#23         // clazz/TestClass.m:I
   #3 = Class              #24            // clazz/TestClass
   #4 = Class              #25            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lclazz/TestClass;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               inc
  #19 = Utf8               ()I
  #20 = Utf8               SourceFile
  #21 = Utf8               TestClass.java
  #22 = NameAndType        #7:#8          // "<init>":()V
  #23 = NameAndType        #5:#6          // m:I
  #24 = Utf8               clazz/TestClass
  #25 = Utf8               java/lang/Object
{
  public clazz.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lclazz/TestClass;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lclazz/TestClass;
}
SourceFile: "TestClass.java"
複製代碼

6.3.1 魔數與Class文件版本

8C05AAA1-97AC-41BA-A22B-940C08F3C071.png 魔數(Magic Number) CAFEBABE ,表示這是一個Class類型文件,4字節。 minor version: 0(0x0000),2字節。 major version: 51 (0x0033),2字節。

6.3.2 常量池

接在魔數和版本後面的是常量池。 常量池中存放兩大類型常量:

  • 字面量(Literal):文本字符串,被申明爲final的常量等
  • 符號引用(Symbolic References)
    • 類和接口的全限定名,
      • #13,Lclazz/TestClass
    • 字段的名稱和描述符 ,
      • #5 = Utf8               m
      • #6 = Utf8               I
    • 方法的名稱和描述符,
      • #14 = Utf8             main,
      • #15 = Utf8             ([Ljava/lang/String;)V
    • 方法句柄和方法類型,
      • #23 = NameAndType        #5:#6          // m:I
    • 動態調用點和動態常量

常量池中的常量有17種類型,好比說 CONSTANT_Methodref_info,CONSTANT_Classref_info,CONSTANT_Utf8_info等等。 每種類型常量的結構也不近相同。共同點是,都已u1的tag開頭,表示類型。《深刻理解Java虛擬機》中,列出了完整的定義,下面簡單舉例。

CONSTANT_Methodref_info:

  • tag,u1,值爲10
  • index,u2指向聲明方法的類描述符CONSTANT_Classref_info的索引
  • index,u2指向名稱及類型描述符CONSTANT_NameAndType_info的索引

CONSTANT_Classref_info:

  • tag,u1,值爲7
  • index,u2指向全限定名常量項的索引

ED0C3D04-EB72-4F88-A7DF-976C05B2CAAB.png 常量池項目數量:25 (0x001A是26,常量池索引值從1開始,0保留,因此實際只用25個常量,0能夠理解成不引用常量池中的項目)。

第一項(0A 00 04 00 16), #1 = Methodref   #4.#22:

  • 0A,tag,u1 爲10表示是CONSTANT_Methodref_info
  • 00 04,index,u2,所在類描述符索引,即 #4 = Class    #25      // java/lang/Object
  • 00 16,index,u2,名稱及類型描述符索引,即#22 = NameAndType    #7:#8     // "":()V

值得注意的是 父類方法類型是Methodref,類方法inc類型是utf8。

第二項(09 00 03 00 17) , #2 = Fieldref           #3.#23         // clazz/TestClass.m:I

  • 09,tag,u1 爲9表示是CONSTANT_Fieldref_info
  • 00 03,index,u2爲 3所在類描述符索引, #3 = Class     #24       // clazz/TestClass
  • 00 17,index,u2爲23名稱及類型描述符索引,即 #23 = NameAndType     #5:#6      // m:I

剩餘的23個項目,太多了,懂意思就行了。

6.3.3 訪問標誌

121C6A28-F028-418E-B254-A9DD4E548CBA.png 結束了常量池25項解析,接着是Class訪問標誌(access_flags) 00 21 表示 0x0020 & 0x0001

  • 0x0001 ACC_PUBLIC 是否爲public類型
  • 0x0020 ACC_SUPER 是否容許使用invokespecial字節碼指令的新語義,invokespecial語義在JDK1.0.2發生改變,1.0.2後編譯的類,這個標誌必須爲真。

6.3.4 類索引、父類索引與接口索引集合

撒大聲地所多.png

  • 類索引,u2,0x0003,#3 = Class              #24            // clazz/TestClass
  • 父類索引,u2,0x0004,#4 = Class          #25            // java/lang/Object
  • 接口索引數量,u2,0x0000,沒有接口

6.3.5 字段表集合

image.png

  • fields_count,u2,0x0001,表示有一個字段
  • access_flags,u2,0x0002,ACC_PRIVATE
  • name_index,u2,0x0005,  #5 = Utf8      m
  • descriptor_index,u2,0x0006,  #6 = Utf8         I
  • attributes_count,u2,0x0000, 無
  • attributes_info,無

6.3.6方法表集合

image.png 方法數量,u2,0x0003,有3個方法 方法表結構

  • access_flags,u2,0x0001
  • name_index,u2,0x0007
  • descriptor_index,u2,0x0008
  • attributes_count,u2,0x0001
  • attribute_info
    • attribute_name_index,u2,0x0009,對應是Code屬性,接下來按照Code來解析
    • attribute_length,u4,0x0000002f
    • max_stack,u2,0x0001,操做數棧的最大深度
    • max_locals,u2,0x0001,局部變量表所須要的空間
    • code_length,u4,0x00000005,code長度爲5個u1
    • code,u1, 2A,B7,00,01,B1
      • 2A,對應指令aload_0,將第0個變量槽中reference類型的本地變量推送到操做數棧
      • B7, 對應指令invokespecial,
      • 00, 對應指令nop,什麼也不作
      • 01,對應指令aconst_null,將null推到棧頂
      • B1,對應指令 return
    • exception_table_length,u2,0x0000
    • exception_table,
    • attributes_count,u2,0x0002
    • attributes。。。後面就不解析了,意會就好。

6.3.7屬性表集合

在方法表中,咱們遇到了一個屬性"Code",還有其餘屬性。

屬性名稱 使用位置 含義
Code 方法表 Java代碼編譯成的字節碼指令
ConstantValue 字段表 由final關鍵字定義的常量值
LocalVariableTable Code屬性 方法的局部變量描述
SourceFile 類文件 記錄類文件名稱
... ... ...

解析工做就不作了。

6.4 字節碼指令簡介

Java虛擬機指令,單字節操做碼(OpCode)+操做數(Operand,零或多個)。面向操做數棧,而非寄存器。多數指令不包含操做數,指令參數放在操做數棧。 指令執行過程簡易僞代碼

do{
   自動計算PC寄存器值加1;
   根據PC寄存器指示位置,從字節碼流中取出操做碼;
   if(字節碼存在操做數) 從字節碼流中取出操做數;
    執行操做碼所定義的操做;
}while(字節碼流長度>0);
複製代碼

6.4.1 字節碼與數據類型

大多數指令都包含其操做要求的數據類型,例如iload,從局部變量表中加載int型數據到操做數棧中。 i表明int,l表明long,f表明float,a表明reference。 也有些指令跟數據類型無關,好比 goto。

6.4.2指令分類

  • 加載和存儲指令, iload,iload_0,iload_1,fload,istore,bipush,sipush,wide...
  • 運算指令,iadd,isub,idiv,ishl,ior,ixor,iinc,dcmpg
  • 類型轉換指令,窄化顯式類型轉換 i2b,i2c,d2f。
  • 對象建立與訪問指令,new,newarray,anewarray,getfield,baload,iaload
  • 操做數棧管理指令,pop,pop2,dup2_x1,swap
  • 控制轉移指令,ifeq,tableswitch,goto,goto_w
  • 方法調用和返回指令, invokevirtual,invokeinterface,invokespecial,invokestatic,invokedynamic
  • 異常處理指令,athrow
  • 同步指令。

其餘等等。。這章就瞭解下字節碼構成和指令。

第7章 虛擬機類加載機制

7.1 概述

類加載機制:從Class文件到內存中Java類型的過程。各個階段時間段上能夠有重疊。 類加載是在運行期間執行的,也描述爲動態加載和動態鏈接。

7.2 類加載時機

截屏2020-08-24下午1.28.17.png 對於何時開始加載, 《Java虛擬機規範》沒有強制約束。可是嚴格規定了有且只有六種狀況,若是類沒有初始化,必須當即對類進行"初始化" (加載、鏈接必然會先執行),稱之爲主動引用:

  • 遇到new、getstatic、putstatic、invokestatic字節碼指令時
    • 指令new實例化對象。
    • 指令getstatic/putstatic 訪問其靜態對象(被final修飾,編譯期已放入常量池的除外)。
    • 指令invokestatic,調用其靜態方法。
  • 反射調用
  • 子類初始化時,先觸發父類的初始化
  • 虛擬機啓動時,初始化用戶指定的要執行的主類
  • MethodHandle解析結果爲REF_getStatic,REF_setStatic,REF_invokeStaitc,Ref_newInvokeSpecial
  • 當一個接口定義了JDK 8新加入的默認方法(default修飾)

不會觸發類初始化的幾個場景舉例:

  • 經過子類引用父類的靜態字段,不會致使子類初始化
  • 經過數組定義引用類,不會觸發此類的初始化
  • 引用在編譯期已被放入常量池的常量。
//場景一 經過子類引用父類的靜態字段,不會致使子類初始化
public class SuperClass {
    static { System.out.println("SuperClass init!");}
    public static int value=123;
}

public class SubClass extends SuperClass{
    static { System.out.println("SubClass init!");}
}

public class NoInitialization1 {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

複製代碼
//場景二 經過數組定義引用類,不會觸發此類的初始化
public class NoInitialization2 {
    public static void main(String[] args) {
        SuperClass[] scarray=new SuperClass[10];
    }
}
複製代碼
//場景三 引用在編譯期已被放入常量池的常量。
public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWRLD="hello world";
}
public class NoInitialization3 {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWRLD);
    }
}
複製代碼

對於場景三咱們查看NoInitialization3的字節碼發現,「hello world」已經在其常量池中,使用 ldc指令將常量壓入棧中。而System.out則是使用getstatic指令。這個地方不涉及到ConstClass的初始化

Constant pool:
  #4 = String             #25            // hello world
  #25 = Utf8               hello world
{
  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String hello world
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}
複製代碼

7.3類加載過程

7.3.1  加載

加載(loading):從靜態文件到運行時方法區。完成三件事情:

  • 經過一個類的全限定名獲取定義此類的二進制字節流。
    • 能夠從ZIP包中讀取(JAR,WAR等等)
    • 從網絡中獲取,好比Web Applet
    • 運行時計算生成,好比動態代理技術, 「*$Proxy」代理類
    • 數據庫中讀取
    • 加密文件中讀取
    • ......
  • 將該字節流的類靜態存儲結構轉化成方法區的運行時數據結構。
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區中這個類的各類數據的訪問入口。

使用Java虛擬機內置的引導類加載器,或者用戶自定義的類加載器。

7.3.2 驗證

確保字節流符合《Java虛擬機規範》的約束,代碼安全性問題驗證。驗證是重要的,但不是必須的。 四個階段:

  • 文件格式驗證,此階段經過後,會存儲到方法區。後面階段基於方法區數據進行驗證,再也不讀取字節流。
  • 元數據驗證,類元數據信息語義校驗
  • 字節碼驗證,最複雜,對類的Code部分進行檢驗分析。程序語義合法性,安全性等等
  • 符號引用驗證,在解析過程當中發生,若是沒法經過符號引用驗證,Java虛擬機會拋出java.lang.IncompatibleClassChangeError的子類異常,如 NoSuchFieldError,NoSuchMethodError等等。

7.3.3 準備

一般狀況下,爲類變量(靜態變量),分配內存並設置初始值(零值)。初值並非代碼中賦的值123。123要等到初始化階段。

public static int value = 123;
複製代碼

編譯成class文件

public static int value;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
...
 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
         0: bipush        123
         2: putstatic     #2                  // Field value:I
         5: return     
...
複製代碼

某些狀況下,設置初始值爲456。好比final修飾的變量。由於變量值456,會提早加入到常量池。

public static final int value2 = 456;
複製代碼
public static final int value2;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 456
複製代碼

7.3.4 解析

將常量池內的符號引用替換爲直接引用的過程。 好比說這種,咱們要把 #2替換成實際的類引用,若是是未加載過的類引用,又會涉及到這個類加載過程。

getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
複製代碼
  • 類或接口解析
  • 字段解析
  • 方法解析
  • 接口方法解析

7.3.5 初始化

執行類構造器()方法,非實例構造器()方法 。 ()方法:執行類變量賦值語句和靜態語句塊(static{})。順序爲其在源文件中順序決定。 舉例1:非法向前引用變量。 value的定義在 static{} 以後,只能賦值,不能讀取值。

public class PrepareClass {
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
    public static int value=123;
}
複製代碼

可是下面就能夠

public class PrepareClass {
    public static int value=123;
    static {
        value=3;
        System.out.println(value);// value: illegal forword reference
    }
}
複製代碼

class文件參考

0: bipush        123
2: putstatic     #2                  // Field value:I
5: iconst_3
6: putstatic     #2                  // Field value:I
9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
12: getstatic     #2                  // Field value:I
15: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
18: return
複製代碼

舉例2: ()執行順序。子類初始化時,要先初始化父類

public class TestCInitClass2 {

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
複製代碼

輸出:

2
複製代碼

Java虛擬機必須保證()方法在多線程環境下的同步問題。

7.4 類加載器

實現「經過一個類的全限定名來獲取其二進制字節流」的代碼,稱之爲「類加載器」(Class Loader)。

7.4.1 類與類加載器

類與其加載器肯定了這個類在Java虛擬機中的惟一性。

三層類加載器,絕大多數Java程序會用到如下三個系統提供的類加載器進行加載:

  • 啓動類加載器(BootStrap Class Loader)
  • 擴展類加載器(Extension Class Loader)
  • 應用程序類加載器(Application Class Loader)

除了以上三個還有用戶自定義的加載器,經過集成java.lang.ClassLoader類來實現。

啓動類加載器

加載Java的核心庫,native代碼實現,不繼承java.lang.ClassLoader

URL[]  urls= sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
    System.out.println(url);
}

結果輸出:
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/resources.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jsse.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jce.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/charsets.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jfr.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/classes
複製代碼

擴展類加載器

加載Java的擴展庫,加載ext目錄下的Java類

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

結果輸出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
複製代碼

應用程序類加載器

加載Java應用的類。經過ClassLoader.getSystemClassLoader()來獲取。

URL[]  urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
    System.out.println(url);
}

結果輸出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
...
file:/.../jdk1.8.0_73.jdk/Contents/Home/lib/tools.jar
file:/.../java_sample/out/production/java_sample/  //這是咱們的應用程序
file:/Applications/IntelliJ%20IDEA.app/Contents/lib/idea_rt.jar
複製代碼

自定義類加載器

7.4.2 雙親委派模型截屏2020-08-25下午5.04.25.png

ClassLoader.loadClass

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
複製代碼

AppClassLoader,ExtClassLoader都繼承URLClassLoader。 URLClassLoader.findClass(name)

protected Class<?> findClass(final String name)throws ClassNotFoundException {
       // 一、安全檢查
    // 二、根據絕對路徑把硬盤上class文件讀入內存
    byte[] raw = getBytes(name); 
    // 三、將二進制數據轉換成class對象
    return defineClass(raw);
    }
複製代碼

若是咱們本身去實現一個類加載器,基本上就是繼承ClassLoader以後重寫findClass方法,且在此方法的最後調包defineClass。 ** 雙親委派確保類的全局惟一性。 例如不管哪一個類加載器須要加載java.lang.Object,都會委託給最頂端的啓動類加載器加載。

參考: 通俗易懂 啓動類加載器、擴展類加載器、應用類加載器 深刻探討 Java 類加載器

7.4.3 線程上下文類加載器

線程上下文類加載器(context class loader),能夠從java.lang.Thread中獲取。 雙親委派模型不能解決Java應用開發中遇到的全部類加載器問題。 例如,Java提供了不少服務提供者接口(Service Provider Interface,SPI),容許第三方提供接口實現。常見的SPI有JDBC,JCE,JNDI,JAXP等。SPI接口由核心庫提供,由引導類加載器加載。 而其第三方實現,由應用類加載器實現。此時SPI就找不到具體的實現了。 SPI接口代碼中使用線程上下文類加載器。線程上下文類加載器默認爲應用類加載器。

第8章 虛擬機字節碼執行引擎

8.1 概述

虛擬機是相對於物理機的概念。 物理機的執行引擎是直接創建在處理器,緩存,指令集合操做系統底層上。 虛擬機的執行引擎是創建在軟件之上,不受物理條件限制,定製指令集與執行引擎。 虛擬機實現中,執行過程能夠是解釋執行和編譯執行,能夠單獨選擇,或者混合使用。 但全部虛擬機引擎從統一外觀(Facade)來講,都是輸入字節碼二進制流,字節碼解析執行,輸出執行結果。

本章從概念角度講解虛擬機的方法調用和字節碼執行。

8.2 運行時棧幀結構

Java虛擬機以方法做爲最基本的執行單元。每一個方法在執行時,都會有一個對應的棧幀(Stack Frame) .棧幀同時也是虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。 一個棧幀須要多大的局部變量表,須要多深的操做數棧,早在編譯成字節碼時就寫到了方發表的Code屬性中。 Code:  stack=2, locals=1, args_size=1 所以一個棧幀須要分配多少內存,在運行前就已肯定,取決於源碼和虛擬機自身實現。 截屏2020-08-26上午10.42.35.png

8.2.1 局部變量表

      局部變量表容量最小單位爲變量槽(Variable Slot), 《Java虛擬機規範》規定一個變量槽能夠存放一個boolean,byte,char,init,float,reference或returnAddress類型的數據。32位系統能夠是32位,64位系統能夠是64位去實現一個變量槽。對於64位的數據類型(long和double),以高位對齊的方式分配兩個連續的變量槽。 因爲是線程私有,不管兩個連續變量槽的讀寫是否爲原子操做,都不會有線程安全問題。

從參數到參數列表

     當一個方法被調用時,會把參數值放到局部變量表中。類方法參數Slot從0開始。實例方法參數Slot從1開始,Slot0給了this,指向實例。 咱們比較類方法和實例方法的字節碼。

public static int add(int a, int b) {return a + b;}
public int remove(int a, int b) {return a - b;}
複製代碼
public static int add(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0     a   I
            0       4     1     b I public int remove(int, int);
    flags: ACC_PUBLIC
    Code:
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lexecute/Reerer;
            0       4     1     a   I
            0       4     2     b   I
複製代碼

變量槽複用

當變量的做用域小於整個方法體時,變量槽能夠複用,爲了節約棧內存空間。好比 {},if(){}等代碼塊內。變量槽複用會存在「輕微反作用」,內存回收問題。

public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        System.gc();
}

//執行結果
[GC (System.gc())  69468K->66040K(251392K), 0.0007701 secs]
[Full GC (System.gc()) 66040K->65934K(251392K), 0.0040938 secs] //解釋: 雖然placeholder的做用域被限制,但gc時,局部變量表仍然引用placeholder,沒法被回收。 複製代碼
public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
        }
        int i=0;
        System.gc();
}
//執行結果
[GC (System.gc())  69468K->66040K(251392K), 0.0007556 secs]
[Full GC (System.gc()) 66040K->398K(251392K), 0.0044978 secs] //解釋: 雖然placeholder的做用域被限制,int i=0複用了slot0,切斷了局部變量表的引用placeholder。 複製代碼
public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64 * 1024 * 1024];
            placeholder=null;
        }
        System.gc();
}
//執行結果
[GC (System.gc())  69468K->66088K(251392K), 0.0022762 secs]
[Full GC (System.gc()) 66088K->398K(251392K), 0.0050265 secs] //解釋 主動釋放placeholder 複製代碼

局部變量賦值

類變量在準備階段,會被賦默認零值。而局部變量沒有準備階段。因此下面代碼是編譯不經過的,即使編譯經過,在檢驗階段,也會被發現,致使類加載失敗。

public static void fun4(){
     int a;
     
     //編譯失敗,Variable ‘a’ might not have been initialized
     System.out.println(a);
}
複製代碼

8.2.2操做數棧

操做數棧(Operand Stack)      字節碼指令讀取和寫入操做數棧。操做數棧中元素的數據類型必須與指令序列嚴格匹配。編譯階段和類檢驗階段都會去保證這個。     在大多數虛擬機實現中,上面棧幀的操做數棧與下面棧幀的局部變量會有一部分重疊,這樣不只節約了空間,重要的是在方法調用時直接公用數據,無須而外的參數複製。

8.2.3 動態鏈接

在類加載過程當中,會把符號引用解析爲直接引用。方法調用指令以常量池中的符號引用爲參數。這些方法符號引用一部分在類加載或者第一次使用時轉化爲直接引用,這種轉化被稱爲靜態解析。另一部分則須要在每次運行期間轉化爲直接引用,這部分稱之爲動態鏈接。

8.2.4 方法返回地址

正常調用完成和異常調用完成。 恢復主調方法的執行狀態。

8.3 方法調用

Java虛擬機中的5條方法調用指令:

  • invokestatic,調用靜態方法。
  • invokespecial,調用實例構造器()方法,私有方法和父類中的方法。
  • invokevirtual,調用虛方法?
  • invokeinterface,調用接口方法,會在運行期,肯定一個該接口的實現對象。
  • invokedynamic,運行時動態解析出調用點限定符所引用的方法,而後再執行該方法。前4個指令邏輯固化在虛擬機內部,而invokedynamic指令的分派邏輯由用戶設定的引導方法決定。

方法按照類加載階段是否能轉化成直接引用分類,能夠分爲:

  • 非虛方法(Non-Virtual Method), 類加載階段,直接把符號引用解析爲該方法的直接引用。
    • 包括能夠被invokestatic調用的靜態方法,
    • 包括能夠被invokespecial調用的實例構造器,私有方法,父類方法
    • final修飾的方法(儘管它使用invokevirtual調用),此類型方法沒法被覆蓋,不存在多太選擇,是惟一的。
  • 虛方法 (Virtual Method) ,其餘方法。

8.3.1 解析

非虛方法的調用稱之爲解析(Resolution),"編譯器可知,運行期不可變",即類加載階段把符號引用轉化爲直接引用。 而另一個方法調用的方式稱之爲分派(Dispatch)。

8.3.2 分派

分派(Dispatch)是靜態或者動態的,又或者是單分派或者多分派。重載或者重寫會出現同名方法。同名方法的選擇,我能夠稱之爲分派

1. 靜態分派

Method Overload Resolution, 這部份內容實際上叫作方法重載解析。靜態分派發生在編譯階段。 先來看一段代碼,sayHello方法重載。

//方法靜態分派
public class StaticDispatch {

    static abstract  class Human{}

    static class Man extends Human{}

    static class Woman extends Human{}

    public static void sayHello(Man man){System.out.println("hello,gentleman!"); }

    public static void sayHello(Human guy){ System.out.println("hello,guy!");}

    public static void sayHello(Woman women){System.out.println("hello,lady!");}

    public static void main(String[] args) {
        Human man=new Man();
        Human woman=new Woman();
        StaticDispatch dispatch=new StaticDispatch();
        dispatch.sayHello(man);
        dispatch.sayHello(woman);
    }
}

//執行結果:
hello,guy!
hello,guy!
複製代碼

對應Class字節碼

public static void main(java.lang.String[]);
    Code:
      stack=2, locals=3, args_size=1
         0: new           #7                  // class execute/StaticDispatch$Man
         3: dup
         4: invokespecial #8                  // Method execute/StaticDispatch$Man."<init>":()V
         7: astore_1
         8: new           #9                  // class execute/StaticDispatch$Woman
        11: dup
        12: invokespecial #10                 // Method execute/StaticDispatch$Woman."<init>":()V
        15: astore_2
        16: new           #11                 // class execute/StaticDispatch
        19: dup
        20: invokespecial #12                 // Method "<init>":()V
        23: astore_3
        24: aload_3
        25: aload_1
        26: invokevirtual #13                 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
        29: aload_3
        30: aload_2
        31: invokevirtual #13                 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
        34: return
複製代碼

第0~15行,咱們構建了Man對象和Woman對象,並放入了局部變量表中。 第26行,執行方法Method sayHello:(Lexecute/StaticDispatch H u m a n ; ) V ,第 31 行,執行方法 M e t h o d s a y H e l l o : ( L e x e c u t e / S t a t i c D i s p a t c h Human;)V, 第31行,執行方法Method sayHello:(Lexecute/StaticDispatch Human;)V, 實際執行的都是sayHello(Human)。而不是sayHello(Man)或者sayHello(Woman)。 這裏涉及到兩個類型:

  • 靜態類型(Static Type),或者叫「外觀類型(Apparent Type)」,即Human
  • 實際類型(Actual Type),或者叫「運行時類型(Runtime Type)」,即Man,Woman

編譯期並不知道對象的實際類型,因此按照對象的靜態類型去分派方法。

2. 動態分派

與重寫(Override)密切關聯。動態分派發生在運行期間。在運行時,肯定方法的接收者(方法所屬對象)

//方法動態分派
public class DynamicDispatch {

    static abstract class Human {
        public void sayHello() {System.out.println("hello,guy!");}
    }

    static class Man extends Human {
        public void sayHello() {System.out.println("hello,gentleman!"); }
    }

    static class Woman extends Human {
        public void sayHello() {System.out.println("hello,lady!");}
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();//hello,gentleman!
        woman.sayHello();//hello,lady!
        man = new Woman();
        man.sayHello();//hello,lady!
    }
}

//執行結果:
hello,gentleman!
hello,lady!
hello,lady!
複製代碼

對應字節碼

0: new   #2  // class execute/DynamicDispatch$Man
3: dup
4: invokespecial #3  // Method execute/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new   #4  // class execute/DynamicDispatch$Woman
11: dup
12: invokespecial #5  // Method execute/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6  // Method execute/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6  // Method execute/DynamicDispatch$Human.sayHello:()V
24: new   #4  // class execute/DynamicDispatch$Woman
27: dup
28: invokespecial #5  // Method execute/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6  // Method execute/DynamicDispatch$Human.sayHello:()V
36: return
複製代碼

第7行,astore_1存儲了Man對象 第15行,astore_2存儲了Woman對象 第16,17行,aload_1,invokevirtual.實際調用的是Man.sayHello()方法 第20,21行,aload_2,invokevirtual.實際調用的是Woman.sayHello()方法 第31行,astore_1存儲了Woman對象 第32,33行,aload_1,invokevirtual.實際調用的是Woman.sayHello()方法

運行期間,選擇是根據man和woman對象的實際類型分派方法。

小知識:字段永遠不參與多態,方法中訪問的屬性名始終是當前類的屬性。子類會遮蔽父類的同名字段

3.單分派與多分派

方法的宗量:方法的接收者與方法的參數 單分派:基於一種宗量分派 多分派:基於多種宗量分派。 當前Java語言是一門靜態多分派,動態單分派的語言。編譯期根據方法接收者和參數肯定方法的符號引用。運行期根據方法的接收者,解析和執行符號引用。

考慮下面一段代碼

public class Dispatch {

    static class Father{
        public void f() {System.out.println("father f void");}
        public void f(int value) {System.out.println("father f int");}
    }
    static  class Son extends Father{
        public void f(int value) {System.out.println("Son f int"); }
        public void f(char value) { System.out.println("Son f char");}
    }

    public static void main(String[] args) {
        Father son=new Son();
        son.f('a');
    }
}
//執行結果: Son f int
複製代碼

字節碼

0: new           #2                  // class execute/Dispatch$Son
3: dup
4: invokespecial #3                  // Method execute/Dispatch$Son."<init>":()V
7: astore_1
8: aload_1
9: bipush        97
11: invokevirtual #4                  // Method execute/Dispatch$Father.f:(I)V
複製代碼

首先是編譯期的靜態分派,先選擇靜態類型Father,因爲Father中沒有f(char),則會選擇最合適的f(int),肯定方法爲Father.f:(I)V。其次是運行期,接收者爲Son,Son中有重寫的f:(I)V。因此最終執行的是Son.f:(I)V

4. 虛擬機動態分派的實現

虛方法表,接口方法表,類型繼承分析,守護內聯,內聯緩存 截屏2020-08-27上午11.41.15.png

8.4 動態類型語言支持

8.4.1 動態類型語言

動態類型語言的關鍵特徵:類型檢查的主體過程是在運行期而不是編譯器,好比說Groovy、JavaScript、Lisp、Lua、Python。 靜態類型語言:編譯器就進行類型檢查,好比C++,Java。

8.4.2 Java與動態類型

Java虛擬機須要支持動態類型語言,因而在JDK7發佈 invokedynamic指令。

8.4.3 java.lang.invoke包

8.4.4 invokedynamic指令

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

8.5.1 解釋執行

8.5.2 基於棧的指令集合基於寄存器的指令集

8.5.3 基於棧的解釋器執行過程

public int calc() {
    int a = 100;
    int b = 200;
    int c = 300;
    return (a + b) * c;
}
複製代碼
stack=2, locals=4, args_size=1
         0: bipush        100   //常量100壓入操做數棧頂
         2: istore_1		    //棧頂元素(100)存入變量槽1,同時消費掉棧頂元素
         3: sipush        200   //常量200壓入操做數棧頂
         6: istore_2            //棧頂元素(200)存入變量槽2,同時消費掉棧頂元素
         7: sipush        300   //常量300壓入操做數棧頂
        10: istore_3            //棧頂元素(300)存入變量槽3,同時消費掉棧頂元素
        11: iload_1             //將局部變量slot1值100壓入操做數棧頂
        12: iload_2             //將局部變量slot2值200壓入操做數棧頂
        13: iadd                //消費棧頂100和200,獲得300,並壓入棧頂
        14: iload_3				//將局部變量slot3值300壓入操做數棧頂
        15: imul				//消費棧頂300和300,獲得90000,並壓入棧頂
        16: ireturn             //消費棧頂90000,整型結果返回給方法調用者
複製代碼

第10章 前端編譯與優化

10.1 概述

先明確幾個概念 即時編譯器(JIT編譯器,Just In Time Compiler),運行期把字節碼變成本地代碼的過程。 提早編譯器(AOT編譯器,Ahead Of Time Compiler),直接把程序編譯成與目標及其指令集相關的二進制代碼的過程。

這裏討論的「前端編譯器」,是指把*.java文件轉換成*.class文件的過程,主要指的是javac編譯器。

10.2 Javac編譯器

10.2.1 介紹

Javac編譯器是由Java語言編寫。分析Javac代碼的整體結構來看,編譯過程大體分爲1個準備過程和3個處理過程。以下:

  • 1)準備過程:初始化插入式註解處理器
  • 2)解析與填充符號表
    • 詞法、語法分析。將源代碼的字符流轉變爲標記集合,構造出抽象語法樹。
    • 填充符號表。產生符號地址和符號信息。
  • 3)插入式註解處理器的註解處理
  • 4)分析與字節碼生成
    • 標註檢查。對語法的靜態信息進行檢查。
    • 數據流及控制流分析。對程序動態運行過程進行檢查
    • 解語法糖。將語法糖代碼還原爲原有形式
    • 字節碼生成。將前面各個步驟生成的信息轉化爲字節碼。

若是註解處理產生新的符號,又會再次進行解析填充過程。 截屏2020-08-28上午10.10.16.png Javac編譯動做的入口com.sun.tools.javac.main.JavaCompiler類。代碼邏輯主要所在方法compile(),compile2() 截屏2020-08-28上午11.24.33.png

10.2.2 解析和填充符號表

1. 詞法、語法分析

對應parserFiles()方法 詞法分析:源碼字符流轉變爲標記(Token)集合的過程。標記是編譯時的最小元素。關鍵字、變量名、字面量、運算符都是能夠做爲標記。 如「int a = b + 2」, 包含了6個標記,int,a , =,  b,  +, 2 。詞法分析由com.sun.tools.javac.parser.Scanner實現。 語法分析:根據標記序列構造抽象語法樹的過程。抽象語法樹(Abstract Syntax Tree,AST),描述代碼語法結構的樹形表示形式,樹的每一個節點都表明一個語法結構,例如包,類型,運算符,接口,返回值等等。com.sun.tools.javac.parser.Parser實現。抽象語法樹是以com.sun.tools.javac.tree.JCTree類表示。 後續的操做創建在抽象語法樹之上。

2.填充符號表

對應enterTree()方法。

10.2.3 註解處理器

      JDK6,JSR-269提案,「插入式註解處理器」API。提早至編譯期對特定註解進行處理,能夠理解成編譯器插件,容許讀取、修改、添加抽象語法樹中的任意元素。若是產生改動,編譯器將回到解析及填充符號表過程從新處理,直到不產生改動。每一次循環過程稱爲一個輪次(Round).    使用註解處理器能夠作不少事情,譬如Lombok,能夠經過註解自動生成getter/setter方法、空檢查、產生equals()和hashCode()方法。

10.2.4 語義分析與字節碼生成

抽象語法樹可以表示一個正確的源程序,但沒法保證語義符合邏輯。語義分析的主要任務是進行類型檢查、控制流檢查、數據流檢查等等。 例如

int a = 1;
boolean b = false;
char c = 2;

//後續可能出現的運算,都是能生成抽象語法樹的,但只有第一條,能經過語義分析
int  d= a + c;
int  d= b + c;
char d= a + c;
複製代碼

在IDE中看到的紅線標註的錯誤提示,絕大部分來源於語義分析階段的結果。

1. 標註檢查

attribute()方法,檢查變量使用前是否已被聲明,變量與賦值的數據類型是否匹配等等。 3個變量的定義屬於標註檢查。標註檢查順便會進行極少許的一些優化,好比常量摺疊(Constant Folding).

int a = 1 + 2; 實際會被摺疊成字面量「3複製代碼

2. 數據及控制流分析

flow()方法,上下文邏輯進一步驗證,好比方法每條路徑是否有返回值,數值操做類型是否合理等等。

3. 解語法糖

語法糖(Syntactic Sugar),編程術語 Peter J.Landin。減小代碼量,增長程序可讀性。好比Java語言中的泛型(其餘語言的泛型不必定是語法糖實現,好比C#泛型直接有CLR支持),變長數組,自動裝箱拆箱等等。 解語法糖,編譯期將糖語法轉換成原始的基礎語法。

4. 字節碼生成

  • 將前面生成的信息(語法樹,符號表)轉化爲字節碼,
  • 少許代碼添加,(),()等等
  • 少許代碼優化轉換,字符串拼接操做替換爲StringBuffer或StringBuilder等等。

10.3 Java語法糖

10.3.1 泛型

1.Java泛型

       JDK5,Java的泛型實現稱爲「類型擦除式泛型」(Type Erasure Generic),相對的C#選擇的是「具現化泛型」(Reified Generics),C#泛型不管在源碼中,仍是編譯後的中間語言表示(此時泛型都是一個佔位符),List 與List是兩個不一樣的類型。而Java泛型,只是在源碼中存在,編譯後都變成了統一的類型,稱之爲類型擦除,在使用處會增長一個強制類型轉換的指令。

Map<String, String> stringMap = new HashMap<String, String>();
stringMap.put("hello", "你好");
System.out.println(stringMap.get("hello"));

Map objeMap = new HashMap();
objeMap.put("hello2", "你好2");
System.out.println((String)objeMap.get("hello2"));
複製代碼

截取部分字節碼

0: new           #2                  // class java/util/HashMap
4: invokespecial #3                  // Method java/util/HashMap."<init>":()V
13: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
25: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
30: checkcast     #9                  // class java/lang/String
33: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
        
36: new           #2                  // class java/util/HashMap
40: invokespecial #3                  // Method java/util/HashMap."<init>":()V
49: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
61: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
66: checkcast     #9                  // class java/lang/String
69: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

72: return
複製代碼

能夠看到兩部分代碼在編譯後是同樣的。 第0行,new HashMap<String, String>() 實際構造的是java/util/HashMap。 第30行,stringMap.get("hello") ,checkcast指令,作了一個類型轉換

2. 歷史背景

2004年,Java5.0。爲了保證代碼的「二進制向後兼容」,引入泛型後,原先的代碼必須可以編譯和運行。例如Java數組支持協變,集合類也能夠存入不一樣類型元素。 代碼以下

Object[] array = new String[10]; 
array[0] = 10; // 編譯期不會有問題,運行時會報錯 

ArrayList things = new ArrayList(); 
things.add(Integer.valueOf(10)); //編譯、運行時都不會報錯
things.add("hello world");
複製代碼

若是要保證Java5.0引入泛型後,上述代碼依然能夠運行,有兩個選擇:

  • 原先須要泛型化的類型保持不變,再新增一套泛型化的類型版本。泛型具現化,好比C#新增了一組System.Collections.Generic的新容器,原先的System.Collections保持不變。
  • 把須要泛型化的類型原地泛型化,Java5.0採用的原地泛型化方式爲類型擦除。

爲什麼C#與Java的選擇不一樣,主要是C#當時才2年遺留老代碼少,Java快10年了老代碼多。類型擦除是偷懶留下的技術債。

3.類型擦除

類型擦除除了前面提到的編譯後都變成了統一的裸類型以及使用時的類型檢查和轉換以外還有其餘缺陷。 1)不支持原始類型(Primitive Type)數據泛型,ArrayList須要使用其對應引用類型ArrayList,致使了讀寫的裝箱拆箱。 2)運行期沒法獲取泛型類型信息,例如

public  <E> void doSomething(E item){
        E[] array=new E[10];  //不合法,沒法使用泛型建立數組
        if(item instanceof  E){}//不合法,沒法對泛型進行實例判斷
}
複製代碼

當咱們去寫一個List到數組的轉換方法時,須要額外傳遞一個數組的組件類型

public  static <T> T[] convert(List<T> list,Class<T> componentType){
        T[] array= (T[]) Array.newInstance(componentType,list.size());
        for (int i = 0; i < list.size(); i++) {
            array[i]=list.get(i);
        }
        return array;
}
複製代碼

3)類型轉換問題。

//沒法編譯經過
//雖然String是Object的子類,但ArrayList<String>並非ArrayList<Object>的子類。
ArrayList<Object> list=new ArrayList<String>();
複製代碼

爲了支持協變和逆變,泛型引入了 extends ,super

//協變 
ArrayList<? extends Object> list = new ArrayList<String>();

//逆變
ArrayList<? super String> list2 = new ArrayList<Object>();
複製代碼

4 值類型與將來泛型

2014年,Oracle,Valhalla語言改進項目內容之一,新泛型實現方案

10.3.2 其餘

自動裝箱,自動拆箱,遍歷循環,變長參數,條件編譯,內部類,枚舉類,數值字面量,switch,try等等。

10.3.3 *擴展閱讀

Java協變介紹 Lambda與invokedynamic

10.4 實戰 Lombok註解處理器

第11章 後端編譯與優化

11.1概述

前面一章講的是從*.java到*.class的過程,即源碼到字節碼的過程。 這一章講的是從二進制字節碼到目標機器碼的過程,分爲兩種即時編譯器和提早編譯器。

11.2 即時編譯器

目前主流的兩款商用Java虛擬機(HotSpot、OpenJ9)裏,Java程序最初都是經過解釋器 (Interpreter)進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁,就會把這些代碼認 定爲「熱點代碼」(Hot Spot Code),爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些代 碼編譯成本地機器碼,並以各類手段儘量地進行代碼優化,運行時完成這個任務的後端編譯器被稱 爲即時編譯器。

11.3 提早編譯器

  • 即時編譯消耗的時間都是本來可用於程序運行的時間,消耗的運算資源都是本來可用

於程序運行的資源,

  • 給即時編譯器作緩存加速,去改善Java程序的啓動時間,以

及須要一段時間預熱後才能到達最高性能的問題。這種提早編譯被稱爲動態提早編譯(Dynamic AOT)或者索性就大大方方地直接叫即時編譯緩存(JIT Caching)

Android虛擬機歷程: Android4.4以前 Dalvik虛擬機 即便編譯器 Android4.4開始 Art虛擬機 提早編譯器,致使安裝時須要編譯App,很是耗時,但運行性能獲得提高 Android7.0開始 從新啓用解釋執行和即時編譯,系統空閒時間時自動進行提早編譯。

11.3 編譯器優化技術

第12章 Java內存模型與線程

12.1 概述

介紹虛擬機如何實現多線程,多線程之間因爲共享數據而致使的一系列問題及解決方案

12.2 硬件效率與一致性

介紹Java虛擬機內存模型前,先了解下物理機的併發問題。

  • 硬件效率問題。計算機處理任務除了處理器計算外,還有內存交互,即讀寫數據。而存儲設備與處理機運行速度相差幾個數量級,爲此引入了讀寫速度儘量接近處理器的高速緩存Cache。處理器讀寫緩存數據,緩存將數據同步到內存。
  • 緩存一致性問題。在共享內存多核系統中,每一個處理器都有本身的高速緩存,又共享同一主內存。爲了解決一致性問題,處理器訪問高速緩存時,須要遵循一些協議,好比MSI,MESI,MISI,Synapse,Dragon Protocol等。
  • 代碼亂序執行優化問題。處理器爲了提升運算效率,會出現不按順序執行的狀況,但單線程下,處理器會保證執行結果與順序執行結果一致。而多線程的狀況下,沒法保證多個任務都按照順序執行。

Java虛擬機有本身的內存模型,也會有與物理機類型的問題。

截屏2020-09-01上午9.38.11.png

12.3 Java內存模型

12.3.1 概述

     Java內存模型規定:因此變量都存儲在主內存(Main Memory)中,線程有本身的工做內存,工做內存保存變量在主內存副本。線程對變量的讀寫只能再工做內存(Working Memory)中,線程間共享變量須要經過主內存完成。      JVM內存模型的執行處理將圍繞解決兩個問題展開:

  • 工做內存數據一致性
  • 指令重排序優化,編譯期重排序和運行期重排序。

截屏2020-09-01上午9.37.19.png

12.3.2 內存交互操做

主內存與工做內存的交互協議定義以下操做,Java虛擬機必須保證這些操做是原子性的。

  • lock,做用於主內存變量,把變量標識爲線程獨佔狀態,使其餘線程沒法lock
  • unlock,做用於主內存變量,解除線程獨佔狀態
  • read,做用於主內存變量,把變量值傳輸到工做內存中,一邊隨後的load使用
  • load,做用於工做內存變量,把read的變量值放入工做內存的變量副本中。
  • use,做用於工做內存變量,變量值傳遞給執行引擎
  • assign,做用於工做內存變量,執行引擎賦值給工做內存中的變量
  • store,做用於工做內存變量,變量值傳輸到主內存,以便後續write使用
  • write,做用於主內存變量,把store的變量值放入主內存變量中。

截屏2020-09-01下午1.31.23.png 若是要把變量從主內存拷貝到工做內存,必須順序執行 read和load,但不要求必定連續。 若是要把變量從工做內存同步到主內存,必須順序執行 store和write,但不要求必定連續。

12.3.3 內存模型運行規則

1.內存交互基本操做的3個特性

Java內存模型是圍繞着在併發過程當中如何處理這3個特性來創建的,歸根結底是爲了實現共享變量在多個工做內存中的一致性,以及併發時,程序能如期運行。

  • 原子性(Atomicity),即一個操做或者多個操做,要麼不執行,要麼所有執行且執行過程不會被打斷
  • 可見性(Visibility),當多個線程訪問同一個變量時,一個線程改變了變量值,其餘線程要能當即看到修改過的值。線程經過共享主內存實現可見性。
  • 有序性(Ordering),線程內指令串行(as-if-serial),線程間,對於同步(synchrinized)代碼以及volatile字段的操做須要維持相對有序

2.先行發生原則

happens-before

  • 程序次序規則
  • 管程鎖定規則
  • volatile變量規則
  • 線程啓動規則
  • 線程終止規則
  • 線程中斷規則
  • 對象終結規則
  • 傳遞性

3.內存屏障

內存屏障是被插入到兩個CPU指令之間的一種指令,用來禁止處理器指令發生指令重排序。

12.3.4 volatile型變量

volatile主要有下面兩種語義

語義1 保證可見性

保證了不一樣線程對該volatile型變量操做的內存可見性,但不等同於併發操做的安全性

  • 線程寫volatile變量的過程assign-store-write必須連續出現:
    • 改變工做內存中volatile變量副本的值
    • 將改變的副本值刷新到主內存中
  • 線程讀volatile變量的過程read-load-use必須連續出現:
    • 從主內存讀取volatile變量值並存入工做線程副本
    • 從工做內存讀取變量副本

語義2 禁止指令重排序截屏2020-09-01下午2.22.56.png

volatile型變量使用場景總結起來就是"一次寫入,處處讀取",某個線程負責更新變量,其餘線程只讀取變量,並根據變量新值執行相應邏輯,例如狀態標誌位更新,觀察者模型變量值發佈

相關文章
相關標籤/搜索