一個一直運行正常的應用突然無法運行了。在類庫被更新之后,返回下面的錯誤。
- Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
- at com.nhn.service.UserService.add(UserService.java:14)
- at com.nhn.service.UserService.main(UserService.java:19)
應用的代碼如下,而且它沒有被改動過。
// UserService.java … public void add(String userName) { admin.addUser(userName); }
更新后的類庫的源代碼和原始的代碼如下。
// UserAdmin.java - Updated library source code … public User addUser(String userName) { User user = new User(userName); User prevUser = userMap.put(userName, user); return prevUser; } // UserAdmin.java - Original library source code … public void addUser(String userName) { User user = new User(userName); userMap.put(userName, user); }
簡而言之,之前沒有返回值的addUser()被改修改成返回一個User類的實例的方法。不過,應用的代碼沒有做任何修改,因為它沒有使用addUser()的返回值。
咋一看,om.nhn.user.UserAdmin.addUser()方法似乎仍然存在,如果存在的話,那么怎么還會出現NoSuchMethodError的錯誤呢?
原因
上面問題的原因是在於應用的代碼沒有用新的類庫來進行編譯。換句話來說,應用代碼似乎是調了正確的方法,只是沒有使用它的返回值而已。不管怎樣,編譯后的class文件表明了這個方法是有返回值的。你可以從下面的錯誤信息里看到答案。
java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;) V
NoSuchMethodError出現的原因是“com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V”方法找不到。注意一下”Ljava/lang/String;”和最后面的“V”。在Java字節碼的表達式里,”L<classname>;”表示的是類的實例。這里表示addUser()方法有一個java/lang/String的對象作為參數。在這個類庫里,參數沒有被改變,所以它是正常的。最后面的“V”表示這個方法的返回值。在Java字節碼的表達式里,”V”表示沒有返回子(Void)。綜上所述,上面的錯誤信息是表示有一個java.lang.String類型的參數,並且沒有返回值的com.nhn.user.UserAdmin.addUser方法沒有找到。
因為應用是用之前的類庫編譯的,所以返回值為空的方法被調用了。但是在修改后的類庫里,返回值為空的方法不存在,並且添加了一個返回值為“Lcom/nhn/user/User”的方法。因此,就出現了NoSuchMethodError。
再回到Java字節碼上來。Java字節碼是JVM很重要的部分。JVM是模擬執行Java字節碼的一個模擬器。Java編譯器不會直接把高級語言(例如C/C++)編寫的代碼直接轉換成機器語言(CPU指令);它會把開發者可以理解的Java語言轉換成JVM能夠理解的Java字節碼。因為Java字節碼本身是平台無關的,所以它可以在任何安裝了JVM(確切地說,是相匹配的JRE)的硬件上執行,即使是在CPU和OS都不相同的平台上(在Windows PC上開發和編譯的字節碼可以不做任何修改就直接運行在Linux機器上)。編譯后的代碼的大小和源代碼大小基本一致,這樣就可以很容易地通過網絡來傳輸和執行編譯后的代碼。
Java class文件是一種人很難去理解的二進文件。為了便於理解它,JVM提供者提供了javap,反匯編器。使用javap產生的結果是Java匯編語言。在上面的例子中,下面的Java匯編代碼是通過javap-c對UserServiceadd()方法進行反匯編得到的。
public void add(java.lang.String); Code: 0: aload_0 1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin; 4: aload_1 5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V 8: return
ddUser()方法是在第四行的“5:invokevitual#23″進行調用的。這表示對應索引為23的方法會被調用。索引為23的方法的名稱已經被javap給注解在旁邊了。invokevirtual是Java字節碼里調用方法的最基本的操作碼。
在Java字節碼里,有四種操作碼可以用來調用一個方法,操作碼的作用分別如下:
- invokespecial: 調用一個初始化(構造)方法,私有方法或者父類的方法
- invokestatic:調用靜態方法
- invokevirtual:調用實例方法
- invokeinterface:調用接口方法
Java字節碼的指令集由操作碼和操作數組成。類似invokevirtual這樣的操作數需要2個字節的操作數。
用更新的類庫來編譯上面的應用代碼,然后反編譯它,將會得到下面的結果。
public void add(java.lang.String); Code: 0: aload_0 1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin; 4: aload_1 5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User; 8: pop 9: return
你會發現,對應索引為23的方法被替換成了一個返回值為”Lcom/nhn/user/User”的方法。
它表示的是字節數。大概這就是為什么運行在JVM上面的代碼成為Java“字節”碼的原因。簡而言之,Java字節碼指令的操作碼,例如aload_0,getfield和invokevirtual等,都是用一個字節的數字來表示的(aload_0=0x2a,getfield=0xb4,invokevirtual=0xb6)。由此可知Java字節碼指令的操作碼最多有256個。
aload_0和aload_1這樣的指令不需要任何操作數。因此,aload_0指令的下一個字節是下一個指令的操作碼。不過,getfield和invokevirtual指令需要2字節的操作數。因此,getfiled的下一條指令是跳過兩個字節,寫在第四個字節的位置上的。
---------------------------------------------------------invokespecial,invokevirtual/靜態綁定與動態綁定-----------------------------
invokespecial指靜態綁定后,由JVM產生調用的方法。如super(),以及super.someMethod(),都屬於invokespecial。而invokevirtual指動態綁定后由JVM產生調用的方法,如obj.someMethod(),屬於invokevirtual。
正是由於這兩種綁定的不同,在子類覆蓋超類的方法、並向上轉型引用后,才產生了多態以及其他特殊的調用結果。運行時,invokespecial選擇方法基於引用聲明的類型,而不是對象實際的類型。但invokevirtual則選擇當前引用的對象的類型。
以下情況使用invokespecial操作碼:
1 init()函數,就是調用構造函數生產一個實例的時候。
2 私有方法
3 final方法
invokespecial和invokestatic是采用靜態綁定,invokevirtual和invokeinterface是采用動態綁定。
程序綁定的概念:
綁定指的是一個方法的調用與方法所在的類(方法主體)關聯起來。對java來說,綁定分為靜態綁定和動態綁定;或者叫做前期綁定和后期綁定
靜態綁定:
在程序執行前方法已經被綁定,此時由編譯器或其它連接程序實現。例如:C。
針對java簡單的可以理解為程序編譯期的綁定;這里特別說明一點,java當中的方法只有final,static,private和構造方法是前期綁定
動態綁定:
后期綁定:在運行時根據具體對象的類型進行綁定。
若一種語言實現了后期綁定,同時必須提供一些機制,可在運行期間判斷對象的類型,並分別調用適當的方法。也就是說,編譯器此時依然不知道對象的類型,但方法調用機制能自己去調查,找到正確的方法主體。不同的語言對后期綁定的實現方法是有所區別的。但我們至少可以這樣認為:它們都要在對象中安插某些特殊類型的信息。
動態綁定的過程:
虛擬機提取對象的實際類型的方法表;
虛擬機搜索方法簽名;
調用方法。
關於綁定相關的總結:
在了解了三者的概念之后,很明顯我們發現java屬於后期綁定。在java中,幾乎所有的方法都是后期綁定的,在運行時動態綁定方法屬於子類還是基類。但是也有特殊,針對static方法和final方法由於不能被繼承,因此在編譯時就可以確定他們的值,他們是屬於前期綁定的。特別說明的一點是,private聲明的方法和成員變量不能被子類繼承,所有的private方法都被隱式的指定為final的(由此我們也可以知道:將方法聲明為final類型的一是為了防止方法被覆蓋,二是為了有效的關閉java中的動態綁定)。java中的后期綁定是有JVM來實現的,我們不用去顯式的聲明它,而C++則不同,必須明確的聲明某個方法具備后期綁定。
java當中的向上轉型或者說多態是借助於動態綁定實現的,所以理解了動態綁定,也就搞定了向上轉型和多態。
前面已經說了對於java當中的方法而言,除了final,static,private和構造方法是前期綁定外,其他的方法全部為動態綁定。而動態綁定的典型發生在父類和子類的轉換聲明之下:
其具體過程細節如下:
1:編譯器檢查對象的聲明類型和方法名。假設我們調用x.f(args)方法,並且x已經被聲明為C類的對象,那么編譯器會列舉出C類中所有的名稱為f的方法和從C類的超類繼承過來的f方法
2:接下來編譯器檢查方法調用中提供的參數類型。如果在所有名稱為f 的方法中有一個參數類型和調用提供的參數類型最為匹配,那么就調用這個方法,這個過程叫做“重載解析”
3:當程序運行並且使用動態綁定調用方法時,虛擬機必須調用同x所指向的對象的實際類型相匹配的方法版本。
---------------------------------invokevirtual和invokeinterface的區別------------------------------
都說調用接口要比調用繼承類要慢,但慢在何處?
先看byteCodeInterpreter.cpp里面對這invokevirtual和invokeInterface的區別。
CASE(_invokeinterface): { //調用接口
u2 index = Bytes::get_native_u2(pc+1);
ConstantPoolCacheEntry* cache = cp->entry_at(index);
methodOop callee;
klassOop iclass = (klassOop)cache->f1();
int parms = cache->parameter_size();
oop rcvr = STACK_OBJECT(-parms);
CHECK_NULL(rcvr);
instanceKlass* int2 = (instanceKlass*) rcvr->klass()->klass_part();
itableOffsetEntry* ki = (itableOffsetEntry*) int2->start_of_itable();
int i;
for ( i = 0 ; i < int2->itable_length() ; i++, ki++ ) {//搜索整個接口表,進行比較,直至找到
if (ki->interface_klass() == iclass) break;
}
.......
int mindex = cache->f2();
itableMethodEntry* im = ki->first_method_entry(rcvr->klass());
callee = im[mindex].method();//通過找到的接口,找到要調用的方法
//而invokevirtual(調用繼承類)
CASE(_invokevirtual):
u2 index = Bytes::get_native_u2(pc+1);
ConstantPoolCacheEntry* cache = cp->entry_at(index);
methodOop callee;
int parms = cache->parameter_size();
instanceKlass* rcvrKlass = (instanceKlass*) STACK_OBJECT(-parms)->klass()->klass_part();
callee = (methodOop) rcvrKlass->start_of_vtable()[ cache->f2()]; //直接調用方法
由上面可見,最大的區別就是接口調用每次都需要搜索接口表,而調用繼承類可以直接找到。
再看看權威書籍《深入java虛擬機》P336頁給出的答案,“java虛擬機使用不同於類引用的操作碼來調用接口引用的方法,這是因為java不能象使用引用那樣,使用許多與方法表偏移量相關的假設。對於類引用來說,無論對象實際的類是什么,方法在方法表始終占據相同的位置。但對於接口引用來說,情況就不是這樣了,位於不同類的同一個方法所占據的位置是不同的,盡管這些類實現同一個接口。”
以下詳細解釋了上面這一段話,摘抄至http://stackoverflow.com/questions/1504633/what-is-the-point-of-invokeinterface
Each Java class is associated with a virtual method table that contains "links" to the bytecode of each method of a class. That table is inherited from the superclass of a particular class and extended with regard to the new methods of a subclass. E.g.,
class BaseClass {
public void method1() { }
public void method2() { }
public void method3() { }
}
class NextClass extends BaseClass {
public void method2() { } // overridden from BaseClass
public void method4() { }
}
results in the tables
BaseClass 1. BaseClass/method1() 2. BaseClass/method2() 3. BaseClass/method3() NextClass 1. BaseClass/method1() 2. NextClass/method2() 3. BaseClass/method3() 4. NextClass/method4()
Note, how the virtual method table of NextClass retains the order of entries of the table ofBaseClass and just overwrites the "link" of method2() which it overrides.
An implementation of the JVM can thus optimize a call to invokevirtual by remembering thatBaseClass/method3() will always be the third entry in the virtual method table of any object this method will ever be invoked on.
With invokeinterface this optimization is not possible. E.g.,
interface MyInterface {
void ifaceMethod();
}
class AnotherClass extends NextClass implements MyInterface {
public void method4() { } // overridden from NextClass
public void ifaceMethod() { }
}
class MyClass implements MyInterface {
public void method5() { }
public void ifaceMethod() { }
}
This class hierarchy results in the virtual method tables
AnotherClass 1. BaseClass/method1() 2. NextClass/method2() 3. BaseClass/method3() 4. AnotherClass/method4() 5. MyInterface/ifaceMethod() MyClass 1. MyClass/method5() 2. MyInterface/ifaceMethod()
As you can see, AnotherClass contains the interface's method in its fifth entry and MyClasscontains it in its second entry. To actually find the correct entry in the virtual method table, a call to a method with invokeinterface will always have to search the complete table without a chance for the style of optimization that invokevirtual does.
There are additional differences like the fact, that invokeinterface can be used together with object references that do not actually implement the interface. Therefore, invokeinterface will have to check at runtime whether a method exists in the table and potentially throw an exception. If you want to dive deeper into the topic, I suggest, e.g., "Efficient Implementation of Java Interfaces: Invokeinterface Considered Harmless".
我的理解是,繼承類的方法調用可以直接用序號就能找到想要的方法,因為繼承類的方法在方法表里是有順序的,而且是固定的,只會越來越多,但不會減少,所以用序號作為索引就能找到,但接口可以在不同的類里實現,導致上面的查找策略不可用了,只能全部遍歷了。
