多態是Java語言極為重要的一個特性,可以說是Java語言動態性的根本,那麼線程執行一個方法時到底在記憶體中經歷了什麼,JVM又是如何確定方法執行版本的呢? ...
1 引言
多態是Java語言極為重要的一個特性,可以說是Java語言動態性的根本,那麼線程執行一個方法時到底在記憶體中經歷了什麼,JVM又是如何確定方法執行版本的呢?
2 棧幀
JVM中由棧幀存儲方法的局部變數表、操作數棧、動態連接和方法返回地址等信息。每一個方法的調用就是從入棧到出棧到過程。
2.1 局部變數表
局部變數表由變數槽組成,《Java虛擬機規範》指出:“每個變數槽都應該能存放一個boolean、byte、char、short、int、float、reference或returnAddress類型的數據”。
這八種數據類型都可以使用32位或更小的物理記憶體來存儲,如果是64位虛擬機環境下,虛擬機需要通過對齊填充來使變數槽與在32位虛擬機環境下外觀一致。
如果是64位的數據類型,比如long和double,JVM會以高位對齊的方式為其分配兩個連續的變數槽空間。且規定不允許以任何方式訪問這兩個變數槽的其中一個,類載入的校驗階段會針對違反規定的行為拋出異常。
類變數會有兩次賦值,一次是準備階段給賦值一個預設值,二是初始化階段,賦予程式定義的值。但方法變數沒有準備階段,所以沒賦值的方法變數不能被使用。
2.2 變數槽的復用
為了節省記憶體空間,變數槽是可以復用的。當程式計數器的值超過方法體中定義的變數的作用域時,這個變數的變數槽就可以被其他變數復用了。不過雖然這樣可以節省記憶體空間,但對GC有一定影響。
舉個例子,如果沒有發生即時編譯的前提下,在方法清單1中placeholder
不會被回收。原因是,方法清單1中gc發生時,變數槽仍然保持著對placeholder
的引用,所以不會被標記為可回收對象。而在方法清單2中國呢增加了int a = 0
後,placeholder
原有的變數槽被變數a復用了,也就不存在引用placeholder
的變數槽了,所以placeholder
就可以被回收了。
方法清單1:
public static void main (String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
方法清單2:
public static void main (String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
但是實際上,大部分程式都是運行在即時編譯下的,所以編譯器會對其進行優化,實際情況下方法清單1中placeholder
也能被回收。
2.3 操作數棧
操作數棧主要作用有二:
1.作為計算過程中的所需變數的臨時存儲空間
2.存儲系統運行過程中的計算中間結果
操作數棧不能通過指針訪問,只能通過彈棧和壓棧來操作其內部元素。當執行某項指令前會將所需變數壓入棧頂,然後真正執行指令時從棧頂依次取出用來執行具體指令,執行完成後會將結果在壓入操作數棧。
大多數虛擬機實現會有一些優化處理,將兩個棧幀部分重疊:上一個棧幀的部分操作數棧和下一個棧幀的部分局部變數表。不僅節約空間,還讓下麵棧的操作可以直接使用上面棧的內容,減少了參數傳遞。
2.4 動態鏈接
Java文件被編譯成Class文件後,變數和方法的引用都作為符號引用保存在Class文件中的常量池中。而對於方法的引用,某些可以在編譯期就確定下來稱為“直接引用”,而有些方法只能在運行期才能確定下來(比如方法的重載)。
動態鏈接的作用就是在運行期將符號引用轉換為直接引用。
2.5 方法返回地址
一個方法執行完成後,有兩種方式退出:正常完成和拋出異常。
當方法A中調用方法B時,A的棧幀中會保存程式計數器的值作為返回地址。而異常退出時,返回地址是要通過異常處理器表來確定的。
方法返回後還會進行幾個操作:
1.恢復主調線程對應棧幀中的局部變數表和操作數棧
2.把返回值壓入主調線程的棧幀中
3.調整程式計數器到方法調用指令的下一條指令
2.6 附加信息
不同虛擬機在實現時可以自定義一些例如調試、性能收集等信息放到棧幀之中。
3 方法調用
一切方法調用在Class文件裡面存儲的都只是符號引用,某些調用需要在類載入時甚至運行期間才能確定目標方法的直接引用,這是Java強大的動態擴展能力的基礎。
3.1 方法調用指令
JVM共支持以下5種方法調用位元組碼指令:
•invokestatic調用靜態方法
•invokespecial調用構造器
•invokevirtual調用所有虛方法
•invokeinterface調用介面方法,運行期會確定具體實現該介面的對象
•invokedynamic調用運行期動態解析出具體調用的方法
其中,invokestatic和invokespecial指令調用的方法,都可以類載入的解析階段確定調用的方法版本,Java中符合這個條件的方法共有五種:靜態方法、私有方法、實例構造器、父類方法和final修飾的方法(它使用invokevirtual指令調用)。
這五種方法稱為“非虛方法”(Non-Virtual Method),剩下的均為“虛方法”(Virtual Method)。
3.2 解析
如果一個方法在類載入的解析階段就能確定方法的調用版本,那麼這類方法的調用被稱為解析(Resolution)。
Java中符合解析標準的主要是靜態方法和私有方法。前者與類型直接相關,後者對外不可見。
方法調用指令中,invokestatic
和invokespecial
指令調用的方法,再加上final修飾的方法,都被稱作“非虛方法”,他們都可以在解析階段確定唯一的調用版本。其他的方法都被稱作“虛方法”。
3.3 分派
在編譯階段,依賴靜態類型確定方法的調用版本,這就叫做“靜態分派”。
而在運行期,根據實際類型確定方法調用版本被稱作“動態分派”。
3.3.1 靜態分派
直接上個