一、jvm數據區域
1.Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。
程序計數器:
(1) java多線程中是通過線程切換的來實現的,在切換到下一個線程過程中需要記錄當前線程的正在執行的虛擬機字節碼指令地址(執行Native方法時計數器為Undefined),CPU切換回來時會按照計數器記錄的行數繼續執行。
(2) 每一個線程都有自己獨自的程序計數器、并且是獨享、互不影響的。
(3) 此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
java虛擬機棧
(1)虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame[插圖])用于存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
(2)在Java虛擬機規范中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規范中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出OutOfMemoryError異常。
本地方法棧:
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native方法服務。
java堆:
(1) 對象的實例、數組都是存儲在此內存區域。
(2) 它是java垃圾收集器管理的區域。
(3) 如果在堆中沒有內存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
方法區:
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
運行時常量池:
常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。
對象訪問:
簡單的Object o=new Object();首先“Object obj”這部分的語義將會反映到Java棧的本地變量表中,作為一個reference類型數據出現。而“newObject()”這部分的語義將會反映到Java堆中,形成一塊存儲了Object類型所有實例數據值的結構化內存。
二、垃圾回收
1..判斷是否是垃圾的算法:
*引用計數法:
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的對象就是不可能再被使用的。
缺點:不能解決循環引用的問題。
*根搜索算法:
在主流的商用程序語言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)判定對象是否存活的。這個算法的基本思路就是通過一系列的名為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
2.方法區的垃圾回收:
方法區(永久代)的垃圾回收分為“廢棄的常量、無用的類”。回收廢棄的常量和回收堆中的對象類似、如果沒有對這個常量的引用之后就會被回收。無用的類回收起來很苛刻如下:
(1) 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
(2) 加載該類的ClassLoader已經被回收。
(3) 該類對應的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
3.垃圾收集算法:
3.1標記清除算法:
標記-清除算法采用從根集合(GC Roots)進行掃描,對存活的對象進行標記,標記完畢后,再掃描整個空間中未被標記的對象,進行回收,如下圖所示。標記-清除算法不需要進行對象的移動,只需對不存活的對象進行處理,在存活對象比較多的情況下極為高效,但由于標記-清除算法直接回收不存活的對象,因此會造成內存碎片。
3.2復制算法:
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。
很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那么Copying算法的效率將會大大降低。
3.3標記整理算法:
該算法標記階段和Mark-Sweep一樣,但是在完成標記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動
*分代收集算法:
根據對象的存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法
jvm將新生代分為三個區域、一個end區和兩個suvivor區。新生代的采用復制算法。新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。
4.垃圾回收策略:
4.1. 對象優先在 Eden 分配
大多數情況下,對象在新生代 Eden 區分配,當 Eden 區空間不夠時,發起 Minor GC。
4.2. 大對象直接進入老年代
大對象是指 需要連續內存空間的對象 ,最典型的大對象是那種很長的字符串以及數組。
經常出現大對象會 提前觸發垃圾收 集以獲取足夠的連續空間分配給大對象。
4.3. 長期存活的對象進入老年代
為對象定義年齡計數器,對象在 Eden 出生并經過 Minor GC 依然存活, 將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。
-XX:MaxTenuringThreshold 用來定義年齡的閾值。
4.4. 動態對象年齡判定
虛擬機并不是永遠地要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代, 如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半, 則年齡大于或等于該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。
4.5. 空間分配擔保
在發生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果條件成立的話,那么 Minor GC 可以確認是安全的。
三、虛擬機性能監控與故障處理工具
虛擬機進程狀況工具:
-
jps命令行工具:
jps是jdk提供的一個查看當前java進程的小工具, 可以看做是JavaVirtual Machine Process Status Tool的縮寫。
示例:
jps –l:輸出主類或者jar的完全路徑名:
image.png
jps –v :輸出jvm參數:
2.jstat:虛擬機統計信息監視工具:
是用于監視虛擬機各種運行狀態信息的命令行工具。它可以顯示本地或遠程[插圖]虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據,在沒有GUI圖形界面,只提供了純文本控制臺環境的服務器上,它將是運行期定位虛擬機性能問題的首選工具。
假設需要每250毫秒查詢一次進程2764垃圾收集的狀況,一共查詢20次,那命令應當是:
jstat -gc 12008 250 20
S0C:第一個幸存區的大小
S1C:第二個幸存區的大小
S0U:第一個幸存區的使用大小
S1U:第二個幸存區的使用大小
EC:伊甸園區的大小
EU:伊甸園區的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法區大小
MU:方法區使用大小
CCSC:壓縮類空間大小
CCSU:壓縮類空間使用大小
YGC:年輕代垃圾回收次數
YGCT:年輕代垃圾回收消耗時間
FGC:老年代垃圾回收次數
FGCT:老年代垃圾回收消耗時間
GCT:垃圾回收消耗總時間
堆內存統計:
jstat -gccapacity 12008
NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:當前新生代容量
S0C:第一個幸存區大小
S1C:第二個幸存區的大小
EC:伊甸園區的大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:當前老年代大小
OC:當前老年代大小
MCMN:最小元數據容量
MCMX:最大元數據容量
MC:當前元數據空間大小
CCSMN:最小壓縮類空間大小
CCSMX:最大壓縮類空間大小
CCSC:當前壓縮類空間大小
YGC:年輕代gc次數
FGC:老年代GC次數
3.jmap:(Memory Map for Java)用于生成堆轉儲快照,即Dump文件;還能查詢finalize執行隊列、java堆和永久代詳細信息,比如空間使用率、當前用的是哪種收集器等。
*jstasck:用于生成虛擬機當前時刻的線程快照(一般稱為threaddump或javacore文件)。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因。
jdk可視化工具:
JDK中除了提供大量的命令行工具外,還有兩個功能強大的可視化工具:JConsole和VisualVM,這兩個工具是JDK的正式成員,沒有被貼上“unsupported andexperimental”的標簽。
4.JConsole:
它是一款基于JMX的可視化監視和管理的工具。直接在jdk bin目錄下啟動JConsole程序
四、類文件結構
1.class文件結構
1.1.Class文件是一組以8個字節為基礎單位的二進制流(可能是磁盤文件,也可能是類加載器直接生成的),各個數據項目嚴格按照順序緊湊地排列,中間沒有任何分隔符;
1.2.Class文件格式采用一種類似于C語言結構體的偽結構來存儲數據,其中只有兩種數據類型:無符號數和表;
1.3.無符號數屬于基本的數據類型,以u1、u2、u4和u8來分別代表1個字節、2個字節、4個字節和8個字節的無符號數,可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值;
表是由多個無符號數獲取其他表作為數據項構成的復合數據類型,習慣以“_info”結尾;
1.4.無論是無符號數還是表,當需要描述同一個類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這時稱這一系列連續的某一類型的數據為某一類型的集合。
2.具體的類文件結構(下面是簡單的打印語句的class文件)
2.1、魔數和版本
2.1.1. Class文件的頭4個字節,唯一作用是確定文件是否為一個可被虛擬機接受的Class文件,固定為“0xCAFEBABE”。
2.1.2 第5和第6個字節是次版本號,第7和第8個字節是主版本號(0x0034為52,對應JDK版本1.8);Java的版本號是從45開始的,JDK1.1之后的每一個JDK大版本發布主版本號向上加1,高版本的JDK能向下兼容低版本的JDK。
對應到class文件中就是:
2.2常量池
2.2.1常量池的組成:
常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近Java語言的常量概念,如文本字符串、聲明為final的常量等。而符號引用則屬于編譯原理方面的概念,它包括三方面的內容:
類和接口的全限定名(Fully Qualified Name);
字段的名稱和描述符(Descriptor);
方法的名稱和描述符;
Java代碼在進行javac編譯的時候并不像C和C++那樣有連接這一步,而是在虛擬機加載class文件的時候進行動態連接。也就是說,在class文件中不會保存各個方法、字段的最終內存布局信息,因此這些字段、方法的符號引用不經過運行期轉換的話無法得到真正的內存入口地址,虛擬機也就無法使用。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址中。
常量池中的每一項都是一個表,這14個表的開始第一個字節是一個u1類型的tag,用來標識是哪一種常量類型。這14種常量類型所代表的含義如下:
2.3、訪問標志
常量池結束后緊接著的兩個字節代表訪問標志,用來標識一些類或接口的訪問信息,包括:這個Class是類還是接口;是否定義為public;是否定義為abstract;如果是類的話,是否被聲明為final等。具體的標志位以及含義如下表:
2.4.類索引、父類索引與接口索引集合
在訪問標志access_flags后接下來就是類索引(this_class)和父類索引(super_class),這兩個數據都是u2類型的,而接下來的接口索引集合是一個u2類型的集合,class文件由這三個數據項來確定類的繼承關系。
2.5.字段表集合
字段表集合,顧名思義就是Java類中的字段,字段又分為類字段(靜態屬性)和實例字段(對象屬性),那么,在Class文件中是如何保存這些字段的呢?我們可以想一想保存一個字段需要保存它的哪些信息呢?
答案是:字段的作用域(public、private和protected修飾符)、是實例變量還是類變量(static修飾符)、可變性(final修飾符)、并發可見性(volatile修飾符)、是否可被序列化(transient修飾符)、字段的數據類型(基本類型、對象、數組)以及字段名稱。
五、虛擬機加載機制
上一節我們已經知道了類文件結構,在class文件中描述的各種信息最終都需要加載到虛擬機中之后才能運行和使用。
1.類的加載過程:
包括加載、驗證、準備、解析、初始化。
1.1加載:
在加載階段,虛擬機需要完成以下三件事情:
1)通過一個類的全限定名來獲取定義此類的二進制字節流。
2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
3)在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。
1.2驗證:
這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
1.2.1.為什么需要驗證:
Class文件并不一定要求用Java源碼編譯而來,可以使用任何途徑,包括用十六進制編輯器直接編寫來產生Class文件。在字節碼的語言層面上,上述Java代碼無法做到的事情都是可以實現的,至少語義上是可以表達出來的。虛擬機如果不檢查輸入的字節流,對其完全信任的話,很可能會因為載入了有害的字節流而導致系統崩潰,所以驗證是虛擬機對自身保護的一項重要工作。
1.2.2.過程:
大致上都會完成下面四個階段的檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
2.準備:
準備階段是正式為類變量分配內存并設置類變量初始值的階段
- 解析:
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程
3.1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經加載到內存中。
3.2. 直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。
4.初始化:
4.1.遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
4.2.使用java.lang.reflect包的方法對類進行發射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
4.3當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4.4當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
對于這四種會觸發類進行初始化的場景,虛擬機規范中使用了一個很強烈的限定語:“有且只有”,這四種場景中的行為稱為對一個類進行主動引用。除此之外所有引用類的方式,都不會觸發初始化,稱為被動引用。下面舉三個例子來說明被動引用,分別見代碼清單7-1、代碼清單7-2和代碼清單7-3。
7-1:
package com.shuai.Util;
class father{
static {
System.out.println("father");
}
public static String name="shuai";
}
class children extends father{
static {
System.out.println("children");
}
}
public class test3 {
public static void main(String[] args) {
System.out.println(children.name);
}
}
上面運行的結果是father shuai不會運行children。當通過子類調用父類的靜態代碼時不會初始化本身的靜態代碼塊。
7-2:
public class test3 {
public static void main(String[] args) {
children[] c=new children[10];
}
}
使用數組的時候不會初始化。
7-3:
class father{
static {
System.out.println("father");
}
public static final String HELLOWORD="hello world";
}
public class test3 {
public static void main(String[] args) {
System.out.println(father.HELLOWORD);
}
}
上述代碼運行之后,也沒有輸出“father”,這是因為雖然在Java源碼中引用了father類中的常量HELLOWORLD,但是在編譯階段將此常量的值“hello world”存儲到了thest類的常量池中,對常量father.HELLOWORLD的引用實際都被轉化為test類對自身常量池的引用了。也就是說實際上father的Class文件之中并沒有father類的符號引用入口,這兩個類在編譯成Class之后就不存在任何聯系了。
5..雙親委派:
站在Java虛擬機的角度講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現[插圖],是虛擬機自身的一部分;另外一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader。
5.1 啟動類加載器(Bootstrap ClassLoader):前面已經介紹過,這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啟動類加載器無法被Java程序直接引用。
5.2擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.LauncherAppClassLoader來實現。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
5.4 過程:
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
六、虛擬機執行引擎
在Java虛擬機規范中制定了虛擬機字節碼執行引擎的概念模型,這個概念模型成為各種虛擬機執行引擎的統一外觀(Facade)。在不同的虛擬機實現里面,執行引擎在執行Java代碼的時候可能有解釋執行(通過解釋器執行)和編譯執行(通過即時編譯器產生本地代碼執行)兩種選擇,也可能兩者兼備,甚至還可能包含幾個不同級別的編譯器執行引擎。但從外觀上看起來,所有的Java虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。本章將主要從概念模型的角度來講解虛擬機的方法調用和字節碼執行。
1.運行時棧幀結構
棧幀(Stack Frame)是用于支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)[插圖]的棧元素。棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。
1.1局部變量表:
局部變量表的容量以變量槽(Variable Slot,下稱Slot)為最小單位,虛擬機規范中并沒有明確指明一個Slot應占用的內存空間大小,只是很有“導向性”地說明每個Slot都應該能存放一個boolean、byte、char、short、int、float、reference或returnAddress類型的數據。
局部變量表中的Slot是可重用的,方法體中定義的變量,其作用域并不一定會覆蓋整個方法體,如果當前字節碼PC計數器的值已經超出了某個變量的作用域,那么這個變量對應的Slot就可以交給其他變量使用。這樣的設計不僅僅是為了節省棧空間,在某些情況下Slot的復用會直接影響到系統的垃圾收集行為。
示例:
public class Test4 {
public static void main(String[] args) {
byte [] a=new byte[64*1024*1024];
System.gc();
}
}
通過設置虛擬機參數-verbose:gc看運行結果:
這是因為當執行system.gc的時候a參數還在當前作用域,此時無法進行垃圾回收。
public static void main(String[] args) {
{
byte[] a = new byte[64 * 1024 * 1024];
}
int b=0;
System.gc();
}
此時可以看到,垃圾會被回收。因為slot被b復用并且清空。
1.2操作數棧:
操作數棧也常被稱為操作棧,它是一個后入先出(Last In First Out,LIFO)棧。同局部變量表一樣,操作數棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數據項之中。操作數棧的每一個元素可以是任意的Java數據類型,包括long和double。32位數據類型所占的棧容量為1,64位數據類型所占的棧容量為2。在方法執行的任何時候,操作數棧的深度都不會超過在max_stacks數據項中設定的最大值。
1.3動態鏈接
每個棧幀都包含一個指向運行時常量池[插圖]中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接。
1.4方法返回地址
當一個方法被執行后,有兩種方式退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者
另外一種退出方式是,在方法執行過程中遇到了異常,并且這個異常沒有在方法體內得到處理,無論是Java虛擬機內部產生的異常,還是代碼中使用athrow字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出。
一般來說,方法正常退出時,調用者的PC計數器的值就可以作為返回地址,棧幀中很可能會保存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。
2.方法調用:
方法調用并不等同于方法執行,方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部的具體運行過程。
2.1.解析:
在類加載的解析階段,會將其中的一部分符號引用轉化為直接引用,這種解析能成立的前提是:方法在程序真正運行之前就有一個可確定的調用版本,并且這個方法的調用版本在運行期是不可改變的。換句話說,調用目標在程序代碼寫好、編譯器進行編譯時就必須確定下來。這類方法的調用稱為解析(Resolution)。
在Java語言中,符合“編譯期可知,運行期不可變”這個要求的方法主要有靜態方法和私有方法兩大類,前者與類型直接關聯,后者在外部不可被訪問,這兩種方法都不可能通過繼承或別的方式重寫出其他版本,因此它們都適合在類加載階段進行解析。與之相對應,在Java虛擬機里面提供了四條方法調用字節碼指令[插圖],分別是:□ invokestatic:調用靜態方法。□ invokespecial:調用實例構造器<init>方法、私有方法和父類方法。□ invokevirtual:調用所有的虛方法。□ invokeinterface:調用接口方法,會在運行時再確定一個實現此接口的對象。只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段確定唯一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器和父類方法四類,它們在類加載的時候就會把符號引用解析為該方法的直接引用。這些方法可以稱為非虛方法,與之相反,其他方法就稱為虛方法。
2.2分派:
解析調用是一個靜態的過程,在編譯期間就完全確定,不會延遲到運行期再去完成。
分派調用則可能是靜態的也可能是動態的。
根據分派宗數量可分為單分派和多分派。這兩類分派又可兩兩組合成:靜態單分派,靜態多分派,動態單分派和動態多分派4中分派組合。
分派體現了Java的多態性,如“重載”和“重寫”。
靜態分派:
所有依賴靜態類型(類型的引用)來定位方法執行版本的分派動作,都稱為靜態分派。靜態分派的最典型應用就是方法重載。靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的。
動態分派:
我們把在運行期根據實際類型確定方法執行版本的分派過程稱為動態分派。