深刻理解Java虛擬機:JVM高級特性與最佳實踐(第三版)周志明 讀書筆記選取了書中部份內容,Java虛擬機更多的是一種規範,具體的Java虛擬機實現是有不少的。做者提到本文多數是以Hotspot虛擬機做爲講解。html
運行時數據區:計數器,Java虛擬機棧(局部變量表,操做數棧)方法區(類型信息,運行時常量池
等), Java堆,直接內存。可能產生內存溢出場景。虛擬機對象建立,內存佈局與訪問定位,前端
強引用,軟引用,弱引用,虛引用,分代收集,併發可達性分析,標記-清除,標記-複製,標記-整理,java
Class類文件結構,字節碼指令集程序員
加載,連接,初始化,類加載器,雙親委派模型算法
運行時棧結構,方法解析分派,動態類型語言支持,基於棧的字節碼執行數據庫
從Java文件到字節碼階段:Java註解處理器,泛型,裝拆箱,插入式註解處理器編程
從字節碼到機器碼:AOT,JIT , Android Dalvik,Android ART後端
對於C/C++程序員來講,擔負每個對象生命從開始到終結的維護責任。而對於Java程序員來講,Java幫助程序員自動管理內存,不須要寫顯式的代碼去釋放內存。但虛擬機不是萬能的,一旦出現內存泄漏和溢出問題,若是不瞭解虛擬機怎樣使用內存,將難以排查錯誤和修正問題。 本章從概念上介紹Java虛擬機內存的各個區域,及其可能產生的問題。數組
Java虛擬機在執行Java程序時,會將它管理的內存分紅功能不一樣的運行時數據區域。這些區域有着不一樣的用戶,不一樣的建立和銷燬時間。 有的區域隨着虛擬機進程生命週期,有的區域則依賴用戶線程的啓動和結束而創建和銷燬。根據《Java虛擬機規範》規定,Java虛擬機管理的內存包括如下運行時數據區。緩存
Java虛擬機基於棧的方式去執行程序。每個線程都會有相應的虛擬機棧,而虛擬機棧的棧幀對應於Java方法。
程序計數器(Progrom Counter Register),記錄當前線程執行的字節碼行號指示器。一般來講,字節碼解釋器工做時就是經過改變計數器的值來選取下一條須要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等都依賴計數器完成。每一個線程都有獨立的計數器,互不影響。計數器分配在線程私有的內存空間中。此區域不會出現OOM的狀況。
複製代碼
Java虛擬機棧也是線程私有的,它的生命週期與線程相同,描述的是Java方法執行的線程內存模型,即每一個方法被執行時,都會同步建立一個棧幀(Stack Frame) 存儲局部變量表,操做數棧,動態鏈接等。方法的調用與執行完畢,對應一個棧幀的入棧和出棧。
複製代碼
在Java源碼編譯成字節碼時,一個棧幀須要多大的局部變量表,須要多深的操做數棧就已經被分析計算出來,即編譯事後就已經可以須要多大內存,內存取決於源碼和具體的虛擬機棧內存佈局形式。 在《Java虛擬機規範》中提到:若是線程請求的棧深度大於虛擬機所容許的深度,將會拋出StackOverflowError異常;若是虛擬機棧容量能夠動態擴展,無限擴展,內存不足會拋出OutOfMemoryError異常。 Hotspot虛擬機棧容量不可動態擴展,但若是線程申請棧空間失敗,仍然會OOM。
相較於Java虛擬機棧執行Java方法,本地方法棧執行Native方法。做用是類似的,也會有一樣的異常問題。在HotSpot虛擬機中,本地方法棧與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直接分配。 不管怎麼劃分,都不會改變堆存放對象實例的做用。各類劃分是爲了更好的分配和回收內存。 《Java虛擬機規範》規定,邏輯上連續的內存空間,在物理上能夠不連續。但多數虛擬機實現出於實現簡單和存儲高效,也會要求連續的物理內存空間。 主流Java虛擬機的堆內存空間都是可擴展的,但有上限值。當對象實例沒法被分配內存,且堆達到上限。Java虛擬機便會拋出OutOfMemoryError異常。
方法區(Method Area),運行時常量池(Runtime Constant Pool)
複製代碼
方法區,線程共享,存儲已被虛擬機加載的類型信息,常量,靜態變量,即時編譯器編譯後的代碼緩存等數據。 JDK8,再也不使用永久代(Permanent Generation Space)實現方法區,而是在本地內存中實現的元空間(Metaspace)來代替。而字符串常量移到Java堆。這部分的內存回收目標主要針對常量池的回收和對類型的卸載。 運行時常量池,存放常量池表(Constant Pool Table),即Class文件編譯期生成的各類字面量與符號引用。 根據《Java虛擬機規範》規定,若是方法區沒法知足新的內存分配,將會拋出OutOfMemoryError異常。
直接內存(Direct Memory). NIO(New input/output)是 JDK1.4新加入的類,引入了一種基於通道(channel)和緩衝區(buffer)的I/O方式,它可使用Native函數直接分配堆外內存,而後經過堆上DirectByteBuffer對象對這塊內存進行引用和操做。直接內存的大小不受JVM的限制,但一樣可能會OutOfMemoryError異常。
以HotSpot爲例,講述在Java堆中對象的建立、結構和訪問。
分配堆內存 根據內存是否規整,分爲兩種:指針碰撞(Bump The Pointer)和空閒列表(Free List).前者內存規整。 解決併發狀況下的線程安全問題的兩種方式
在《Java虛擬機規範》中規定,棧上的reference類型數據只是一個指向對象的引用。實際經過引用訪問對象有兩種方式:
模擬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]
複製代碼
HotSpot不區分虛擬機棧和本地方法棧,須要設置 -Xss。 -Xoss(設置本地方法棧)沒有效果。
複製代碼
兩種異常:
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)
...
複製代碼
在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
複製代碼
因爲字符串常量轉移到了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)
複製代碼
直接內存(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)
複製代碼
到目前爲止,明白了虛擬機中內存的劃分,以及出現內存溢出的場景。下一章將詳細講解Java垃圾收集機制如何避免內存溢出。
垃圾收集(Garbage Collection,GC),1960年麻省理工Lisp語言,使用動態內存分配和垃圾收集技術。 當Lisp胚胎時期,其做者John McCarthy就思考過GC須要完成的三件事情:
Java虛擬機內存運行時區間中,程序計數器、虛擬機棧、本地方法棧隨線程而生,隨線程而滅。在編譯器其大小基本肯定。而GC關注的是線程共享的區域 Java堆和方法區。
GC回收堆以前,如何斷定哪些對象須要被回收,或者說這些對象已死?
可固定做爲GC Roots的對象包括:
除固定外,還有臨時性的其餘對象等。
Java將引用分爲四種:
要宣告一個對象死亡,至少要經歷兩次不可達標記過程。重載對象的finalize()方法,能夠再第一次標記後執行,從新掛上引用鏈避免被回收,但finalize()只會被執行一次。
這裏咱們討論的是追蹤式垃圾收集。
分代收集(Generational Collection)的理論假說基礎:
絕大多數對象都是朝生夕滅的
熬過越屢次垃圾收集過程的對象越難以消亡
跨代引用相對同代引用來講,僅佔極少數
基於弱分代/強分代假說,通常Java虛擬機至少會把Java堆劃分爲
每次垃圾收集,新生代大量死去的對象會被回收,存活的對象將會逐步晉升到老年代中。大對象直接進入老年代。在新生代中創建一個全局的數據結構(記憶集,Remebered Set),把老年代分紅多個小塊,標識出某些塊存在跨代引用。 針對不一樣級別的分代的收集,咱們定義一下名詞:
標記-清除(Mark-Swap) 1960 Lisp John McCarthy,基礎性算法。 缺點:
1969 Fenichel "半區複製" SemisSpace Copying 解決內存碎片化和執行效率問題,缺點明顯:浪費一半內存。
IBM研究發現98%的新生代對象熬不過第一輪收集,所以不用1:1分配內存 1989年 Andrew Appel ,提出更優化的半區複製分代策略。 能夠看到始終有一個 Survivor(10%新生代內存)做爲保留,用來存放回收後存活對象,若果Survivor空間不夠,則須要老年代進行分配擔保(Handle Promotion) 標記-複製算法適用於新生代,即大量對象會被回收,須要複製的對象不多。老年代對象存活率高,就不適用了。
標記-整理(Mark-Compact) 1974年 Edward Lueders。 移動式回收算法,標記-清除是非移動式的。 移動對象則回收時複雜,不移動對象則分配內存時複雜。
全部垃圾回收算法在根節點枚舉時,都須要暫停用戶線程,即 Stop The World! HotSpot採用準確式(Exact)垃圾回收,使用稱爲OopMap的數據結構,記錄棧上本地變量到堆上對象的引用關係。 從而減小根節點枚舉耗費的大量時間。 找出棧上的指針/引用 介紹了保守式,半自動式,準確式垃圾回收,同時也引出了OopMap。
根節點枚舉須要暫停線程,總不能在每條指令後都去中斷線程,因此有些固定的指令位置,做爲中斷的點,稱之爲 safe point。採用主動式中斷,即達到安全點,檢查是否要執行中斷線程。 安全點的位置通常爲:
safe region。安全區域指在某個代碼片斷中,引用關係不會發生變化,在這個區域內能夠安全的開始垃圾收集。
RememberSet,記錄非收集區到收集區的引用,避免把整個非收集區加入到GC Root掃描。好比說收集新生代對象時,避免整個老年代加入GCRoot掃描。
複製代碼
從精度上來看,記憶集能夠分爲
卡表,即爲常見的卡精度的記憶集。 卡表簡單來講,能夠只是一個字節數組(Card Table)。每一個數組元素都對應一個卡頁(Card Page),卡頁是某塊特定大小的內存塊,通常來講大小爲2的N次冪字節數,HotSpot中爲512字節。只要卡頁中有對象存在跨代引用,則對應卡表元素標記爲1,即元素變髒(Dirty)
什麼時候去記錄RememberSet? 寫屏障(Write Barrier),虛擬機層面對"引用類型字段賦值"動做的AOP切面,虛擬機爲賦值操做生成相應指令。 環形(Around)通知,提供寫前屏障(Pre-Write Barrier)和寫後屏障(Post-Write Barrier)。
假設處理器的緩存行大小爲64字節,因爲一個卡表元素佔1個字節,64個卡表元素將共享同一個緩 存行。卡表在高併發下的僞共享(False Sharing)問題, 寫髒前,先判斷是否已髒。 在JDK 7以後,HotSpot虛擬機增長了一個新的參數-XX:+UseCondCardMark,用來決定是否開啓卡表更新的條件判斷
前面咱們經過OopMap、安全區域、RememberSet等手段,提高了根節點枚舉的速度。根節點枚舉帶來的停頓已經至關短暫和固定了,而從GC Roots繼續往下遍歷對象的停頓時間與堆容量成正比。 可達性分析(標記)算法要求全過程在一個一致性的快照中分析,勢必要凍結所有用戶線程,且凍結的時間徹底不可控。在堆容量過大狀況下,凍結時間是沒法接受的。所以,可達性分析過程,若是能與用戶線程併發執行,是最好不過了。 咱們先來看併發可達性分析過程 即三色標記 併發可達性分析又會引發兩類問題:
當且僅當同時知足下面兩個條件,會產生對象消失問題,即本來應當爲黑色的對象被誤標爲白色(Wilson,1994年證實):
在介紹以前咱們先明確幾個概念:
新生代,無並行,無併發,標記複製,簡單高效,額外內存消耗(Memory Footprint)最小,適用於單核/少核,JDK1.3.1以前。
新生代,並行,Serial的多線程版本,標記複製, [JDK1.3 - JDK8)
新生代,並行 ,無併發,JDK1.4,標記複製,注重吞吐量
老年代,無並行,無併發,Serial老年代版本,標記整理
老年代,並行,無併發,Parallel Scavenge 老年代版本,標記整理,注重吞吐量
Concurent Mark Sweep, 老年代,並行,併發, [JDK5 - JDK8],標記清除,注重低延遲, 併發標記使用增量更新。四個步驟:
CMS的三個明顯缺點:
Garbage First,JDK7完善, JDK9開始成爲默認垃圾收集器。 新生代,老年代。Region分區,局部標記-複製,總體標記-整理,注重低延遲,併發標記使用原始快照。 大對象(內存超過Region內存的一半)直接進入 Humongous Region區域。Region是回收的最小單元。每個Region均可以根據須要扮演Eden空間,Survivor空間或者老年代空間。可預測時間停頓模型。 四個步驟:
G1是垃圾收集器技術發展歷史上的里程碑式的結果,開創了面向局部手機的設計思路和基於Region的內存佈局形式。從G1開始,垃圾收集器的設計導向變爲追求應付內存分配速率(Allocation Tate),而不追求一次把整個Java堆清理乾淨。G1的更多介紹
略,實在不會
略,實在不會
代碼編譯的結果從本地機器碼變爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。
程序語言 --> 字節碼 --> 二進制本地機器碼
無關性的基石 -- 字節碼(Byte Code) 平臺無關性,語言無關性
Class文件以8個字節爲單位的二進制流,各數據項嚴格按照順序緊湊排列在文件中,中間沒有任何分隔符。 Class文件結構中只有兩種數據類型:
類型 | 名稱 | 數量 | 解釋 |
---|---|---|---|
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
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"
複製代碼
魔數(Magic Number) CAFEBABE ,表示這是一個Class類型文件,4字節。 minor version: 0(0x0000),2字節。 major version: 51 (0x0033),2字節。
接在魔數和版本後面的是常量池。 常量池中存放兩大類型常量:
常量池中的常量有17種類型,好比說 CONSTANT_Methodref_info,CONSTANT_Classref_info,CONSTANT_Utf8_info等等。 每種類型常量的結構也不近相同。共同點是,都已u1的tag開頭,表示類型。《深刻理解Java虛擬機》中,列出了完整的定義,下面簡單舉例。
CONSTANT_Methodref_info:
CONSTANT_Classref_info:
常量池項目數量:25 (0x001A是26,常量池索引值從1開始,0保留,因此實際只用25個常量,0能夠理解成不引用常量池中的項目)。
第一項(0A 00 04 00 16), #1 = Methodref #4.#22:
值得注意的是 父類方法類型是Methodref,類方法inc類型是utf8。
第二項(09 00 03 00 17) , #2 = Fieldref #3.#23 // clazz/TestClass.m:I
剩餘的23個項目,太多了,懂意思就行了。
結束了常量池25項解析,接着是Class訪問標誌(access_flags) 00 21 表示 0x0020 & 0x0001
方法數量,u2,0x0003,有3個方法 方法表結構
在方法表中,咱們遇到了一個屬性"Code",還有其餘屬性。
屬性名稱 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java代碼編譯成的字節碼指令 |
ConstantValue | 字段表 | 由final關鍵字定義的常量值 |
LocalVariableTable | Code屬性 | 方法的局部變量描述 |
SourceFile | 類文件 | 記錄類文件名稱 |
... | ... | ... |
解析工做就不作了。
Java虛擬機指令,單字節操做碼(OpCode)+操做數(Operand,零或多個)。面向操做數棧,而非寄存器。多數指令不包含操做數,指令參數放在操做數棧。 指令執行過程簡易僞代碼
do{
自動計算PC寄存器值加1;
根據PC寄存器指示位置,從字節碼流中取出操做碼;
if(字節碼存在操做數) 從字節碼流中取出操做數;
執行操做碼所定義的操做;
}while(字節碼流長度>0);
複製代碼
大多數指令都包含其操做要求的數據類型,例如iload,從局部變量表中加載int型數據到操做數棧中。 i表明int,l表明long,f表明float,a表明reference。 也有些指令跟數據類型無關,好比 goto。
其餘等等。。這章就瞭解下字節碼構成和指令。
類加載機制:從Class文件到內存中Java類型的過程。各個階段時間段上能夠有重疊。 類加載是在運行期間執行的,也描述爲動態加載和動態鏈接。
對於何時開始加載, 《Java虛擬機規範》沒有強制約束。可是嚴格規定了有且只有六種狀況,若是類沒有初始化,必須當即對類進行"初始化" (加載、鏈接必然會先執行),稱之爲主動引用:
不會觸發類初始化的幾個場景舉例:
//場景一 經過子類引用父類的靜態字段,不會致使子類初始化
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
}
複製代碼
加載(loading):從靜態文件到運行時方法區。完成三件事情:
使用Java虛擬機內置的引導類加載器,或者用戶自定義的類加載器。
確保字節流符合《Java虛擬機規範》的約束,代碼安全性問題驗證。驗證是重要的,但不是必須的。 四個階段:
一般狀況下,爲類變量(靜態變量),分配內存並設置初始值(零值)。初值並非代碼中賦的值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
複製代碼
將常量池內的符號引用替換爲直接引用的過程。 好比說這種,咱們要把 #2替換成實際的類引用,若是是未加載過的類引用,又會涉及到這個類加載過程。
getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
複製代碼
執行類構造器()方法,非實例構造器()方法 。 ()方法:執行類變量賦值語句和靜態語句塊(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虛擬機必須保證()方法在多線程環境下的同步問題。
實現「經過一個類的全限定名來獲取其二進制字節流」的代碼,稱之爲「類加載器」(Class Loader)。
類與其加載器肯定了這個類在Java虛擬機中的惟一性。
三層類加載器,絕大多數Java程序會用到如下三個系統提供的類加載器進行加載:
除了以上三個還有用戶自定義的加載器,經過集成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
複製代碼
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 類加載器
線程上下文類加載器(context class loader),能夠從java.lang.Thread中獲取。 雙親委派模型不能解決Java應用開發中遇到的全部類加載器問題。 例如,Java提供了不少服務提供者接口(Service Provider Interface,SPI),容許第三方提供接口實現。常見的SPI有JDBC,JCE,JNDI,JAXP等。SPI接口由核心庫提供,由引導類加載器加載。 而其第三方實現,由應用類加載器實現。此時SPI就找不到具體的實現了。 SPI接口代碼中使用線程上下文類加載器。線程上下文類加載器默認爲應用類加載器。
虛擬機是相對於物理機的概念。 物理機的執行引擎是直接創建在處理器,緩存,指令集合操做系統底層上。 虛擬機的執行引擎是創建在軟件之上,不受物理條件限制,定製指令集與執行引擎。 虛擬機實現中,執行過程能夠是解釋執行和編譯執行,能夠單獨選擇,或者混合使用。 但全部虛擬機引擎從統一外觀(Facade)來講,都是輸入字節碼二進制流,字節碼解析執行,輸出執行結果。
本章從概念角度講解虛擬機的方法調用和字節碼執行。
Java虛擬機以方法做爲最基本的執行單元。每一個方法在執行時,都會有一個對應的棧幀(Stack Frame) .棧幀同時也是虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。 一個棧幀須要多大的局部變量表,須要多深的操做數棧,早在編譯成字節碼時就寫到了方發表的Code屬性中。 Code: stack=2, locals=1, args_size=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);
}
複製代碼
操做數棧(Operand Stack) 字節碼指令讀取和寫入操做數棧。操做數棧中元素的數據類型必須與指令序列嚴格匹配。編譯階段和類檢驗階段都會去保證這個。 在大多數虛擬機實現中,上面棧幀的操做數棧與下面棧幀的局部變量會有一部分重疊,這樣不只節約了空間,重要的是在方法調用時直接公用數據,無須而外的參數複製。
在類加載過程當中,會把符號引用解析爲直接引用。方法調用指令以常量池中的符號引用爲參數。這些方法符號引用一部分在類加載或者第一次使用時轉化爲直接引用,這種轉化被稱爲靜態解析。另一部分則須要在每次運行期間轉化爲直接引用,這部分稱之爲動態鏈接。
正常調用完成和異常調用完成。 恢復主調方法的執行狀態。
Java虛擬機中的5條方法調用指令:
方法按照類加載階段是否能轉化成直接引用分類,能夠分爲:
非虛方法的調用稱之爲解析(Resolution),"編譯器可知,運行期不可變",即類加載階段把符號引用轉化爲直接引用。 而另一個方法調用的方式稱之爲分派(Dispatch)。
分派(Dispatch)是靜態或者動態的,又或者是單分派或者多分派。重載或者重寫會出現同名方法。同名方法的選擇,我能夠稱之爲分派
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 Human;)V, 實際執行的都是sayHello(Human)。而不是sayHello(Man)或者sayHello(Woman)。 這裏涉及到兩個類型:
編譯期並不知道對象的實際類型,因此按照對象的靜態類型去分派方法。
與重寫(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對象的實際類型分派方法。
小知識:字段永遠不參與多態,方法中訪問的屬性名始終是當前類的屬性。子類會遮蔽父類的同名字段
方法的宗量:方法的接收者與方法的參數 單分派:基於一種宗量分派 多分派:基於多種宗量分派。 當前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
虛方法表,接口方法表,類型繼承分析,守護內聯,內聯緩存
動態類型語言的關鍵特徵:類型檢查的主體過程是在運行期而不是編譯器,好比說Groovy、JavaScript、Lisp、Lua、Python。 靜態類型語言:編譯器就進行類型檢查,好比C++,Java。
Java虛擬機須要支持動態類型語言,因而在JDK7發佈 invokedynamic指令。
略
略
略
略
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,整型結果返回給方法調用者
複製代碼
先明確幾個概念 即時編譯器(JIT編譯器,Just In Time Compiler),運行期把字節碼變成本地代碼的過程。 提早編譯器(AOT編譯器,Ahead Of Time Compiler),直接把程序編譯成與目標及其指令集相關的二進制代碼的過程。
這裏討論的「前端編譯器」,是指把*.java文件轉換成*.class文件的過程,主要指的是javac編譯器。
Javac編譯器是由Java語言編寫。分析Javac代碼的整體結構來看,編譯過程大體分爲1個準備過程和3個處理過程。以下:
若是註解處理產生新的符號,又會再次進行解析填充過程。 Javac編譯動做的入口com.sun.tools.javac.main.JavaCompiler類。代碼邏輯主要所在方法compile(),compile2()
對應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類表示。 後續的操做創建在抽象語法樹之上。
對應enterTree()方法。
JDK6,JSR-269提案,「插入式註解處理器」API。提早至編譯期對特定註解進行處理,能夠理解成編譯器插件,容許讀取、修改、添加抽象語法樹中的任意元素。若是產生改動,編譯器將回到解析及填充符號表過程從新處理,直到不產生改動。每一次循環過程稱爲一個輪次(Round). 使用註解處理器能夠作不少事情,譬如Lombok,能夠經過註解自動生成getter/setter方法、空檢查、產生equals()和hashCode()方法。
抽象語法樹可以表示一個正確的源程序,但沒法保證語義符合邏輯。語義分析的主要任務是進行類型檢查、控制流檢查、數據流檢查等等。 例如
int a = 1;
boolean b = false;
char c = 2;
//後續可能出現的運算,都是能生成抽象語法樹的,但只有第一條,能經過語義分析
int d= a + c;
int d= b + c;
char d= a + c;
複製代碼
在IDE中看到的紅線標註的錯誤提示,絕大部分來源於語義分析階段的結果。
attribute()方法,檢查變量使用前是否已被聲明,變量與賦值的數據類型是否匹配等等。 3個變量的定義屬於標註檢查。標註檢查順便會進行極少許的一些優化,好比常量摺疊(Constant Folding).
int a = 1 + 2; 實際會被摺疊成字面量「3」
複製代碼
flow()方法,上下文邏輯進一步驗證,好比方法每條路徑是否有返回值,數值操做類型是否合理等等。
語法糖(Syntactic Sugar),編程術語 Peter J.Landin。減小代碼量,增長程序可讀性。好比Java語言中的泛型(其餘語言的泛型不必定是語法糖實現,好比C#泛型直接有CLR支持),變長數組,自動裝箱拆箱等等。 解語法糖,編譯期將糖語法轉換成原始的基礎語法。
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指令,作了一個類型轉換
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#與Java的選擇不一樣,主要是C#當時才2年遺留老代碼少,Java快10年了老代碼多。類型擦除是偷懶留下的技術債。
類型擦除除了前面提到的編譯後都變成了統一的裸類型以及使用時的類型檢查和轉換以外還有其餘缺陷。 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>();
複製代碼
2014年,Oracle,Valhalla語言改進項目內容之一,新泛型實現方案
自動裝箱,自動拆箱,遍歷循環,變長參數,條件編譯,內部類,枚舉類,數值字面量,switch,try等等。
前面一章講的是從*.java到*.class的過程,即源碼到字節碼的過程。 這一章講的是從二進制字節碼到目標機器碼的過程,分爲兩種即時編譯器和提早編譯器。
目前主流的兩款商用Java虛擬機(HotSpot、OpenJ9)裏,Java程序最初都是經過解釋器 (Interpreter)進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁,就會把這些代碼認 定爲「熱點代碼」(Hot Spot Code),爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些代 碼編譯成本地機器碼,並以各類手段儘量地進行代碼優化,運行時完成這個任務的後端編譯器被稱 爲即時編譯器。
於程序運行的資源,
及須要一段時間預熱後才能到達最高性能的問題。這種提早編譯被稱爲動態提早編譯(Dynamic AOT)或者索性就大大方方地直接叫即時編譯緩存(JIT Caching)
Android虛擬機歷程: Android4.4以前 Dalvik虛擬機 即便編譯器 Android4.4開始 Art虛擬機 提早編譯器,致使安裝時須要編譯App,很是耗時,但運行性能獲得提高 Android7.0開始 從新啓用解釋執行和即時編譯,系統空閒時間時自動進行提早編譯。
略
介紹虛擬機如何實現多線程,多線程之間因爲共享數據而致使的一系列問題及解決方案
介紹Java虛擬機內存模型前,先了解下物理機的併發問題。
Java虛擬機有本身的內存模型,也會有與物理機類型的問題。
Java內存模型規定:因此變量都存儲在主內存(Main Memory)中,線程有本身的工做內存,工做內存保存變量在主內存副本。線程對變量的讀寫只能再工做內存(Working Memory)中,線程間共享變量須要經過主內存完成。 JVM內存模型的執行處理將圍繞解決兩個問題展開:
主內存與工做內存的交互協議定義以下操做,Java虛擬機必須保證這些操做是原子性的。
若是要把變量從主內存拷貝到工做內存,必須順序執行 read和load,但不要求必定連續。 若是要把變量從工做內存同步到主內存,必須順序執行 store和write,但不要求必定連續。
Java內存模型是圍繞着在併發過程當中如何處理這3個特性來創建的,歸根結底是爲了實現共享變量在多個工做內存中的一致性,以及併發時,程序能如期運行。
happens-before
內存屏障是被插入到兩個CPU指令之間的一種指令,用來禁止處理器指令發生指令重排序。
volatile主要有下面兩種語義
保證了不一樣線程對該volatile型變量操做的內存可見性,但不等同於併發操做的安全性
volatile型變量使用場景總結起來就是"一次寫入,處處讀取",某個線程負責更新變量,其餘線程只讀取變量,並根據變量新值執行相應邏輯,例如狀態標誌位更新,觀察者模型變量值發佈