問題
1、Java到底是按值傳遞(Call by Value),還是按引用傳遞(Call by Reference)?
2、如下面的程式碼,為什麼不能進行交換?
1 2 3 4 5 6 |
public CallBy swap2(CallBy a,CallBy b) { CallBy t = a; a = b; b = t; return b; } |
3、如下面的程式碼,為什麼能夠交換成功?
1 2 3 4 5 6 |
public int swap2(CallBy a,CallBy b) { int t = a.value; a.value = b.value; b.value = t; return t; } |
簡單的C++例子
為了解決上面的三個問題,我們從簡單的例子開始,為什麼是C++的例子呢?看完了你就會明白。
假設我們要交換兩個整形變數的值,在C++中怎麼做呢?
我們來看多種方式,哪種能夠做到.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <iostream> using namespace std; // 可以交換的例子 void call_by_ref(int &p,int &q) { int t = p; p = q; q = t; } // 不能交換的例子 void call_by_val_ptr(int * p,int * q) { int * t = p; p = q; q = t; } // 不能交換的例子 void call_by_val(int p,int q){ int t = p ; p = q; q = t; } int main() { int a = 3; int b = 4; cout << "---------- input ------------" << endl; cout << "a = " << a << ", b = " << b << endl << endl; call_by_val(a,b); cout << "---------- call_by_val ------------" << endl; cout << "a = " << a << ", b = " << b << endl << endl; call_by_val_ptr(&a,&b); cout << "---------- call_by_val_ptr ------------" << endl; cout << "a = " << a << ", b = " << b << endl << endl; call_by_ref(a,b); cout << "---------- call_by_ref ------------" << endl; cout << "a = " << a << ", b = " << b << endl; } |
因為例子非常簡單,看程式碼即可知道只有call_by_ref這個方法可以成功交換。這裡,你一定還知道一種可以交換的方式,彆著急,慢慢來,我們先看看為什麼只有call_by_ref可以交換。
1、call_by_ref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void call_by_ref(int &p,int &q) { push %rbp mov %rsp,%rbp mov %rdi,-0x18(%rbp) mov %rsi,-0x20(%rbp) //int t = p; mov -0x18(%rbp),%rax //關鍵點:rax中存放的是變數的實際地址,將地址處存放的值取出放到eax中 mov (%rax),%eax mov %eax,-0x4(%rbp) //p = q; mov -0x20(%rbp),%rax //關鍵點:rax中存放的是變數的實際地址,將地址處存放的值取出放到edx mov (%rax),%edx mov -0x18(%rbp),%rax mov %edx,(%rax) //q = t; mov -0x20(%rbp),%rax mov -0x4(%rbp),%edx //關鍵點:rax存放的也是實際地址,同上. mov %edx,(%rax) } |
上面這段彙編的邏輯非常簡單,我們看到裡面的關鍵點都在強調:
將值存放在實際地址中.
上面這句話雖然簡單,但很重要,可以拆為兩點:
1、要有實際地址.
2、要有將值存入實際地址的動作.
從上面的程式碼中,我們看到已經有“存值”這個動作,那麼傳入的是否實際地址呢?
1 2 3 4 5 6 7 8 9 10 |
// c程式碼 call_by_val_ptr(&a,&b); // 對應的彙編程式碼 lea -0x18(%rbp),%rdx lea -0x14(%rbp),%rax mov %rdx,%rsi mov %rax,%rdi callq 4008c0 <_Z11call_by_refRiS_> |
注意到,lea操作是取地址,那麼就能確定這種“按引用傳遞“的方式,實際是傳入了實參的實際地址。
那麼,滿足了上文的兩個條件,就能交換成功。
2、call_by_val
call_by_val的反彙編程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void call_by_val(int p,int q){ push %rbp mov %rsp,%rbp mov %edi,-0x14(%rbp) mov %esi,-0x18(%rbp) //int t = p ; mov -0x14(%rbp),%eax mov %eax,-0x4(%rbp) //p = q; mov -0x18(%rbp),%eax mov %eax,-0x14(%rbp) //q = t; mov -0x4(%rbp),%eax mov %eax,-0x18(%rbp) } |
可以看到,上面的程式碼中在賦值時,僅僅是將某種”值“放入了暫存器,再觀察下傳參的程式碼:
1 2 3 4 5 6 7 8 9 |
// c++程式碼 call_by_val(a,b); // 對應的彙編程式碼 mov -0x18(%rbp),%edx mov -0x14(%rbp),%eax mov %edx,%esi mov %eax,%edi callq 400912 <_Z11call_by_valii> |
可以看出,僅僅是將變數a、b的值存入了暫存器,而非”地址“或者能找到其”地址“的東西。
那麼,因為不滿足上文的兩個條件,所以不能交換。
這裡還有一點有趣的東西,也就是我們常聽說的拷貝(Copy):
當一個值,被放入暫存器或者堆疊中,其擁有了新的地址,那麼這個值就和其原來的實際地址沒有關係了,這種行為,是不是很像一種拷貝?
但實際上,在我看來,這是一個很誤導的術語,因為上面的按引用傳遞的call_by_ref實際上也是拷貝一種值,它是個地址,而且是實際地址。
所以,應該記住的是那兩個條件,在你還不能真正理解拷貝的意義之前最好不要用這個術語。
2、call_by_val_ptr
這種方式,本來是可以完成交換的,因為我們可以用指標來指向實際地址,這樣我們就滿足了條件1:
要有實際地址。
彆著急,我們先看下上文的實現中,為什麼沒有完成交換:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void call_by_val_ptr(int * p,int * q) { push %rbp mov %rsp,%rbp mov %rdi,-0x18(%rbp) mov %rsi,-0x20(%rbp) //int * t = p; mov -0x18(%rbp),%rax mov %rax,-0x8(%rbp) //p = q; mov -0x20(%rbp),%rax mov %rax,-0x18(%rbp) //q = t; mov -0x8(%rbp),%rax mov %rax,-0x20(%rbp) } |
可以看到,上面的邏輯和call_by_val非常類似,也只是做了將值放到暫存器這件事,那麼再看下傳給它的引數:
1 2 3 4 5 6 7 8 9 |
// c++程式碼 call_by_val_ptr(&a,&b); // 對應的彙編程式碼 lea -0x18(%rbp),%rdx lea -0x14(%rbp),%rax mov %rdx,%rsi mov %rax,%rdi callq 4008ec <_Z15call_by_val_ptrPiS_> |
注意到,lea是取地址,所以這裡實際也是將地址傳進去了,但為什麼沒有完成交換?
因為不滿足條件2:
將值存入實際地址。
call_by_val_ptr中的交換,從彙編程式碼就能看出,只是交換了指標指向的地址,而沒有通過將值存入這個地址而改變地址中的值。
Java
通過上面的例子,我們掌握了要完成交換的兩個條件,也瞭解了什麼是傳引用,什麼是傳值,從實際效果來講:
如果傳入的值,是實參的實際地址,那麼就可以認為是按引用傳遞。否則,就是按值傳遞。而實際上,傳值和傳引用在彙編層面或者機器碼層面沒有語義,因為都是將某個”值“丟給了暫存器或者堆疊。
所以,類似Java是按值傳遞還是按引用傳遞
這種問題,通常沒有任何意義,因為要看站在哪個抽象層次上看。
如果非要定義一下,好吧,也許你會在面試中碰到這種問題,那麼最好這樣回答:
Java是按值傳遞的,但可以達到按引用傳遞的效果。
那麼,什麼是有意義的呢?
上文的那兩個條件。
但從編譯的角度講,引用和地址有很強的關係,卻不是一回事。
Java按值傳遞的行為
我們回顧下開頭的三個問題中的第二個問題:
如下面的程式碼,為什麼不能進行交換?
1 2 3 4 5 6 |
public CallBy swap2(CallBy a,CallBy b) { CallBy t = a; a = b; b = t; return b; } |
我們首先從比較簡單的bytecode看起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public com.haoran.CallBy swap2(com.haoran.CallBy, com.haoran.CallBy); descriptor: (Lcom/haoran/CallBy;Lcom/haoran/CallBy;)Lcom/haoran/CallBy; flags: ACC_PUBLIC Code: stack=1, locals=4, args_size=3 0: aload_1 1: astore_3 2: aload_2 3: astore_1 4: aload_3 5: astore_2 6: aload_2 7: areturn LineNumberTable: line 45: 0 line 46: 2 line 47: 4 line 48: 6 LocalVariableTable: Start Length Slot Name Signature 0 8 0 this Lcom/haoran/CallBy; 0 8 1 a Lcom/haoran/CallBy; 0 8 2 b Lcom/haoran/CallBy; 2 6 3 t Lcom/haoran/CallBy; |
集中精力看這一塊:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//t = a 0: aload_1 1: astore_3 //a = b 2: aload_2 3: astore_1 //b = t 4: aload_3 5: astore_2 6: aload_2 |
程式碼很簡單,註釋也表明了在做什麼, 那麼需要不需要看傳遞給它什麼引數呢?
不需要,先看下彙編程式碼,滿足不滿足”將值放到實際地址“這個條件,我們擷取a = b這一句來觀察:
1 2 3 |
// a = b bytecode 2: aload_2 3: astore_1 |
1 2 3 4 5 6 7 8 9 10 11 |
/*************************************** * aload_2對應的彙編(未優化) ****************************************/ mov -0x10(%r14),%rax\n //---------------------------------------- movzbl 0x1(%r13),%ebx inc %r13 movabs $0x7ffff71ad900,%r10 jmpq *(%r10,%rbx,8) |
1 2 3 4 5 6 7 8 9 10 11 12 |
/*************************************** * astore_1對應的彙編(未優化) ****************************************/ pop %rax mov %rax,-0x8(%r14) //---------------------------------------- movzbl 0x1(%r13),%ebx inc %r13 movabs $0x7ffff71ae100,%r10 jmpq *(%r10,%rbx,8) |
如果將上面的程式碼和c++例項中的call_by_ref和call_by_val對比,就會發現,上面的程式碼缺失了這樣一種語義:
將值放入實際地址中。
其僅僅是將值放入暫存器或者堆疊上,並沒有將值放入實際地址這個操作。
為什麼不需要觀察給這個方法傳參的過程?
這是一個很簡單的必要條件問題,所以,不需要觀察。
從上面的過程來看,Java的行為是Call by Value的。
Java按引用傳遞的行為
現在來討論第三個問題:
如下面的程式碼,為什麼能夠交換成功?
1 2 3 4 5 6 |
public int swap2(CallBy a,CallBy b) { int t = a.value; a.value = b.value; b.value = t; return t; } |
還是從bytecode先看起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public com.haoran.CallBy swap2(com.haoran.CallBy, com.haoran.CallBy); descriptor: (Lcom/haoran/CallBy;Lcom/haoran/CallBy;)Lcom/haoran/CallBy; flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=3 0: aload_1 1: getfield #2 // Field value:I 4: istore_3 5: aload_1 6: aload_2 7: getfield #2 // Field value:I 10: putfield #2 // Field value:I 13: aload_2 14: iload_3 15: putfield #2 // Field value:I 18: aload_1 19: areturn LineNumberTable: line 41: 0 line 42: 5 line 43: 13 line 44: 18 LocalVariableTable: Start Length Slot Name Signature 0 20 0 this Lcom/haoran/CallBy; 0 20 1 a Lcom/haoran/CallBy; 0 20 2 b Lcom/haoran/CallBy; 5 15 3 t I |
聚焦putfield這句位元組碼,其對應的是在b.value的值放到運算元棧頂後,拿下這個值,賦 給a.value:
1 |
a.value = b.value; |
因為putfield位元組碼對應的彙編程式碼非常長,我們進一步聚焦其巨集彙編,因為CallBy類的value欄位,是個int型,所以我們關注itos:
1 2 3 4 5 6 7 8 9 10 11 |
// itos { __ pop(itos); if (!is_static) pop_and_check_object(obj); // 關鍵點:只看這裡 __ movl(field, rax); if (!is_static) { patch_bytecode(Bytecodes::_fast_iputfield, bc, rbx, true, byte_no); } __ jmp(Done); } |
其中關鍵的一句:__ movl(field, rax);
1 2 3 4 5 6 |
void Assembler::movl(Address dst, Register src) { InstructionMark im(this); prefix(dst, src); emit_int8((unsigned char)0x89); emit_operand(src, dst); } |
movl對應的彙編程式碼:
1 2 3 4 |
// 對應的關鍵彙編程式碼 ... mov %eax,(%rcx,%rbx,1) ... |
上面的彙編程式碼的意思是:
將eax中的值存入rcx + rbx*1所指向的地址處。
其中,eax的值就是b.value的值,而rcx+rbx*1所指向的地址就是a.value的地址。
上面的過程滿足了這樣的語義:
將值存入實際地址中.
所以,a.value和b.value可以交換,類似這樣,Java的按值傳遞方式也可以表現出按引用傳遞的行為。
另一種交換值的方式
還記得在C++的例項中,我們提到還有一種交換值的方式,是什麼呢?
call_by_WHAT
1 2 3 4 5 |
void call_by_WHAT(int * p,int * q) { int t = *p; *p = *q; *q = t; } |
這樣傳參:
1 2 3 4 5 6 7 8 9 10 11 |
int main() { int a = 3; int b = 4; cout << "---------- input ------------" << endl; cout << "a = " << a << ", b = " << b << endl << endl; call_by_WHAT(&a,&b); cout << "---------- call_by_WHAT ------------" << endl; cout << "a = " << a << ", b = " << b << endl; } |
會不會交換呢?
1 2 3 4 5 6 7 |
// 輸出 ---------- input ------------ a = 3, b = 4 ---------- call_by_WHAT ------------ a = 4, b = 3 |
從這種方式中,我們也看到了所有能夠交換值的方式的統一性:
1、指標p、q或者物件引用objectRef,能夠直接指向物件的實際地址。
2、要有一個將值放入實際地址的操作:
1 2 3 4 5 6 7 8 9 10 11 12 |
// C/C++ *p = *q; ... // Java putField -> a.value = b.value ... // 彙編 mov reg_src , (reg_dst) mov reg_src , (addr_dst) ... |
結語
老生常談,並無LUAN用,終。