淺談對象與引用
- 2020 年 3 月 26 日
- 筆記
對象與引用
new一個對象
最簡單的例子開始:
new Object();
簡單地講,new Object()就是創建了一個Object類型的實例(instance),分配在了JVM的堆內存中
以public方法作為示例,來看一下:
PS: 無論是public方法,還是private/protected/package方法,抑或是構造方法,甚至是在靜態代碼塊,靜態變量,實例變量,對於new Object這個動作來說,都是大同小異的
public class Test { public void fun1() { Object o = new Object(); } }
在類Test的fun1方法中實例化了一個Object,並賦值給一個Object類型的變量,當這個方法被調用時,發生了什麼?
1.執行javac Test.java
編譯為Test.class
文件
2.執行javap -v Test.class
,可以查看編譯後的.class
文件的位元組碼。這裡只列出了fun1
public void fun1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=1 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: astore_1 8: return LineNumberTable: line 3: 0 line 4: 8
重點關注Code部分
stack=2表示該方法需要深度為2的操作數棧
locals=2表示該方法需要2個Slot的局部變量空間
下面跟着的是偏移量以及對應的JVM指令集,我們可以一步一步分析這些指令集做了什麼事情
首先,初始化的操作數棧和局部變量空間是這樣的:
指令集 | 對應的CODE |
---|---|
0: new,創建一個java.lang.Object類型的實例,並將它的引用值壓入操作數棧的棧頂(PS:這個引用值並不是指Object o )操作數棧:[空閑], [objectref] 局部變量表:[this], [空閑] |
new Object() |
3: dup,複製操作數棧的棧頂數值,並將數值壓入操作數棧的棧頂 操作數棧:[objectref], [objectref] 局部變量表:[this], [空閑] |
new Object() |
4: invokespecial,調用java.lang.Object的" 操作數棧:[空閑], [objectref] 局部變量表:[this], [空閑] |
new Object() |
7: astore_1,將棧頂引用值存入第二個本地變量 操作數棧:[空閑], [空閑] 局部變量表:[this], [objectref] |
Object o = new Object(),主要是這個賦值操作符 |
8: return,從當前方法返回void |
從上面的步驟分析,可以發現,在方法中簡單的一個new Object動作,JVM執行了3個指令,分別是:
- 創建對象並將引用值入棧
- 複製棧頂數值
- 調用超類構造方法
這個引用值objectref比較容易引起歧義,我們通常說的引用是指Object o = new Object()
中,賦予操作符左邊的Object o,要注意的是,這句話並不是創建一個引用,而是將Object實例的引用,存入本地變量中
賦值 VS 不賦值
對象的創建時用來使用的,看一下這種情況
Object o = new Object(); o.toString();
創建一個Object類型的實例,然後調用它的toString
方法
同樣的寫法,還可以是這種方式:
new Object().toString();
這兩種方式有什麼區別嗎?通過JVM指令來觀察一下
源碼:
public class Test { public void invokeWithoutReference() { new Object().toString(); } public void invokeWithReference() { Object o = new Object(); o.toString(); } }
指令集(javap -v Test.class
只保留了指令集部分):
public void invokeWithoutReference(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>": ()V 7: invokevirtual #3 // Method java/lang/Object.toString:()Ljava/lang/String; 10: pop 11: return public void invokeWithReference(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=1 0: new #2 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>": ()V 7: astore_1 8: aload_1 9: invokevirtual #3 // Method java/lang/Object.toString:()Ljava/lang/String; 12: pop 13: return
有reference和沒有reference的區別就是,在invokeWithReference
中,生成Object
實例後,執行了astore_1
和aload_1
兩個指令,其中:
astore_1
表示將棧頂引用值存入局部變量表中第二個Slot,代表了賦值操作符(=
)做的事情
aload_1
表示,將第二個引用類型局部變量推到操作數棧的棧頂
具體來看一下兩個不同方法的指令集執行過程:
invokeWithoutReference
指令集 | 對應的CODE |
---|---|
0: new,創建一個java.lang.Object類型的實例,並將它的引用值壓入操作數棧的棧頂 操作數棧:[空閑], [objectref] 局部變量表:[this] |
new Object() |
3: dup,複製操作數棧的棧頂數值,並將數值壓入操作數棧的棧頂 操作數棧:[objectref], [objectref] 局部變量表:[this] |
new Object() |
4: invokespecial,調用java.lang.Object的" 操作數棧:[空閑], [objectref] 局部變量表:[this] |
new Object() |
7: invokevirtual,調用java.lang.Object的toString方法,因為toString方法有返回值,所以這裡會將執行的結果推入棧頂 操作數棧:[空閑], [java.lang.String] 局部變量表:[this] |
new Object().toString(); |
10: pop,將棧頂數值彈出 操作數棧:[空閑], [空閑] 局部變量表:[this] |
new Object().toString(); |
11: return,從當前方法返回void |
invokeWithReference
指令集 | 對應的CODE |
---|---|
0: new,創建一個java.lang.Object類型的實例,並將它的引用值壓入操作數棧的棧頂 操作數棧:[空閑], [objectref] 局部變量表:[this], [空閑] |
new Object() |
3: dup,複製操作數棧的棧頂數值,並將數值壓入操作數棧的棧頂 操作數棧:[objectref], [objectref] 局部變量表:[this], [空閑] |
new Object() |
4: invokespecial,調用java.lang.Object的" 操作數棧:[空閑], [objectref] 局部變量表:[this], [空閑] |
new Object() |
7: astore_1,將棧頂引用值存入第二個本地變量 操作數棧:[空閑], [空閑] 局部變量表:[this], [objectref] |
Object o = new Object(); |
8: aload_1,將第二個本地變量推入棧頂 操作數棧:[空閑], [objectref] 局部變量表:[this], [objectref] |
|
9: invokevirtual,調用java.lang.Object的toString方法,因為toString方法有返回值,所以這裡會將執行的結果推入棧頂 操作數棧:[空閑], [java.lang.String] 局部變量表:[this], [objectref] |
new Object().toString(); |
12: pop,將棧頂數值彈出 操作數棧:[空閑], [空閑] 局部變量表:[this], [objectref] |
new Object().toString(); |
13: return,從當前方法返回void |
引用?
首先,什麼是引用?
《深入理解JVM虛擬機》一書中多次對Java的引用進行了討論
對象引用(reference類型,它不等同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)——《深入理解JVM虛擬機》 2.2.2 Java虛擬機棧
建立對象是為了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。——《深入理解JVM虛擬機》 2.3.3 對象的訪問定位
一般來說,虛擬機實現至少都應當能通過這個引用做到兩點,一是從此引用中直接或間接地查找到對象在Java堆中地數據存放地起始地址索引,二是此引用中直接或間接地查找到對象所屬數據類型在方法區中的存儲的類型信息 ——《深入理解JVM虛擬機》 8.2.1 局部變量表
對於new Object()
來說,JVM執行的new(0xbb)
指令天然的就會將新實例的引用壓入操作數棧的棧頂
而Object o = new Object()
只是利用=
運算符,讓JVM執行了astore_n
指令,將這個引用保存到了局部變量表中,以便我們以後可以直接通過o.xxx()
來對這個實例做一些操作
等到我們需要使用的時候,JVM再通過aload_n
將指定的局部變量表中的引用類型值推到操作數棧的棧頂進行後續操作
所以在我看來,Object o
其實是一個引用類型的本地變量
創建對象到底賦值嗎?
回到初衷,是否定義一個引用類型的本地變量,沒有一個絕對的優劣
Object o = new Object()
僅僅是比new Object()
多在局部變量表中保存了一個Object o
引用類型,但它可以讓我們在創建了實例之後,重複對這個實例進行操作
new Object()
在進行了new Object().toString()
這種方式的調用之後,由於局部變量表中沒有了該實例的引用,操作數棧中的那個兩個由dup
產生的兩個引用,也已經分別因為invokespecial
和invokevirtual
彈出棧了,所以這個對象已經沒有指向它的引用了
如果我們對於實例只是一次性調用,那麼直接new Object()
的方式也未嘗不可