1. 程式人生 > >淺談物件與引用

淺談物件與引用

物件與引用

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指令集,我們可以一步一步分析這些指令集做了什麼事情

首先,初始化的運算元棧和區域性變數空間是這樣的:
運算元棧:[空閒], [空閒]
區域性變量表:[this], [空閒]

下面來逐個指令分析,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_1aload_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產生的兩個引用,也已經分別因為invokespecialinvokevirtual彈出棧了,所以這個物件已經沒有指向它的引用了

如果我們對於例項只是一次性呼叫,那麼直接new Object()的方式也未嘗不