在操作系統OS中為了優化內存的使用會采用一種動態鏈接方式,一個文件想要在操作系統中運行必須經過編譯、匯編譯、鏈接、裝載等步驟。可以參考Java程序是怎么跑起來的。本篇主要講解Java棧幀中動態鏈接部分與操作系統的的動態鏈接的區別與聯系
操縱系統為什么需要動態鏈接
OS是向下統一管理機器硬件、向上給各個應用程序提供統一的系統調用的程序。其中對內存的管理也是重頭戲,以下是32位Linux操作系統中虛擬內存的空間分配圖
每一個應用程序看到的內存就是這樣的一段虛擬內存空間。應用程序的代碼指令就存儲在.text段也叫代碼段,只讀。.data段也叫數據段用于存儲程序的靜態變量、全局變量,可讀可寫
在應用程序的運行中除了運行自身的代碼指令外還需要加載一些系統的公共庫,比如用于網絡收發的socket庫等。在windows中,這些共享庫以.dll(Dynamic-Link Libary 動態鏈接庫)結尾,在Linux中以.so(Shared Object)結尾。加載這些共享庫可進行對系統資源的調用
靜態鏈接
當應用程序代碼經歷鏈接過程生成可執行文件時,每鏈接一個共享庫就將共享庫代碼復制一份進應用程序的可執行文件中,因此有多少應用程序調用同一個共享庫文件,該共享庫文件中的代碼就在內存中加載多少份
動態鏈接
在沒用動態鏈接前,系統確實是采用靜態鏈接的方式鏈接共享庫,但是發現對內存的使用是一種極大的浪費,因此動態鏈接孕育而生。為了達到各個應用程序只加載同一個共享庫但內存只存在一份共享庫代碼,動態鏈接首先解決的技術問題是地址無關性
PLT、GOT表解決地址無關性
我們都知道程序代碼指令加載進內存的代碼段是可執行只讀的,無法動態的修改代碼指令。那么當共享庫載入內存中時是怎么被各個不同的應用程序找到的呢?
其實在應用程序的可執行文件加載進內存后,該程序的內存數據段(.data)存在一張GOT(Global Offset Table)全局偏移表,GOT表中,當有需要引用共享庫地址的方法指令,都會查詢 GOT,根據GOT表找到共享庫方法指令的地址位置并調用。因為GOT存在于數據段,因此當共享庫發生變化時,應用程序也不需要重新編譯,可以直接動態的改變GOT表中的虛擬內存,從而找到最新的共享庫。
共享庫載入實際的物理內存,雖然物理內存不會變,但是每個應用程序看到的虛擬內存不一樣,所以共享庫在不同的應用程序中的虛存地址是不一樣的,好在每個應用程序都擁有自己的GOT表,能夠準確的記錄了共享庫的位置。這也就達到了地址無關性。
PLT(Procedure Link Table)程序鏈接表存在于內存的代碼段中,主要是用于延遲綁定,我們可以將其理解為跳表。應用程序先是調用PLT表中查詢需要調用的GOT表的地址位置,跳到GOT表后查詢出共享庫的虛存,然后再去調用共享庫方法。因為很多動態裝載的函數庫都是不會被實際調用到的,而共享庫中存在非常多的函數,因此采用PLT可達到延遲加載。
像動態鏈接這樣通過修改“地址數據”來進行間接跳轉,去調用一開始不能確定位置代碼的思路,Java中的多態也采用了這種思想。
Java棧幀中的動態鏈接
以前的文章中解釋了棧幀(拆解棧幀中本地變量表),其中動態鏈接也是組成棧幀的一部分。在上面對OS的解釋中,動態鏈接是一種技術名稱,在Java棧幀這里怎么就成了一個實體了呢?其實根據Java虛擬機對動態鏈接的描述,翻譯成中文就是一個【引用】,那么棧幀存在的這個【引用】是干什么的呢?
在解釋這個【引用】的作用之前,還是先說明一點,Java棧幀中的動態鏈接的目的其實跟OS的是一樣的,都是為了節省內存空間,知道這個目的后我們再說明為什么可以節省。也因此在看JVM的時候,我總是會與OS做類比。
這個【引用】在虛機規范的解釋為指向運行時常量池的方法引用。每當棧幀中調用其他方法時都會存在一個【引用】。在.class文件中所有的變量和方法引用都是符號引用(Symbolic Reference)也就是下面字節碼中的 #數字。比如下面用javap反編譯的.class文件中的Constant pool。
public?class?com.ethan.chapter02.Test02LocalVariablesminor?version:?0major?version:?52flags:?ACC_PUBLIC,?ACC_SUPERConstant?pool:
???#1?=?Methodref??????????#7.#28?????????//?java/lang/Object."":()V
???#2?=?Class??????????????#29????????????//?com/ethan/chapter02/Test02LocalVariables
???#3?=?Methodref??????????#2.#28?????????//?com/ethan/chapter02/Test02LocalVariables."":()V
???#4?=?Methodref??????????#2.#30?????????//?com/ethan/chapter02/Test02LocalVariables.test3:()V
???#5?=?Long???????????????100l
???#7?=?Class??????????????#31????????????//?java/lang/Object
???#8?=?Utf8???????????????
???#9?=?Utf8???????????????()V
??#10?=?Utf8???????????????Code
??#11?=?Utf8???????????????LineNumberTable
??#12?=?Utf8???????????????LocalVariableTable
??#13?=?Utf8???????????????this
??#14?=?Utf8???????????????Lcom/ethan/chapter02/Test02LocalVariables;
??#15?=?Utf8???????????????main
??#16?=?Utf8???????????????([Ljava/lang/String;)V
??#17?=?Utf8???????????????args
??#18?=?Utf8???????????????[Ljava/lang/String;
??#19?=?Utf8???????????????variablesTable
??#20?=?Utf8???????????????num
??#21?=?Utf8???????????????I
??#22?=?Utf8???????????????test3
??#23?=?Utf8???????????????q
??#24?=?Utf8???????????????J
??#25?=?Utf8???????????????a
??#26?=?Utf8???????????????SourceFile
??#27?=?Utf8???????????????Test02LocalVariables.java
??#28?=?NameAndType????????#8:#9??????????//?"":()V
??#29?=?Utf8???????????????com/ethan/chapter02/Test02LocalVariables
??#30?=?NameAndType????????#22:#9?????????//?test3:()V
??#31?=?Utf8???????????????java/lang/Object
{public?com.ethan.chapter02.Test02LocalVariables();
????descriptor:?()V
????flags:?ACC_PUBLIC
????Code:
??????stack=1,?locals=1,?args_size=10:?aload_01:?invokespecial?#1??????????????????//?Method?java/lang/Object."":()V4:?return
??????LineNumberTable:
????????line?10:?0
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature0???????5?????0??this???Lcom/ethan/chapter02/Test02LocalVariables;public?static?void?main(java.lang.String[]);
????descriptor:?([Ljava/lang/String;)V
????flags:?ACC_PUBLIC,?ACC_STATIC
????Code:
??????stack=2,?locals=3,?args_size=10:?new???????????#2??????????????????//?class?com/ethan/chapter02/Test02LocalVariables3:?dup4:?invokespecial?#3??????????????????//?Method?"":()V7:?astore_18:?bipush????????1010:?istore_211:?aload_112:?invokevirtual?#4??????????????????//?Method?test3:()V15:?return
??????LineNumberTable:
????????line?12:?0
????????line?13:?8
????????line?14:?11
????????line?15:?15
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature0??????16?????0??args???[Ljava/lang/String;8???????8?????1?variablesTable???Lcom/ethan/chapter02/Test02LocalVariables;11???????5?????2???num???Ipublic?void?test3();
????descriptor:?()V
????flags:?ACC_PUBLIC
????Code:
??????stack=2,?locals=4,?args_size=10:?ldc2_w????????#5??????????????????//?long?100l3:?lstore_14:?bipush????????106:?istore_37:?return
??????LineNumberTable:
????????line?20:?0
????????line?21:?4
????????line?22:?7
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature0???????8?????0??this???Lcom/ethan/chapter02/Test02LocalVariables;4???????4?????1?????q???J7???????1?????3?????a???I
}
在.class文件中的常量池會隨著文件被加載而轉換進JVM中的運行時常量池中。由于存在了這些【符號引用】,可以使用Java層面的動態鏈接技術,將這些符號引用轉換為調用方法的直接引用。比如字節碼中的 invokevirtual指令就能夠支持動態鏈接。
在類加載子系統中,一個.class文件被加載進JVM共需要經歷3步驟,加載-鏈接-初始化。而在鏈接階段中的第三步【解析】的目的就是將常量池內的符號引用轉換為直接引用的過程,也就是動態鏈接產生的過程。
我們類比一下OS的動態鏈接與Java的動態鏈接。Java的.class文件類比于OS的每一個應用程序的可執行文件,.class文件中的常量池類比于GOT表,java的運行時常量池類比于共享庫。java產生動態的鏈接是在.class的解析階段,根據.class文件中的符號引用去查詢常量池然后,將.class文件中的符號引用轉換為直接應用,并存于棧幀中。
因為在加載不同的.class文件時,都可能調用相同的常量或者方法,所以只需要在運行時常量池存儲一份,然后記錄其直接引用即可,因此節省了空間。
解釋完Java層面的動態鏈接我們也就能解釋Java多態的實現過程了,在Java源代碼編譯期間方法的重寫導致無法確認出調用方法的真正位置,只有在運行時將符號引用轉為為直接應用采用確定方法的位置。這個過程也就是在【解析】階段實現的。
這種編譯時期無法確定方法的調用位置,只能夠在程序運行期根據實際的類型綁定相關方法,這種綁定方式也就被稱之為晚期綁定。