Java基礎知識——JVM基礎

JVM基礎

jvm:運行在操作系統上的假想的計算機
1:Java文件的編譯和解釋
2:Java線程
3:JVM組成
4:GC回收算法
5:Java引用
6:GC垃圾收集器
7:Java IO/NIO
8:類加載

兩大無關性

平臺無關性:
每一臺平臺解釋器不同,但是虛擬機相同,跨平臺的原因。
一個程序對應一個虛擬機,多個程序對應多個虛擬機, 虛擬機之間數據不共享
什麼是平臺:操作系統及其硬件環境。跨平臺即代碼的運行不依賴於操作系統及其硬件環境。

c語言不是跨平臺的,因爲不同操作系統下的編譯器編譯代碼得到不同文件,不能在其他操作系統上運行。即由Windows編譯器得到的exe文件不能在Linux上運行
在這裏插入圖片描述
Java跨平臺:因爲編譯後都得到class文件,可以在不同的操作系統上的解釋器上執行。
並且注意:
不同的系統下有不同的JVM,所以JVM不是跨平臺
JAVA依賴於JVM,JVM給JAVA提供了運行環境,所以JAVA是跨平臺的。

在這裏插入圖片描述
程序執行的方式:
第一是編譯執行:C它把源程序由特定平臺的編譯器一次性編譯爲平臺相關的機器碼,優點是執行速度快,缺點是無法跨平臺;
第二是解釋執行,如HTML,JavaScript,它使用特定的解釋器,把代碼一行行解釋爲機器碼,類似於同聲翻譯,它的優點是可以跨平臺,缺點是執行速度慢,暴露源程序;
第三種是先編譯後解釋,Java

Java源文件——編譯器javac——class文件(字節碼文件,二進制文件)
字節碼文件——解釋器(jvm)java——機器碼文件

關於JVM的解釋過程:
參考資料
第一種:解釋執行:即逐條將字節碼翻譯成機器碼並執行 [在解釋執行過程中,每當爲 Java 方法分配棧楨時,Java 虛擬機往往需要開闢一塊額外的空間作爲操作數棧,來存放計算的操作數以及返回結果]
第二種:即時編譯(Just-In-Time compilation,JIT),利用JIT即時編譯器,一個方法中包含的所有字節碼編譯成機器碼後再執行,並且會儲存下次再運行時無需編譯。

前者的優勢在於無需等待編譯,而後者的優勢在於實際運行速度更快。HotSpot 默認採用混合模式,綜合瞭解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而後將其中反覆執行的熱點代碼,以方法爲單位進行即時編譯。
注意即時編譯和編譯是不同的
在這裏插入圖片描述

語言無關性:JVM不僅能運行Java文件,還能運行入JRuby,Jython,Scala等。
關鍵是class文件,JVM要求class文件必須實現一系列規範,但是並沒有要求必須Java編譯,其他語言也可以編譯成class文件在這裏插入圖片描述

線程

jvm線程和原生操作系統線程具有直接映射關係

虛擬機線程 (VM thread)
這個線程等待 JVM 到達安全點操作出現。這些操作必須要在獨立的線程裏執行,因爲當堆修改無法進行時,線程都需要 JVM 位於安全點。這些操作的類型有:stop-the- world 垃圾回收、線程棧 dump、線程暫停、線程偏向鎖(biased locking)解除。

週期性任務線程
這線程中斷事件,用來調度週期性操作的執行。

GC 線程
這些線程支持 JVM 中不同的垃圾回收活動。

編譯器線程
這些線程在運行時將字節碼動態編譯成本地平臺相關的機器碼。

信號分發線程
這個線程接收發送到 JVM 的信號並調用適當的 JVM 方法處理

jvm內存區域

執行引擎:編譯器,垃圾收集
數據:
共享數據區:方法區,堆
私有數據區:虛擬機棧,本地方法棧,程序計數器
本地庫接口 用於連接本地方法庫
在這裏插入圖片描述
線程私有數據生命週期和操作系統線程相同
線程共享區域和虛擬機生命週期相同

私有數據
程序計數器:
是當前線程所執行的字節碼的行號指示器。
在解釋執行中,因爲是從頭開始的。如果存在多線程併發執行,但線程被掛起時,需要記錄正在執行的代碼,保存在程序計數器中。
如果是執行Java代碼,那麼就是正在執行的虛擬機字節碼的地址,如果是Naive方法,那麼爲空

虛擬機棧:
由棧幀組成,每一個方法對應一個棧幀,棧幀隨着方法創建而創建,隨着方法銷燬而銷燬。
棧幀由局部變量表、操作數棧、動態鏈接、方法出口
1、局部變量表:是一組變量值的存儲空間。
包括方法參數
方法內的變量:基本數據類型,對象的引用reference不是對象本身
returnAddress類型(一條字節碼執行的地址,異常處理,現很少使用)
是以32位的slot進行存儲,如果遇到64位則高位補齊的2個slot。以索引表的形式訪問

2、操作數棧:常稱爲操作數棧,是一個後入先出棧。
用途:剛開始爲空,執行某些用途時,會將局部變量表中的數據拷貝到操作數棧
算術運算
調用其他的方法進行參數傳遞
在概念模型中,兩個棧幀是相互獨立的。但是大多數虛擬機的實現都會進行優化,令兩個棧幀出現一部分重疊。令下面的部分操作數棧與上面的局部變量表重疊在一塊,這樣在方法調用的時候可以共用一部分數據,無需進行額外的參數複製傳遞。

3、動態連接:棧幀持有一個指向 方法區常量池 中所屬方法的引用明確調用的是哪一個方法

4、方法返回地址:方法的返回分爲兩種情況
一種是正常退出,退出後會根據方法的定義來決定是否要傳返回值給上層的調用者
一種是異常導致的方法結束,這種情況是不會傳返回值給上層的調用方法.
不過無論是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置

本地方法棧:Native 方法服務

共享數據:
堆:創建的對象本身和數組都保存在 Java 堆內存中
數組同樣是對象 int a[]=new int [3] 所以一切new的對象本身都在堆中
對象又包括成員變量和方法,成員變量在堆中

本地方法區:我們常說的永久代(Permanent Generation), 用於存儲被 JVM 加載的類信息class、常量(final ,“abc”)、靜態變量(static)、即時編譯器編譯後的代碼等數據
注意常量和靜態變量在jdk1.7後放入堆中

在這裏插入圖片描述
首先明確,常量池在解釋之前,準備階段就已經初始化
注意常量池中有基本數據類型的值:
對於int,都會放入常量池,但是如果int值過於簡單如1,不會放入常量池而是通過JVM字節碼將值賦值給相應字段
對於Integer,利用了享元技術,值在(-128,127)會放入常量池,而如果超出範圍,會自動裝箱從而保存在堆中。
參考資料
符號引用其實是從字節碼角度來標識類、方法、字段。字節碼只有加載到內存中才能運行,加載到內存中,就是內存尋址了。
符號引用:
在class文件中不會保存各個方法、字段的最終內存佈局信息,因此這些字段、方法,類就如果不經過運行期轉換的話無法得到真正的內存入口地址,只能通過字節碼的形式保存也就是符號引用。
符號引用轉換爲直接引用:
加載驗證準備解釋中的解釋的時候:靜態鏈接
運行的時候,通過虛擬機棧的棧幀中的動態鏈接:動態鏈接
參考資料

運行時常量池:存在於內存中,是靜態常量池被加載這裏的加載時解釋的一部分到內存之後的版本
class常量池和運行時常量池的區別:
1:先後區別
2:JVM隊靜態常量池有嚴格的規定,對運行時常量池沒有嚴格規定
3:動態性
靜態常量池不具有動態性,不可改變
運行時常量池可以改變:可以利用String的Intern方法將String儲存到常量池中
例子:在JDK1.7之前返回false,之後返回true(因爲之後將常量池移到了堆中)參考資料

String s3 = new String("123") + new String("123");
s3.intern();
String s4 = "123123";
System.out.println(s3 == s4);

Java堆的內存回收機制

因爲私有空間在線程結束後自動釋放不需要回收
本地方法區回收效率低其實也有回收方法
所以一般回收都是對堆回收

如何確定垃圾

引用計數法:引用計數爲0則說明是可回收對象。一般不不用
1:可能有循環引用的問題,如A引用B,B飲用A,則AB無法被回收
2:存在四種引用類型,單純的引用計數法無法表示
可達性分析:如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。
能作爲GC roots對象的:
1:虛擬機棧中
2:本地方法棧中Navie
3:本地方法區中靜態對象
4:本地方法區中常量對象

第一次標記後也不一定死亡
當第一次標記後對象沒有在引用鏈上,則會進入F-Queue中並調用finalize()方法
調用finalize()後如果仍然沒有在引用鏈上,則會真正死亡
所以說可以利用finalize()進行標記,將自己(this)賦值給某個標記類的成員變量
注意finalize()一個類僅僅能調用一次,第二次進入F-Queue後會被回收

GC算法

標記清除:標記後清楚
複製:分爲兩塊。一塊使用一段時間後,將存活的複製到另一塊上
標記整理標記後將回收對象聚攏移動,並且消除邊界以外的對象
分代收集算法:
Generational Collecting
新生代(1/3):Eden(8/10) 、ServivorFrom(1/10)、ServivorT(1/10)
老年代(2/3):
1:Eden
當 Eden 區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。
2:ServivorFrom
上一次 GC 的倖存者,作爲這一次 GC 的被掃描者。
3:ServivorTo
保留了一次 MinorGC 過程中的倖存者

MinorGC:複製算法
1:eden、servicorFrom 複製到 ServicorTo,年齡+1
首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域,同時年齡+1
如果有對象的年齡以及達到了老年的標準,則賦值到老年代區
如果 ServicorTo 不夠位置了就放到老年區
2:清空 eden、servicorFrom
清空 Eden 和 ServicorFrom 中的對象;
3:ServicorTo 和 ServicorFrom 互換
ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom沒有進行復制
//算法導致實際只能使用新生代百分之90的空間

爲什麼要有ServivorTo區域?爲什麼要有兩個ServivorTo區域

老年代:存放穩定的對象。
MinorGC,使得有新生代的對象晉身入老年代,當空間不夠用時觸發 MajorGC。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。
MajorGC :
採用標記清除算法/ 標記整理算法:
首先掃描一次所有老年代,標記出存活的對象,然後回收沒有標記的對象。MajorGC 的耗時比較長,因爲要掃描再回收。MajorGC 會產生內存碎片,爲了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。

永久代:指內存的永久保存區域,GC 不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨着加載的 Class 的增多而脹滿,最終拋出 OOM 異常

元數據區:
在 Java8 中,永久代已經被移除,被一個稱爲“元數據區”(元空間)的區域所取代。
元空間的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池(常量池)和類的靜態變量放入 java 堆中,這樣可以加載多少類的元數據就不再由 MaxPermSize 控制, 而由系統的實際可用空間來控制

數據存放規則:
1:優先使用新生代
2:大對象直接進入老年代(編程的時候避免短命大對象)
3:年齡足夠後進入老年代MaxTenuringThreshold
4:當Sevivor同年齡的對象佔用空間大於一半,那麼大於或者等於此年齡的對象進入老年代
5:空間分配擔保。
如果一次minorGC得到的剩餘對象大於Sevivor,那麼多餘的會進入老年代。
但如果老年代空間也不足夠,那麼這次minorGC就是有風險的。
判斷老年代的剩餘空間是否大於歷次晉升到老年代的平均大小
如果大於則還是minorGC
如果小於則full GC

引用

Java中的四種引用類型:
//創建一個引用,引用可以獨立存在,並不一定需要與一個對象關聯
String s;

1:強引用FinalReference
String str = new String(“abc”);
只要強引用存在,垃圾回收器將永遠不會回收被引用的對象,哪怕內存不足時,JVM也會直接拋出OutOfMemoryError
如果想中斷引用關係str=null 此時JVM就可以適時回收內存

2:軟引用:在內存足夠的時候,軟引用對象不會被回收,只有在內存不足時,系統則會回收軟引用對象
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);

3:弱引用:無論內存是否足夠,只要 JVM 開始進行垃圾回收,那些被弱引用關聯的對象都會被回收
byte[] buff = new byte[1024 * 1024];
WeakReference<byte[]> sr = new WeakReference<>(buff);

4:虛引用:如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,它隨時可能會被回收,用 PhantomReference 類來表示

GC垃圾收集器

Stop-The-World(STW):所有正在工作的線程停止,主要是GC引起的
OOPMAP:什麼偏移量上是什麼數據,理解爲不需要遍歷整個上下文,就知道引用了哪些對象和數據隨着指令執行隨時改變

安全點(Safepoint)OOPMAP不會更改的點
即程序執行時並非在所有地方都能停頓下來開始GC,只有到達安全點纔會暫停。

安全區域(Safe Region):引用關係不會更改的點。OOPMAP不會更改
如果程序在sleep或者blocked則無法進入安全點。

中斷的方式:
搶先式中斷(Preemptive Suspension):在GC發生時,首先把所有線程全中斷,若發現有線程中斷的地方不在安全點上,就恢復線程,讓他跑到安全點上。(此方式幾乎沒有虛擬機還在採用)

主動式中斷(Voluntary Suspension):當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單的設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起輪詢標誌的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

新生代垃圾收集器:Serial,ParallelNew,Parallel Scavenge
老年代垃圾收集器:SerialOld,ParallelOld,CMS
新生代:複製算法
Serial:單線程
使用一個 CPU 或一條線程去完成垃圾收集工作,並且在進行垃圾收集的同時,必須暫停其他所有的工作線程,直到垃圾收集結束
沒有線程交互的開銷,可以獲得最高的單線程垃圾收集效率
Client 模式下默認的新生代垃圾收集器(簡單)

ParallelNew:多線程
開啓和 CPU 數目相同的線程數,仍然要暫停其他所有工作的線程
Server 模式下新生代的默認垃圾收集器

Parallel Scaveng:多線程:
其他關注的是用戶線程的停頓時間
關注的是程序達到一個可控制的吞吐量(吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間))
高吞吐量可以最高效率地利用 CPU 時間,儘快地完成程序的運算任務,主要適用於在後臺運算而不需要太多交互的任務。

年老代:標誌整理算法
SerialOld:單線程

ParallelOld:多線程

CMS:最主要目標是獲取最短垃圾回收停頓時間。標記-清楚算法
分爲幾個階段:
初始標記: 標記GC Roots能直接關聯的對象。 會STW
併發標記:從初始標記對象開始標記所有可達對象。不會STW
併發預處理:標記新對象,即新生代晉升對象等對象。不會STW
目的是爲了標記由於程序執行新引用的對象,再給你一次 機會
重標記(remark):從GC Roots開始掃描對象進行可達性分析,標記可回收對象。會STW
併發清理:回收可回收的對象。不會STW

G1:是優先處理那些垃圾多的內存塊的意思
在這裏插入圖片描述
特點一:不是劃分爲三大塊,而是很多小塊。使得回收工作更加並行化
特點二:G1能夠讓用戶設置應用的暫停時間。
因爲他選擇一些內存塊,而不是整代內存來回收,到底回收多少內存就看用戶配置的暫停時間,配置的時間短就少回收點,配置的時間長就多回收點,伸縮自如

JVM加載機制

類加載機制

Java語言的類型加載,連接,初始化都是程序運行期間完成的
動態擴展語言
1:面向接口的程序可以在運行時才指定實現類接口回調,即面向接口編程
參考資料
2:本地程序可以從其他地方加載二進制文件作爲程序代碼的一部分

什麼時候進行類加載?
1: new等
2:當加載子類發現父類還沒有加載
3:反射機制
4:虛擬機剛啓動時,加載主類(main)

類加載過程:
加載——連接(驗證準備解析)——初始化,使用,卸載整個過程稱爲類加載

一:加載
1:獲取class的二進制流因爲這裏沒有限定在哪獲取,所以如果從zip中就是jar,從網絡就是Applet等,增加擴展性
2:class文件讀入方法區
3:生成一個各種數據訪問入口的 java.lang.Class 對象(本地方法區)

類信息:編譯後形成class文件 二進制流
類似於C語言結構題的僞結構,兩種數據類型:無符號數和表
1:前四個字節是魔數,用於進行驗證是否是可以使用的class文件
2:class常量池(constant pool table),用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References);
3:訪問標誌 public class或者interface
4:類索引(類的全限定名),父親索引,接口索引
5:字段表集合 包括實例變量
每一個實例變量,都包含下面五個字段
在這裏插入圖片描述
access_flags代表的是private/public等值,可以從下面中列舉了標誌值和對應的含義
在這裏插入圖片描述
而name_index和descriptor_index指向常量表,是成員變量的類型和值因爲成員變量的類型和值無法分類的,所以交給常量池去刻畫。而public/trasient是有限集合可以分類

6:方法表。與常量表非常類似,修飾符可以定義,而名稱等需要藉助常量池。而方法的代碼,需要藉助7的Code
7:屬性表集合
Code,方法裏的方法經過編譯後生成字節碼文件存儲在其中

二:驗證
確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,並且不會危害虛擬機自身的安全

三:準備
爲類變量和常量分配內存並設置初始值階段,即在方法區中分配這些變量所使用的內存空間。
注意這裏所說的初始值概念,類變量定義爲:
public static int v = 8080;
實際上變量 v 在準備階段過後的初始值爲0而不是8080,將 v 賦值爲 8080 的 put static指令是程序被編譯後,存放於類構造器方法之中。
但是注意如果聲明常量:
public static final int v = 8080;
在編譯階段會爲 v 生成 ConstantValue 屬性,在準備階段虛擬機會根據 ConstantValue 屬性將 v 賦值爲 8080

四:解析靜態鏈接
常量池的部分符號引用轉化爲直接引用(主要是私有方法和靜態方法)

符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現。符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中。
在Java中,一個java類將會編譯成一個class文件。在編譯時,java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際內存地址,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。
CONSTANT_Class_info類或接口的解析:就將此類的全限定名給類加載器去加載
CONSTANT_Fieldref_info:通過對應的CONSTANT_Class_info的字段表進行解析
其他的同理

直接引用:我理解的是具體的字段表,方法表或者類的全限定名
直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中

五:初始化:
真正執行類中定義的 Java 程序代碼,執行 類構造器< clinit>方法。理解執行靜態變量賦值和靜態語句塊
1:< clinit>方法是由編譯器自動收集類中的靜態變量的賦值操作和靜態語句塊中的語句合併而成的。
2:虛擬機會保證子方法執行之前,父類的方法已經執行完畢
3:如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器可以不爲這個類生成()方法。

類加載器

1:啓動類加載器(Bootstrap ClassLoader)
負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath 參數指定路徑中的,且被虛擬機認可(按文件名識別,如 rt.jar)的類。

2:擴展類加載器(Extension ClassLoader)
負責加載 JAVA_HOME\lib\ext 目錄中的,或通過 java.ext.dirs 系統變量指定路徑中的類庫。

3:應用程序類加載器(Application ClassLoader):
負責加載用戶路徑(classpath)上的類庫。

每一個類加載器都有獨立的名稱空間,即兩個不同的類加載器加載同一個class文件,兩個是獨立的類

雙親委派:
當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成其中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class),子類加載器纔會嘗試自己去加載。
採用雙親委派的一個好處是比如加載位於 rt.jar 包中的類 java.lang.Object,不管是哪個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個 Object 對象。在這裏插入圖片描述

HotSpot虛擬機的對象的創建

TLAB:參考資料
init:參考資料
1:在常量池中是否有一個類的符號引用,且是否被加載等如果沒有則類加載
2:分配內存(從Java堆中劃分一塊內存)類加載完成後確定大小,因爲class含有字段表,可以計算實際佔的空間
首先判斷有沒有使用TLAB(每個線程單獨擁有空間),如果沒有
利用CAS操作保證原子性
具體劃分有兩種方法:區別在於Java堆是否有壓縮功能
指針碰撞:當堆中內存規整時,只需要移動分界指針
空閒列表:當堆中內存不規整,需要遍歷空閒列表
3:內存初始化爲0
4:根據對象頭獲取設置必要信息
5:調用init方法,實例構造器我理解爲構造方法,並且是如果存在父類那麼先吊用父構造

堆中的對象:三部分組成:對象頭,實例數據,對齊填充
對象頭:
1:mark word 包括哈希,GC年齡,鎖等
2:類型指針,指向元數據即哪個類的實例指針本地方法區?

如何訪問對象?
我們知道虛擬機棧——棧幀——局部變量表——reference對象的引用
通過對象的引用訪問,有兩種方式
1:通過句柄池,每一個句柄有兩個指針,一個指向實例數據,一個指向類型。在這裏插入圖片描述2:直接訪問。直接指向Java堆中的對象在這裏插入圖片描述
句柄池優勢在於不用修改reference進行移動,直接訪問優勢在於速度快

方法調用

一個方法相當於一個棧幀,方法的執行和完成對應棧幀的入棧和出棧。
首先明確,棧幀屬於線程私有屬性,爲每線程所私有。
所以只有位於頂端的棧幀是有效的在這裏插入圖片描述

方法調用:注意到編譯期間方法都是符號引用,不知道具體的虛擬地址,需要轉換

靜態鏈接=解析:類加載階段的鏈接 符合“編譯時可知,運行時不可變”。 靜態方法和私有方法,實例構造器,父類方法——非虛方法
動態鏈接=分派:實際運行鏈接——虛方法
Human man =new man();
上述的代碼,分爲兩個部分。Human是靜態類型,man是動態類型。
靜態類型是編譯器可以知道的,不會改變的確定是在編譯階段,JVM不知道
動態類型是可以改變,只有在實際運行時才能知道
靜態分派:
使用場景:方法重載(OverLoad)
由靜態類型確定使用的方法
例子:
public class Test {

// 類定義
    static abstract class Human { 
    } 
 
// 繼承自抽象類Human
    static class Man extends Human { 
    } 
 
    static class Woman extends Human { 
    } 
 
// 可供重載的方法
    public void sayHello(Human guy) { 
        System.out.println("hello,guy!"); 
    } 
 
    public void sayHello(Man guy) { 
        System.out.println("hello gentleman!"); 
    } 
 
    public void sayHello(Woman guy) { 
        System.out.println("hello lady!"); 
    } 

// 測試代碼
    public static void main(String[] args) { 
        Human man = new Man(); 
        Human woman = new Woman(); 
        Test test = new Test(); 

        test.sayHello(man); 
        test.sayHello(woman); 
    } 
}

// 運行結果
hello,guy! 
hello,guy!

動態分派:
使用場景:方法重寫(Override),一般的方法執行
由動態類型確定,先去動態類型的方法段尋找,沒找到則依次去找父類,一旦找到則返回
所以若子類重寫父類方法,執行子類
若子類繼承父類方法,執行父類
例子:

// 定義類
    class Human { 
        public void sayHello(){ 
            System.out.println("Human say hello"); 
        } 
    } 
 
// 繼承自 抽象類Human 並 重寫sayHello()
    class Man extends Human { 
        @Override 
        protected void sayHello() { 
            System.out.println("man say hello");  
        } 
    } 
 
    class Woman extends Human { 
        @Override 
        protected void sayHello() { 
            System.out.println("woman say hello"); 
        } 
    } 

// 測試代碼
    public static void main(String[] args) { 
        // 情況1
        Human man = new man(); 
        man.sayHello(); 
        // 情況2
        man = new Woman(); 
        man.sayHello(); 
    } 
}
// 運行結果
man say hello
woman say hello
// 原因解析
// 1. 方法重寫(Override) = 動態分派 = 根據 變量的動態類型 確定執行(重寫)哪個方法
// 2. 對於情況1:根據變量(Man)的動態類型(man)確定調用man中的重寫方法sayHello()
// 3. 對於情況2:根據變量(Man)的動態類型(woman)確定調用woman中的重寫方法sayHello()

早期優化

編譯的過程:
1:解析與填充符號表(詞法分析語法分析):
詞法分析:將字符流變爲標記集合 int a=b+2就爲6個token
語法分析:根據token序列構造標記語法樹
填充符號表:生成符號表(符號信息和符號地址)
2:註解處理
3:分析與字節碼生成
所有說一個new的類,會在編譯期間就生成一個對應class類文件(格式看前述),而如果沒有導入對應的包,將無法構成文件並在編譯期間出錯。
但是如果利用反射,編譯期間會將起看錯字符串,則不會在編譯期間出錯。但是動態編譯時即運行時會出錯

關鍵點:
1:語法糖(3:分析與字節碼生成)
虛擬機不支持,在編譯階段還原回簡單的基礎語法,僅方便程序員編程 例如a[i]=(a+i)
泛型和泛型擦除
C++中的泛型是真泛型List< String>和List< Integer>是兩種不同的類型
而Java中是僞泛型,僅僅在源代碼中存在,而在字節碼中會還原成原類型,只不過在使用時會自動添加強轉
在這裏插入圖片描述
自動裝箱和拆箱
List< Integer>中add(1)實際上是add(Integer.valueOf(1))自動裝箱
int I=get(3)實際上是int I=((Integer)get(3)).intValue()自動拆箱
forEach循環
for(int I:list){sum+=I};實際上是
在這裏插入圖片描述
條件編譯
C++的#ifdef實現
Java 利用if實現,需要if裏面是常量
在這裏插入圖片描述

2:生成字節碼 添加實例構造器init和類構造器clinit且按順序執行
注意變量初始化塊是按順序初始化
init:1.父類變量初始化塊 2.父類語句塊 3.父類構造函數 4.子類變量初始化塊 5.子類語句塊 6.子類構造函數
clinit :1.父類靜態變量初始化 2.父類靜態語句塊 3.子類靜態變量初始化 4.子類靜態語句塊
所以當加載並實例化一個類
執行順序
   (1) 父類靜態對象和靜態代碼塊 父親的類構造器clinit
   (2) 子類靜態對象和靜態代碼塊 構造器clinit
   (3) 父類非靜態對象和非靜態代碼塊父親的init
   (4) 父類構造函數父親的init
   (5) 子類 非靜態對象和非靜態代碼塊init
   (6) 子類構造函數init

晚期優化

即時編譯

解釋:快速啓動,不佔內存
編譯:執行效率高。且編譯時間越久代碼執行效率一般越高
JIT:Client Compiler C1編譯器和Server Compiler C2編譯器
編譯方式:混合模式 解釋模式 編譯模式
分層編譯:代碼可能會多次編譯
第0層:解釋執行,編譯器不開啓(Profiling)
第1層: C1編譯,短時間優化
第2層:C2編譯,長時間優化

如何進行?

熱點代碼:
被多次調用的方法,被多次執行的循環體
第一種是標準的
第二種仍然會將整個方法作爲編譯對象,被稱爲棧上替換(On Stack Replacement OSR)
熱點探測:
基於採樣:當週期性檢測棧頂
基於計數器:當調用達到一定次數
方法調用計數器(1:探測方法 2:會衰減,擁有半衰週期)和回邊計數器(1:探測循環不會衰減)
在這裏插入圖片描述

優化技術

1:公共子表達式消除:
int d=(ca)12+a+(b+ac)
優化爲int d=E
12+a+(E+a)
2:邊界檢查消除:在編譯期間通過字節流分析進行判斷從而消除上下界判斷
3:方法內聯:
final用於聲明屬性方法和類,分別表示屬性不可變,方法不可覆蓋,類不可繼承
如果有final直接內聯
非虛方法直接內聯
虛方法:類型集成關係分析CHA等措施
這裏體現出JIT是一種激進優化:即有可能內聯錯誤的函數,當發現錯誤的時候,重現編譯或者解釋執行
4:逃逸分析:
對象的作用域
方法逃逸:作爲調用參數進入其他方法
線程逃逸:作爲類變量
如果是非逃逸的對象,可以理解爲對象的作用域就在這個方法內
棧上分配:將對象分配在棧上
同步消除:對於不會線程逃逸的變量,可以消除鎖
標量替換:用基本數據類型(標量)來替換對象(聚合量)即不創建這個對象,而是創建起成員變量

與C++編譯器的對比:
1:即時編譯佔用的用戶時間,而靜態編譯已經編譯好了(即時)
2:Java是動態的類型安全語言,隨時檢查如上下界,類型轉換等(安全)
3:面向對象,多態性。方法內聯時花費更多時間(多態)
4:動態擴展語言。運行時加載,難以全局優化(動態)
5:Java對象分配在堆上,C++既可以在堆上也可以在棧上(棧上自動回收,堆上程序員決定)(分配)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章