摘要:下文將從字節碼的角度,分析中基本類型傳參和對象傳參。主函數執行時,操作棧會推入主函數棧幀,其中包含了主函數的局部變量表,字節碼,返回值等信息。主函數的棧幀會被推入棧,成為當前操作棧。
一個小問題個人網站地址: http://kailuncen.me/2017/06/0...
在開源中國看到這樣一則問題
https://www.oschina.net/quest...,其中的變量a前后的輸出是什么?
我答錯了,我認為傳入function的就是main函數中的a,在function中修改了a的地址,因此回到主函數后,a的地址已經變成了function中所賦予的a2的地址,因此經過function處理后a的值已經改變了。
但結果并不是,因為我忽略了Java的基礎知識點之一。
Java中傳參都是值傳遞,如果是基本類型,就是對值的拷貝,如果是對象,就是對引用地址的拷貝。
下文將從字節碼的角度,分析Java中基本類型傳參和對象傳參。
基本類型傳參以下是處理類Porcess,代碼應該已經能夠自解釋了。function1是將傳參a變成2,function2是初始化int b,賦值為5,然后將b賦值給a。
public class Process { public void function3(int a) { a = 2; } public void function4(int a) { int b = 5; a = b; } }
我們繼續看測試類TestPrimitive
public class TestPrimitive { public static void main(String[] args) { Process process = new Process(); int age = 18; System.out.println(age); process.function3(age); System.out.println(age); } }
結果是在經過function3的處理后,輸出結果是
18 18
修改測試類代碼,在經過function4處理后,仍然一致。
18 18
結論: 基本類型的傳參,對傳參進行修改,不影響原本參數的值。
對象類型傳參以下是處理類Porcess,function1,將參數car的顏色設置成blue。function2,新建了car2,將car2賦值給了參數car。
public class Process { public void function1(Car car) { car.setColor("blue"); } public void function2(Car car) { Car car2 = new Car("black"); car = car2; car.setColor("orange"); } }
我們繼續看測試類TestReference
public class TestReference { public static void main(String[] args) { Process process = new Process(); Car car = new Car("red"); System.out.println(car); process.function1(car); System.out.println(car); } }
結果是在經過function1的處理后,輸出結果是
Car{color="red"} Car{color="blue"}
修改測試類,在經過function2的處理后
Car{color="red"} Car{color="red"}
結論: 對象類型的傳參,直接調用傳參set方法,可以對原本參數進行修改。如果修改傳參的指向地址,調用傳參的set方法,無法對原本參數的值進行修改。
綜上所述,基本類型的傳參,在方法內部是值拷貝,有一個新的局部變量得到這個值,對這個局部變量的修改不影響原來的參數。對象類型的傳參,傳遞的是堆上的地址,在方法內部是有一個新的局部變量得到引用地址的拷貝,對該局部變量的操作,影響的是同一塊地址,因此原本的參數也會受影響,反之,若修改局部變量的引用地址,則不會對原本的參數產生任何可能的影響。
上文已經得到結論,我們從JVM的字節碼的角度看一下過程是怎么樣的。
首先大致JVM的基本結構,對基本類型,和對象存放的位置有一個大致的了解。下圖是JVM的基本組件圖。
介紹幾個基本的組件
程序計數器: 存儲每個線程下一步將執行的JVM指令。
JVM棧(JVM Stack): JVM棧是線程私有的,每個線程創建的同時都會創建JVM棧,JVM棧中存放的為當前線程中局部基本類型的變量(java中定義的八種基本類型:boolean、char、byte、short、int、long、float、double)、部分的返回結果以及Stack Frame(每個方法都會開辟一個自己的棧幀),非基本類型的對象在JVM棧上僅存放一個指向堆上的地址
堆(heap): JVM用來存儲對象實例以及數組值的區域,可以認為Java中所有通過new創建的對象的內存都在此分配,Heap中的對象的內存需要等待GC進行回收。
方法區(Method Area): 方法區域存放了所加載的類的信息(名稱、修飾符等)、類中的靜態變量、類中定義為final類型的常量、類中的Field信息、類中的方法信息,當開發人員在程序中通過Class對象中的getName、isInterface等方法來獲取信息時,這些數據都來源于方法區域。
本地方法棧(Native Method Stacks): JVM采用本地方法棧來支持native方法的執行,此區域用于存儲每個native方法調用的狀態。
運行時常量池(Runtime Constant Pool): 存放的為類中的固定的常量信息、方法和Field的引用信息等,其空間從方法區域中分配。JVM在加載類時會為每個class分配一個獨立的常量池,但是運行時常量池中的字符串常量池是全局共享的。
下圖是從另一個角度解析JVM的結構,JVM是基于棧來操作的,每一個線程有自己的操作棧,遇到方法調用時會開辟棧幀,它含有自己的返回值,局部變量表,操作棧,以及對常量池的符號引用。
如果是基本類型,則存放在棧里的是值,如果是對象,存放在棧上是對象在堆上存放的地址。
了解了JVM的基本結構,我們來看一下上述的兩種代碼,一種是基本類型傳參,一種是對象傳參,在字節碼表現上的不同。
使用javap對字節碼進行反編譯
javap -verbose Main基本類型傳參字節碼
以下是TestPrimitive類在執行function3時的字節碼。
public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class Process 3: dup 4: invokespecial #3 // Method Process."":()V 7: astore_1 8: bipush 18 10: istore_2 11: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 14: iload_2 15: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 18: aload_1 19: iload_2 20: invokevirtual #6 // Method Process.function3:(I)V 23: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 26: iload_2 27: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 30: return LineNumberTable: ........... LocalVariableTable: Start Length Slot Name Signature 0 31 0 args [Ljava/lang/String; 8 23 1 process LProcess; 11 20 2 age I
主函數執行時,JVM操作棧會推入主函數棧幀,其中包含了主函數的局部變量表,字節碼,返回值等信息。LocalVariableTable就是局部變量表,以0為索引起點,第0個是局部變量String數組 args,第1個是局部變量process,保存新創建的Process對象的引用地址。第2個是局部變量age。在字節碼第8行,通過bipush 18,將常量18直接壓入操作棧,然后第20行,是調用了process的function3方法,傳入了age作為參數。
然后JVM操作棧將function3棧幀推入JVM棧,使得function3棧幀成為當前棧幀,開始執行。
public void function3(int); flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: iconst_2 1: istore_1 2: return LocalVariableTable: Start Length Slot Name Signature 0 3 0 this LProcess; 0 3 1 a I
字節碼顯示,通過iconst_2,istore_1,將基本類型2推入棧,并保存在局部變量a中,這里就展示了我們在方法內部的修改都是對function3的局部變量a的值修改,不影響主函數中的a。從主函數的字節碼中可以看到,它的值保存的還是第10行,通過istore_2保存到局部變量第2個索引處的18.
如果用圖示來表示上述字節碼執行過程中,JVM棧,man函數棧幀,function3棧幀內部變化的話,如下圖所示。
1.主函數的棧幀會被推入JVM棧,成為當前操作棧。
2.然后進去main函數棧幀,初始化完畢后如下圖所示。
3.主要看bipush 18,將基本變量18推入操作棧,基本變量類型是存儲在棧幀內部的。
4.然后執行istore_2, 將棧頂出棧,并且保存在局部變量索引2處。
5.然繼續執行至18: aload_1,,將創建的process的地址保存在局部變量索引1處,19:iload_2,將局部變量2處保存的基本類型壓入棧。
6.然后執行至20:invokevirtula #6,也就是調用function3,進入function3的棧幀。執行0: iconst_2,將常量2推入棧,此時function3的棧幀有一個局部變量1處保存著傳入的參數18。
7.繼續執行1:istore_1,將棧頂推出,保存在局部變量1處,覆蓋了傳入的參數18,然后return,將function3函數棧幀彈出JVM棧,繼續執行main函數棧幀。
之后會繼續執行main函數棧幀,在function3函數棧幀中發生的一切都和Main Stack中的局部變量age的值沒有任何關系。
對象類型傳參字節碼以下是TestReference類在執行function2時的字節碼。
Code: stack=3, locals=3, args_size=1 0: new #2 // class Process 3: dup 4: invokespecial #3 // Method Process."":()V 7: astore_1 8: new #4 // class Car 11: dup 12: ldc #5 // String red 14: invokespecial #6 // Method Car." ":(Ljava/lang/String;)V 17: astore_2 18: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 21: aload_2 22: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 25: aload_1 26: aload_2 27: invokevirtual #9 // Method Process.function2:(LCar;)V 30: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 33: aload_2 34: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 37: return LocalVariableTable: Start Length Slot Name Signature 0 38 0 args [Ljava/lang/String; 8 30 1 process LProcess; 18 20 2 car LCar;
我們可以通過字節碼14-17行,看到局部變量索引2處存放的是Car的實例在堆上的地址,這和基本類型不同,基本類型的值都是直接存放在棧里面的。然后通過字節碼第27行將car的引用地址傳入function2。接下來我們看看function2的字節碼。
public void function2(Car); flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=2 0: new #4 // class Car 3: dup 4: ldc #5 // String black 6: invokespecial #6 // Method Car."":(Ljava/lang/String;)V 9: astore_2 10: aload_2 11: astore_1 12: aload_1 13: ldc #7 // String orange 15: invokevirtual #3 // Method Car.setColor:(Ljava/lang/String;)V 18: return LocalVariableTable: Start Length Slot Name Signature 0 13 0 this LProcess; 0 13 1 car LCar; 10 3 2 car2 LCar;
題外話,因為這個是調用具體實例的函數,所以索引0處保存的是實例的引用。索引1保存的是傳參car的引用地址,car2保存的是函數內創建的Car實例的地址。字節碼0-9,完成了car2的引用地址保存,第10行將Car2的引用地址推入棧,第11行通過astore_1,將棧頂值保存到第一個局部變量,也就是修改了覆蓋了局部變量car的引用地址。因此第15行,修改的是car當前引用的地址的實例的參數值。當退出棧幀,回到主函數,主函數的局部變量a保存的引用地址沒有改變。
如果用圖示來表示上述字節碼執行過程中,JVM棧,man函數棧幀,function3棧幀內部變化的話,如下圖所示。
1.main函數棧幀和上文測試基本類型傳參時的字節碼大致類似,不同的是局部變量處。局部變量2處保存的是main函數中新建的Car實例的堆上地址。對象的實際存放都是在堆中,棧幀的局部變量中保存的是他們在堆上的地址。
2.一直執行到調用function2,進入function2棧幀。在執行至9:astore_2時,棧中新創建的Car實例的引用地址出棧,保存在局部變量2處。局部變量1保存的是傳參進來的Car實例的引用地址。
3.然后執行至10: aload_2,11:store_1,在這里,1236df被推入棧,然后保存在了局部變量1,覆蓋了局部變量car本來的引用地址。
**
因此,當function2對局部變量2進行相關操作時,影響的都是1236df這塊地址,和main函數局部變量car中保存的1235df不是一塊地址,所以前后打印結果一致。**
測試類TestReference調用function1時,function1沒有改變局部變量car的引用地址,保存的仍然是傳入的引用地址,所以function1中car進行的操作影響了這塊地址保存的內容,導致了前后打印結果不一致。
Code: stack=2, locals=2, args_size=2 0: aload_1 1: ldc #2 // String blue 3: invokevirtual #3 // Method Car.setColor:(Ljava/lang/String;)V 6: return LocalVariableTable: Start Length Slot Name Signature 0 7 0 this LProcess; 0 7 1 car LCar;
本文對Java基本類型傳參和對象傳參,從字節碼角度進行了分析,現在不會再搞錯了吧~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70142.html
摘要:性能,大量運用在哈希的處理中,由于的不可變性,可以只計算一次哈希值,然后緩存在內部,后續直接取就好了。這是目前的一個底層字節碼的實現,那么是不是沒有使用或者的必要了呢。 凱倫說,公眾號ID: KailunTalk,努力寫出最優質的技術文章,歡迎關注探討。 1. 前言 最近看到幾個有趣的關于Java核心類String的問題。 String類是如何實現其不可變的特性的,設計成不可變的好處...
摘要:前言本文內容基本摘抄自深入理解虛擬機,以供復習之用,沒有多少參考價值。此區域是唯一一個在虛擬機規范中沒有規定任何情況的區域。堆是所有線程共享的內存區域,在虛擬機啟動時創建。虛擬機上把方法區稱為永久代。 前言 本文內容基本摘抄自《深入理解Java虛擬機》,以供復習之用,沒有多少參考價值。想要更詳細了解請參考原書。 第二章 1.運行時數據區域 showImg(https://segment...
摘要:也正是因此,一旦出現內存泄漏或溢出問題,如果不了解的內存管理原理,那么將會對問題的排查帶來極大的困難。 本文已收錄【修煉內功】躍遷之路 showImg(https://segmentfault.com/img/bVbsP9I?w=1024&h=580); 不論做技術還是做業務,對于Java開發人員來講,理解JVM各種原理的重要性不必再多言 對于C/C++而言,可以輕易地操作任意地址的...
閱讀 3690·2021-11-22 15:24
閱讀 1593·2021-09-26 09:46
閱讀 1904·2021-09-14 18:01
閱讀 2601·2019-08-30 15:45
閱讀 3526·2019-08-30 14:23
閱讀 1866·2019-08-30 12:43
閱讀 2915·2019-08-30 10:56
閱讀 800·2019-08-29 12:20