先抛问题出来:
public class Parent {
private String p = "parent";
private String p1 = "parent";
}
public class Son extends Parent{
private String p = "son";
private String p1 = "son";
}
以上是两个父子类,可以看到子类有和父类同类型同名的实例变量,那么我们在实例化Son的时候,Son son = new Son();
,那么这个son对象在内存中的布局是什么样的呢?
一般对于jvm底层不是很熟的同学会有两种误解:
- 觉得子类和父类有同名同类型的变量,会覆盖父类的变量,因此在子类进行实例化的时候就不会为父类的这两个实例变量进行内存分配,
- 有些同学会认为,子类在实例化的时候会先去实例化一个父类的对象,然后再实例化子类对象,因此在这次的实例化过程中应该会为Parent和Son分别创建一个对象,然后分别为自己的实例变量分配实例变量空间
其实并不是这样的,我们通过工具jol-core来查看下我们的son对象的布局:
引入工具:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
// 打印对象son的内存布局
Son son = new Son();
System.out.println(ClassLayout.parseInstance(son).toPrintable());
com.yangsong.common.ext.Son object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 8c cd 01 20 (10001100 11001101 00000001 00100000) (536989068)
12 4 java.lang.String Parent.p (object)
16 4 java.lang.String Parent.p1 (object)
20 4 java.lang.String Son.p (object)
24 4 java.lang.String Son.p1 (object)
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到在OFFSET-12开始就是Son的实例变量,可以很明显看到我们的son对象的内存布局中也包含了Parent的实例变量。
那这是怎么回事呢?
原来子类继承了父类的一些属性之后,并不会因为同名同类型而“覆盖掉”父类属性,该属性定义依然在父类的类信息中,只是在子类进行实例化的时候会先为子类对象分配一个内存空间(并不会为父类分配空间,这一点有些说会先实例化一个父类对象是不对的),然后再进行初始化,初始化分为两步:
一. 执行实例变量定义
a. 先在子类对象的内存空间,执行父类实例变量的定义并赋默认值
b. 然后执行子类的实例变量定义,并赋默认值
在字节码中操作数的出栈压栈都是一句引用(如下),是没有变量名的,因此字节码是分辨不出来同名覆盖这回事儿的
二. 执行初始化代码,为实例变量赋初始值
a. 执行父类的实例初始化代码(代码块+构造函数)为父类的实例变量赋初始值
b. 执行子类的实例初始化代码(代码块+构造函数) 为子类的实例变量赋初始值
以上,实例化子类对象的时候是不存在同时实例化父类对象的,他只是调用了父类的实例变量的初始化代码(代码块+构造器),但是并没有为父类分配内存空间。
new指令开辟空间,用于存放对象的各个属性引用等,反编译字节码你会发现只有一个new指令,所以开辟的是一块空间,一块空间就放一个对象。
然后,子类调用父类的属性,方法啥的,那并不是一个实例化的对象。
在字节码中子类会有个u2类型的父类索引,属于CONSTANT_Class_info类型,通过CONSTANT_Class_info的描述可以找到CONSTANT_Utf8_info,然后可以找到指定的父类啊啥的。
你的方法啊,属性名称都是在这个上面解析出来的,然后实际变量内容存储在new出来的空间那里。。。
super这个关键字只不过是访问了这个空间(父类信息)特定部分的数据(也就是专门存储父类数据的内存部分)。。。。。。
默认的hashcode和equals(直接使用的==比较)都是一样的,所以,这根本就在一个空间里,也不存在单独的出来的父类对象。
我们通过以下一个例子可以更清晰地看出来,在实例化一个子类对象的时候,到底是谁在调用父类的构造方法等实例初始化代码的:
public class Demo {
public static void main(String[] args) {
new GrandSon();
}
}
class Father{
Father(){
System.out.println("我是Father,当前调用我的构造函数的是:"+this.getClass());
}
}
class Sons extends Father{
Sons(){
System.out.println("我是Son,当前调用我的构造函数的是:"+this.getClass());
}
}
class GrandSon extends Sons{
GrandSon(){
System.out.println("我是GrandSon,当前调用我的构造函数的是:"+this.getClass());
}
}
结果:
以上可以看到,在实例化一个GrandSon的对象的时候,我们在new的时候为GrandSon分配了一个内存空间,然后往上找到最高的父类的实例初始化代码,从上而下,依次调用,来初始化当前这个对象(GrandSon对象),可以看到this
始终指向GrandSon的对象,因此可以知道,父类们的实力初始化代码只是被this调用了,而不是实例化了父类的对象。
好了,回归最初的问题: 一个子类在实例化之后,这个对象的内存布局是什么样子的呢?
答:应该在他的对象的实例变量里包含他的父类所定义的实例变量。