浅谈对象与引用

对象与引用

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的"": ()v方法
操作数栈:[空闲], [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的"": ()v方法
操作数栈:[空闲], [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的"": ()v方法
操作数栈:[空闲], [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()的方式也未尝不可

posted @ 2020-03-26 15:48  大地的谎言  阅读(...)  评论(...编辑  收藏