摘要:下文將從字節(jié)碼的角度,分析中基本類型傳參和對(duì)象傳參。主函數(shù)執(zhí)行時(shí),操作棧會(huì)推入主函數(shù)棧幀,其中包含了主函數(shù)的局部變量表,字節(jié)碼,返回值等信息。主函數(shù)的棧幀會(huì)被推入棧,成為當(dāng)前操作棧。
一個(gè)小問(wèn)題個(gè)人網(wǎng)站地址: http://kailuncen.me/2017/06/0...
在開(kāi)源中國(guó)看到這樣一則問(wèn)題
https://www.oschina.net/quest...,其中的變量a前后的輸出是什么?
我答錯(cuò)了,我認(rèn)為傳入function的就是main函數(shù)中的a,在function中修改了a的地址,因此回到主函數(shù)后,a的地址已經(jīng)變成了function中所賦予的a2的地址,因此經(jīng)過(guò)function處理后a的值已經(jīng)改變了。
但結(jié)果并不是,因?yàn)槲液雎粤薐ava的基礎(chǔ)知識(shí)點(diǎn)之一。
Java中傳參都是值傳遞,如果是基本類型,就是對(duì)值的拷貝,如果是對(duì)象,就是對(duì)引用地址的拷貝。
下文將從字節(jié)碼的角度,分析Java中基本類型傳參和對(duì)象傳參。
基本類型傳參以下是處理類Porcess,代碼應(yīng)該已經(jīng)能夠自解釋了。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; } }
我們繼續(xù)看測(cè)試類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); } }
結(jié)果是在經(jīng)過(guò)function3的處理后,輸出結(jié)果是
18 18
修改測(cè)試類代碼,在經(jīng)過(guò)function4處理后,仍然一致。
18 18
結(jié)論: 基本類型的傳參,對(duì)傳參進(jìn)行修改,不影響原本參數(shù)的值。
對(duì)象類型傳參以下是處理類Porcess,function1,將參數(shù)car的顏色設(shè)置成blue。function2,新建了car2,將car2賦值給了參數(shù)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"); } }
我們繼續(xù)看測(cè)試類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); } }
結(jié)果是在經(jīng)過(guò)function1的處理后,輸出結(jié)果是
Car{color="red"} Car{color="blue"}
修改測(cè)試類,在經(jīng)過(guò)function2的處理后
Car{color="red"} Car{color="red"}
結(jié)論: 對(duì)象類型的傳參,直接調(diào)用傳參set方法,可以對(duì)原本參數(shù)進(jìn)行修改。如果修改傳參的指向地址,調(diào)用傳參的set方法,無(wú)法對(duì)原本參數(shù)的值進(jìn)行修改。
綜上所述,基本類型的傳參,在方法內(nèi)部是值拷貝,有一個(gè)新的局部變量得到這個(gè)值,對(duì)這個(gè)局部變量的修改不影響原來(lái)的參數(shù)。對(duì)象類型的傳參,傳遞的是堆上的地址,在方法內(nèi)部是有一個(gè)新的局部變量得到引用地址的拷貝,對(duì)該局部變量的操作,影響的是同一塊地址,因此原本的參數(shù)也會(huì)受影響,反之,若修改局部變量的引用地址,則不會(huì)對(duì)原本的參數(shù)產(chǎn)生任何可能的影響。
上文已經(jīng)得到結(jié)論,我們從JVM的字節(jié)碼的角度看一下過(guò)程是怎么樣的。
首先大致JVM的基本結(jié)構(gòu),對(duì)基本類型,和對(duì)象存放的位置有一個(gè)大致的了解。下圖是JVM的基本組件圖。
介紹幾個(gè)基本的組件
程序計(jì)數(shù)器: 存儲(chǔ)每個(gè)線程下一步將執(zhí)行的JVM指令。
JVM棧(JVM Stack): JVM棧是線程私有的,每個(gè)線程創(chuàng)建的同時(shí)都會(huì)創(chuàng)建JVM棧,JVM棧中存放的為當(dāng)前線程中局部基本類型的變量(java中定義的八種基本類型:boolean、char、byte、short、int、long、float、double)、部分的返回結(jié)果以及Stack Frame(每個(gè)方法都會(huì)開(kāi)辟一個(gè)自己的棧幀),非基本類型的對(duì)象在JVM棧上僅存放一個(gè)指向堆上的地址
堆(heap): JVM用來(lái)存儲(chǔ)對(duì)象實(shí)例以及數(shù)組值的區(qū)域,可以認(rèn)為Java中所有通過(guò)new創(chuàng)建的對(duì)象的內(nèi)存都在此分配,Heap中的對(duì)象的內(nèi)存需要等待GC進(jìn)行回收。
方法區(qū)(Method Area): 方法區(qū)域存放了所加載的類的信息(名稱、修飾符等)、類中的靜態(tài)變量、類中定義為final類型的常量、類中的Field信息、類中的方法信息,當(dāng)開(kāi)發(fā)人員在程序中通過(guò)Class對(duì)象中的getName、isInterface等方法來(lái)獲取信息時(shí),這些數(shù)據(jù)都來(lái)源于方法區(qū)域。
本地方法棧(Native Method Stacks): JVM采用本地方法棧來(lái)支持native方法的執(zhí)行,此區(qū)域用于存儲(chǔ)每個(gè)native方法調(diào)用的狀態(tài)。
運(yùn)行時(shí)常量池(Runtime Constant Pool): 存放的為類中的固定的常量信息、方法和Field的引用信息等,其空間從方法區(qū)域中分配。JVM在加載類時(shí)會(huì)為每個(gè)class分配一個(gè)獨(dú)立的常量池,但是運(yùn)行時(shí)常量池中的字符串常量池是全局共享的。
下圖是從另一個(gè)角度解析JVM的結(jié)構(gòu),JVM是基于棧來(lái)操作的,每一個(gè)線程有自己的操作棧,遇到方法調(diào)用時(shí)會(huì)開(kāi)辟棧幀,它含有自己的返回值,局部變量表,操作棧,以及對(duì)常量池的符號(hào)引用。
如果是基本類型,則存放在棧里的是值,如果是對(duì)象,存放在棧上是對(duì)象在堆上存放的地址。
了解了JVM的基本結(jié)構(gòu),我們來(lái)看一下上述的兩種代碼,一種是基本類型傳參,一種是對(duì)象傳參,在字節(jié)碼表現(xiàn)上的不同。
使用javap對(duì)字節(jié)碼進(jìn)行反編譯
javap -verbose Main基本類型傳參字節(jié)碼
以下是TestPrimitive類在執(zhí)行function3時(shí)的字節(jié)碼。
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
主函數(shù)執(zhí)行時(shí),JVM操作棧會(huì)推入主函數(shù)棧幀,其中包含了主函數(shù)的局部變量表,字節(jié)碼,返回值等信息。LocalVariableTable就是局部變量表,以0為索引起點(diǎn),第0個(gè)是局部變量String數(shù)組 args,第1個(gè)是局部變量process,保存新創(chuàng)建的Process對(duì)象的引用地址。第2個(gè)是局部變量age。在字節(jié)碼第8行,通過(guò)bipush 18,將常量18直接壓入操作棧,然后第20行,是調(diào)用了process的function3方法,傳入了age作為參數(shù)。
然后JVM操作棧將function3棧幀推入JVM棧,使得function3棧幀成為當(dāng)前棧幀,開(kāi)始執(zhí)行。
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
字節(jié)碼顯示,通過(guò)iconst_2,istore_1,將基本類型2推入棧,并保存在局部變量a中,這里就展示了我們?cè)诜椒▋?nèi)部的修改都是對(duì)function3的局部變量a的值修改,不影響主函數(shù)中的a。從主函數(shù)的字節(jié)碼中可以看到,它的值保存的還是第10行,通過(guò)istore_2保存到局部變量第2個(gè)索引處的18.
如果用圖示來(lái)表示上述字節(jié)碼執(zhí)行過(guò)程中,JVM棧,man函數(shù)棧幀,function3棧幀內(nèi)部變化的話,如下圖所示。
1.主函數(shù)的棧幀會(huì)被推入JVM棧,成為當(dāng)前操作棧。
2.然后進(jìn)去main函數(shù)棧幀,初始化完畢后如下圖所示。
3.主要看bipush 18,將基本變量18推入操作棧,基本變量類型是存儲(chǔ)在棧幀內(nèi)部的。
4.然后執(zhí)行istore_2, 將棧頂出棧,并且保存在局部變量索引2處。
5.然繼續(xù)執(zhí)行至18: aload_1,,將創(chuàng)建的process的地址保存在局部變量索引1處,19:iload_2,將局部變量2處保存的基本類型壓入棧。
6.然后執(zhí)行至20:invokevirtula #6,也就是調(diào)用function3,進(jìn)入function3的棧幀。執(zhí)行0: iconst_2,將常量2推入棧,此時(shí)function3的棧幀有一個(gè)局部變量1處保存著傳入的參數(shù)18。
7.繼續(xù)執(zhí)行1:istore_1,將棧頂推出,保存在局部變量1處,覆蓋了傳入的參數(shù)18,然后return,將function3函數(shù)棧幀彈出JVM棧,繼續(xù)執(zhí)行main函數(shù)棧幀。
之后會(huì)繼續(xù)執(zhí)行main函數(shù)棧幀,在function3函數(shù)棧幀中發(fā)生的一切都和Main Stack中的局部變量age的值沒(méi)有任何關(guān)系。
對(duì)象類型傳參字節(jié)碼以下是TestReference類在執(zhí)行function2時(shí)的字節(jié)碼。
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;
我們可以通過(guò)字節(jié)碼14-17行,看到局部變量索引2處存放的是Car的實(shí)例在堆上的地址,這和基本類型不同,基本類型的值都是直接存放在棧里面的。然后通過(guò)字節(jié)碼第27行將car的引用地址傳入function2。接下來(lái)我們看看function2的字節(jié)碼。
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;
題外話,因?yàn)檫@個(gè)是調(diào)用具體實(shí)例的函數(shù),所以索引0處保存的是實(shí)例的引用。索引1保存的是傳參car的引用地址,car2保存的是函數(shù)內(nèi)創(chuàng)建的Car實(shí)例的地址。字節(jié)碼0-9,完成了car2的引用地址保存,第10行將Car2的引用地址推入棧,第11行通過(guò)astore_1,將棧頂值保存到第一個(gè)局部變量,也就是修改了覆蓋了局部變量car的引用地址。因此第15行,修改的是car當(dāng)前引用的地址的實(shí)例的參數(shù)值。當(dāng)退出棧幀,回到主函數(shù),主函數(shù)的局部變量a保存的引用地址沒(méi)有改變。
如果用圖示來(lái)表示上述字節(jié)碼執(zhí)行過(guò)程中,JVM棧,man函數(shù)棧幀,function3棧幀內(nèi)部變化的話,如下圖所示。
1.main函數(shù)棧幀和上文測(cè)試基本類型傳參時(shí)的字節(jié)碼大致類似,不同的是局部變量處。局部變量2處保存的是main函數(shù)中新建的Car實(shí)例的堆上地址。對(duì)象的實(shí)際存放都是在堆中,棧幀的局部變量中保存的是他們?cè)诙焉系牡刂贰?/p>
2.一直執(zhí)行到調(diào)用function2,進(jìn)入function2棧幀。在執(zhí)行至9:astore_2時(shí),棧中新創(chuàng)建的Car實(shí)例的引用地址出棧,保存在局部變量2處。局部變量1保存的是傳參進(jìn)來(lái)的Car實(shí)例的引用地址。
3.然后執(zhí)行至10: aload_2,11:store_1,在這里,1236df被推入棧,然后保存在了局部變量1,覆蓋了局部變量car本來(lái)的引用地址。
**
因此,當(dāng)function2對(duì)局部變量2進(jìn)行相關(guān)操作時(shí),影響的都是1236df這塊地址,和main函數(shù)局部變量car中保存的1235df不是一塊地址,所以前后打印結(jié)果一致。**
測(cè)試類TestReference調(diào)用function1時(shí),function1沒(méi)有改變局部變量car的引用地址,保存的仍然是傳入的引用地址,所以function1中car進(jìn)行的操作影響了這塊地址保存的內(nèi)容,導(dǎo)致了前后打印結(jié)果不一致。
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;
本文對(duì)Java基本類型傳參和對(duì)象傳參,從字節(jié)碼角度進(jìn)行了分析,現(xiàn)在不會(huì)再搞錯(cuò)了吧~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/70142.html
摘要:性能,大量運(yùn)用在哈希的處理中,由于的不可變性,可以只計(jì)算一次哈希值,然后緩存在內(nèi)部,后續(xù)直接取就好了。這是目前的一個(gè)底層字節(jié)碼的實(shí)現(xiàn),那么是不是沒(méi)有使用或者的必要了呢。 凱倫說(shuō),公眾號(hào)ID: KailunTalk,努力寫(xiě)出最優(yōu)質(zhì)的技術(shù)文章,歡迎關(guān)注探討。 1. 前言 最近看到幾個(gè)有趣的關(guān)于Java核心類String的問(wèn)題。 String類是如何實(shí)現(xiàn)其不可變的特性的,設(shè)計(jì)成不可變的好處...
摘要:大多數(shù)待遇豐厚的開(kāi)發(fā)職位都要求開(kāi)發(fā)者精通多線程技術(shù)并且有豐富的程序開(kāi)發(fā)調(diào)試優(yōu)化經(jīng)驗(yàn),所以線程相關(guān)的問(wèn)題在面試中經(jīng)常會(huì)被提到。將對(duì)象編碼為字節(jié)流稱之為序列化,反之將字節(jié)流重建成對(duì)象稱之為反序列化。 JVM 內(nèi)存溢出實(shí)例 - 實(shí)戰(zhàn) JVM(二) 介紹 JVM 內(nèi)存溢出產(chǎn)生情況分析 Java - 注解詳解 詳細(xì)介紹 Java 注解的使用,有利于學(xué)習(xí)編譯時(shí)注解 Java 程序員快速上手 Kot...
摘要:前言本文內(nèi)容基本摘抄自深入理解虛擬機(jī),以供復(fù)習(xí)之用,沒(méi)有多少參考價(jià)值。此區(qū)域是唯一一個(gè)在虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何情況的區(qū)域。堆是所有線程共享的內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。虛擬機(jī)上把方法區(qū)稱為永久代。 前言 本文內(nèi)容基本摘抄自《深入理解Java虛擬機(jī)》,以供復(fù)習(xí)之用,沒(méi)有多少參考價(jià)值。想要更詳細(xì)了解請(qǐng)參考原書(shū)。 第二章 1.運(yùn)行時(shí)數(shù)據(jù)區(qū)域 showImg(https://segment...
摘要:也正是因此,一旦出現(xiàn)內(nèi)存泄漏或溢出問(wèn)題,如果不了解的內(nèi)存管理原理,那么將會(huì)對(duì)問(wèn)題的排查帶來(lái)極大的困難。 本文已收錄【修煉內(nèi)功】躍遷之路 showImg(https://segmentfault.com/img/bVbsP9I?w=1024&h=580); 不論做技術(shù)還是做業(yè)務(wù),對(duì)于Java開(kāi)發(fā)人員來(lái)講,理解JVM各種原理的重要性不必再多言 對(duì)于C/C++而言,可以輕易地操作任意地址的...
閱讀 3698·2021-11-22 15:24
閱讀 1606·2021-09-26 09:46
閱讀 1919·2021-09-14 18:01
閱讀 2614·2019-08-30 15:45
閱讀 3532·2019-08-30 14:23
閱讀 1881·2019-08-30 12:43
閱讀 2919·2019-08-30 10:56
閱讀 805·2019-08-29 12:20