JAVA虛擬機調用一個類方法時,它會基於對象引用的類型(通常在編譯時可知)來選擇所調用的方法。相反,當虛擬機調用一個實例方法時,它會基於對象實際的類型(只能在運行時得知)來選擇所調用的方法,這就是動態綁定,是多態的一種。動態綁定爲解決實際的業務問題提供了很大的靈活性,是一種非常優美的機制。
1 JAVA對象模型
JAVA虛擬機規範並沒有規定JAVA對象在堆裏是如何表示的。對象的內部表示也影響着整個堆以及垃圾收集器的設計,它由虛擬機的實現者決定。
JAVA對象中包含的基本數據由它所屬的類及其所有超類聲明的實例變量組成。只要有一個對象引用,虛擬機就必須能夠快速地定位對象實例的數據。另外,它也必須能通過該對象引用訪問相應的類數據(存儲於方法區的類型信息),因此在對象中通常會有一個指向方法區的指針。當程序在運行時需要轉換某個對象引用爲另外一種類型時,虛擬機必須要檢查這種轉換是否被允許,被轉換的對象是否的確是被引用的對象或者它的超類型。當程序在執行instanceof操作時,虛擬機也進行了同樣的檢查。所以虛擬機都需要查看被引用的對象的類數據。
不管虛擬機的實現使用什麼樣的對象表示法,很可能每個對象都有一個方法表因爲方法表加快了調用實例方法時的效率。但是JAVA虛擬機規範並未要求必須使用方法表,所以並不是所有實現中都會使用它。
下面是一種JAVA對象的內存表示:
JAVA對象內存模型
方法數據存放在類的方法區中,包含一個方法的具體實現的字節碼二進制。方法指針直接指向這個方法在內存中的起始位置,通過方法指針就可以找到這個方法。
2 動態綁定內部機制
方法表是一個指向方法區中的方法指針的數組。方法表中不包含static、private等靜態綁定的方法,僅僅包含那些需要動態綁定的實例方法。
在方法表中,來自超類的方法出現在來自子類的方法之前,並且排列方法指針的順序和方法在class文件中出現的順序相同,這種排列順序的例外情況是,被子類的方法覆蓋的方法出現在超類中該方法第一次出現的地方。
例如有超類Base和子類Derive:
- public class Base
- {
- public Base()
- {
- }
- public void test()
- {
- System.out.println( "int Base" );
- }
- public void print()
- {
- }
- }
- public class Derive extends Base
- {
- public Derive()
- {
- }
- public void test()
- {
- System.out.println( "int Derive" );
- }
- public void sayHello()
- {
- }
- public static void main( String[] args )
- {
- Base base = new Derive();
- base.test();
- }
- }
上例中的Base和Derive的方法表如下:
在這個例子裏,test()方法在Base和Derive的方法表中都是同一個位置-位置1。在Base方法表中,test()指針是Base的test()方法內存地址;而在Derive方法表中,方法表的位置1放置的是Derive的test()方法內存地址。
當JAVA虛擬機執行base.test()時,通過base引用可以找到base所指向的實際對象的內存位置,現在虛擬機不知道base引用的實際對象是Base還是Derive。但是根據上面的對象內存模型,虛擬機從對象內存中的第一個指針“特殊結構指針”開始,可以找到實際對象的類型數據和Class實例,這樣虛擬機就可以知道base引用的實際對象是Derive對。爲了執行test(),虛擬機需要找到test()的字節碼,方法的字節碼存放在方法區中。虛擬機從對象內存中的第一個指針“特殊結構指針”開始,搜尋方法表的位置1,位置1指向的test()方法是Derive類的test()方法,這就是JAVA虛擬機將要執行的test()的字節碼。現在,虛擬機知道了調用的實際對象是Derive對象,調用的實際test()方法是Derive類的test()方法,所以JAVA虛擬機能夠正確執行-調用base引用的實際對象的方法而不是base引用本身的方法。
這是動態綁定的一種實現方式,根據不同的JAVA虛擬機平臺和不同的實際約束,動態綁定可以有不同的內部實現機制。