借助HSDB工具解析JVM中的对象及this&super关键字

this 关键字

this 关键字是什么? this 指代的是被调用的实例方法的所有者,称为方法接收者(Receiver),以下代码中对于 test() 方法来说,person 就是接收者。

public class ThisTest {

    static class Person{
        private int anInt;
        private static int anStaticInt;
        
        public Person(){
            
        }

        public static Object staticTest(){
            anStaticInt = 456;
            return 0;
        }

        public Object test(){
            int i = 1;
            this.anInt = 123;
            return 1;
        }
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.test();
    }
}

在实例方法中,我们可以通过 this 关键字,访问当前类中的所有实例成员。那么为什么不能访问静态成员呢,静态方法里又为什么不能使用 this 关键字呢?想要解决这两个问题,需要从字节码入手。

javac -g:vars ThisTest.java

javap -verbose ThisTest$Person.class

Person 类中的三个方法反编译结果如下:

1.实例化构造方法(也是实例方法) 
ThisTest$Person();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LThisTest$Person;

2.静态方法  
public static java.lang.Object staticTest();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_0
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn

3.实例方法
public java.lang.Object test();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         4: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
             0      13     0  this   LThisTest$Person;
             2      11     1     i   I

实例方法中的 this

我们可以看到,没有方法参数的1.实例化无参构造方法 2.实例方法 test 两个实例方法中,都有一个 LocalVariableTable 局部变量表。并且局部变量0都是 this 关键字,静态(外观)类型是ThisTest$Person。

输出 this 关键字,与 main 方法中的 person 对象比较内存地址,输出是相同的。说明,当 JVM 调用实例方法时,会将该方法的所有者,传入方法,并存储在方法的局部变量表 LocalVariableTable 的第 0 位,作为 this 关键字。

静态方法没有 this

无方法参数的静态方法 staticTest() 方法没有 LocalVariableTable 属性,说明 JVM 调用静态方法时,不传入方法接收者作为 this 关键字。

因为静态方法通过类名调用,跟类的实例化没关系。类没有实例化也就没有 this 关键字。

super 关键字到底是什么?

搜了很多博客,有很多种说法。有的在说 super 指代的是方法接收者 this 的父类对象。也有的说 super 是将 this 的外观类型强转为父类的静态类型 —>Father super = (Father) this;

也看到了一个反例:“如果父类是抽象类,super 指代父类对象这句话就不对了”。

总之就是看的很兴奋但也不确定啥是对的。直到看到知乎大佬们的回答才清晰起来:

https://www.zhihu.com/question/67126341

认为 super 关键字是一个标记,让编译器知道 super.xxx();是 invokespecial 字节码指令

https://www.zhihu.com/question/51920553

创建子类对象时不会创建父类对象,而是调用父类的实例化构造方法来初始化父类的字段。

子类的内存空间中保存了从父类继承而来的字段及方法(父类区域),通过 super 可以访问这个"父类区域"。this 关键字访问子类自己的数据(子类区域)。

基于大佬们的解答和 HotSpot Debugger 工具,自己来看看 super 的真面目。

public class ThisTest {

    static class Person{
        private int anInt;
        private static int anStaticInt;
        protected double aDouble = 1.0;

        public Person(){
        }

        public static Object staticTest(){
            return 0;
        }

        public Object test(){
            int i = 1;
            this.anInt = 123;
            return 1;
        }

        public void fatherOnlyTest(){
        }
    }

    static class Man extends Person{
        public double aDouble = 99.0;

        @Override
        public Object test(){
            return 666;
        }

        public String sonTest(){
            return "999";
        }

        public void sonOnlyTest(){
        }  

    }
    
    public static void main(String[] args) throws IOException {
        Person person = new Person();
        Person man = new Man();
    }
}

对于 JVM 来说,每个类都是顶层类 TopLevel,在 JVM 中以 Klass 形式存在。当 class 文件被加载时,读取类型信息转换为方法区中的 instanceKlass 对象。java 对象在 JVM 中的以 Oop 对象的形式存在于堆中,Oop 对象中存储了 1.markword 2.类型指针(指向方法区的 klass) 3.实例数据

Person 类的内存布局(字段,方法,常量池)

在这里插入图片描述

person 变量指向的 Oop 对象

在这里插入图片描述

Man 类的内存布局

在这里插入图片描述

man 变量指向的 Oop 对象

在 JVM 中对象以 Oop 形式存在,oop 中存放 _mark 对象头, _metadata 指向 instanceKlass 对象的指针 ,实例数据(包括从父类继承而来的),对齐填充。
在这里插入图片描述

继承虚方法

子类继承父类的方法时,会将父类的 vtable 复制到自己的 vtable 中,然后再 vtable 末尾保存自己的方法。

可以看到 instanceKlass 中的 vtable_len 为 9 ,说明 vtable 中有 9 个 Method ,除去继承 Object 超父类中的5个虚方法(hashCode,equals,toString,clone,finalize),和自己本身的2个虚方法(sonTest,sonOnlyTest),共有7个。

剩下的两个 Method 是从父类 Person 继承而来的,其中 test() 方法被 Man 类重写。
在这里插入图片描述
1.可以在 Man 对应的 instanceKlass 中看到 test() 方法的内存地址为 public java lang Object test() @0x00000001cc33650。父类 Person 对应的 InstanceKlass 中 test() 方法的内存地址为 public java lang Object test() @0x000000001c8e36a8:。虽然 Man 继承了 test() 方法,但指向的是自己的实现版本。

2.子类还继承了父类一个方法: private void fatherOnlyTest(){}

看来 instanceKlass 中的 vtable 不光存储虚方法,还存储 private 类型的非虚方法。其他非虚方法 1.静态方法 2.构造方法 3.final 方法不在 vtable 中。

但是如果给 fatherOnlyTest 加上 final 修饰符,则表示子类不能继承(而非不能重写)该方法。该方法也就不会存在于父类 Person 的 vtable 中,子类 Man 也就继承不到了。

继承字段

在这里插入图片描述
从父类继承而来的字段,按照父类中的存储顺序存储于子类内存中。存储完父类的再存储子类的字段。

子类对象中的实例数据包括了从父类中继承的字段:anInt,aDouble (遗传的基因)

第二个 aDouble 是 Man 类自己的属性。

子类内存空间中同时存在两个 aDouble 字段,可见字段是不参与多态的。

了解了 JVM 中对象长什么样子,现在来验证本节开头的两种说法:1. super 指代父类对象 2.super 是 (Person)this,用于访问子类内存空间中的"父类区域"

创建子类对象时是否创建父类对象

public static void main(String[] args) throws IOException {
        Man man = new Man();
}

在这里插入图片描述

通过 ClassBrowser 工具查询当前 JVM 中所有被加载的类,发现 Person 类被加载。
在这里插入图片描述
通过 Object Histogram 工具查询当前时刻 JVM 中所有的对象,根据 Person 的全限定名查询不到存在的对象。所以没有创建父类对象。

所以在创建子类对象时,只是调用了父类的实例化构造方法进行实例化,没有创建父类对象。

所以 super 指代父类对象的说法也就不攻自破了!
即第二种说法正确,super 指代 invokespecial 字节码指令,访问子类内存空间中的 “父类区域”。

解析 super 用法

  1. super.字段
  2. super()
  3. super.虚方法()
  4. super.getClass()
  5. super.hashCode()

super.字段

通过上面的了解,我们知道子类的内存空间中存在从父类继承的字段,所以字段是不参与多态的。super.字段 就是直接访问子类中"父类区域"的同名字段。

super()

如果父类有无参构造方法,那么子类构造方法的第一行默认是 super();

通过上面的了解我们知道,创建子类对象不会同时创建父类对象。只是子类调用了父类的实例化构造方法。

因为构造方法是实例方法,所以 JVM 会将方法接收者也就是当前子类对象,作为 this 关键字传入父类的实例化构造方法。

super.getClass()

Object 中对于 getClass() 方法的注释为:

@return The {@code Class} object that represents the runtime
*         class of this object.

返回调用该方法的对象的运行时类的 Class 对象。

getClass() 原理

如果通过对象来找到对应的 Class 对象呢?
在这里插入图片描述
相信细心的同学已经发现了,在 Oop 中类型指针指向的 instanceKlass 对象中有一个名为 _java_mirror 的属性_java_mirror : Oop for java/lang/Class。它就是类加载创建 instanceKlass 对象时伴生的 Class 对象。

java 中对象通过调用 getClass() 方法,首先根据 Oop 对象中类型指针 _metadata 找到方法区中的 instanceKlass 对象,再通过 instanceKlass 对象找到堆中对应的 class 对象。(据R大说 class 对象中也有指向 instanceKlass 的隐藏字段)

在 Class 类中声明了许多获取类型信息的方法,可以说 class 对象就是获取方法区中该对象的实际类型信息的入口。

super.getClass()

因为 getClass 是 final 修饰的非虚方法,不存在于 vtable 中。在子类内部 super.getClass() 就是直接调用 Object 中的 getClass() 方法。判断方法接收者的实际类型,所以当我们在子类内部执行以下代码时会发现输出结果都是子类的类型信息。

System.out.println(super.getClass().getName());
System.out.println(this.getClass().getName());

super.虚方法()

super.虚方法() 就是根据子类的 instanceKlass 去找到父类的 instanceKlass,找到父类 vtable 中目标方法的内存地址并调用。虚方法是实例方法,传入当前子类对象作为 this 关键字。

super.hashCode()

当通过 super 关键字调用从 object 类中继承的方法时,需要稍加注意。

分为两种情况 1.父类没有重写该方法 2.父类重写了该方法。以 hashCode() 为例

  1. 父类没有重写该方法

    此时执行 super.hashCode() 时,实际上是通过子类找到父类的 instanceKlass ,找到父类 vtable 中的 hashCode() 方法的内存地址并调用。因为父类没有重写该方法,所以调用的其实就是 Object 实现的 hashCode 方法。

     @return  a hash code value for this object.
      public native int hashCode();
    

    Object 方法中的 hashCode 实现:返回方法调用者(接收者)对象的散列值。因为是由子类对象调用,所以在这种情况下 super.hashCode 与 this.hashCode 输出的值相同。

  2. 如果父类实现了该方法

    在父类 vtable 中找到的 hashCode() 是父类的实现版本,而非 Object 的实现版本。输出结果与方法实现内容有关。

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值