据我了解,当通过new创建一个类对象时,在Heap中分配了一个区域来存储类本身的所有字段和它所有祖先的非静态公共(和保护)字段。然后依次调用所有祖先的构造函数,从最旧的构造函数开始,初始化并可能覆盖该对象字段的值。并且在最后,子对象的构造函数自己初始化它的新字段,或者可能覆盖祖先构造函数已经初始化的字段的值。问题如下。如果祖先有它们的私有字段和公共getter,而继承人没有重写这些字段和getter,那么从继承人的对象中,通过调用继承的getter,可以从祖先的私有字段中获取该字段的值. 它是如何发生的?创建一个对象时,它所有祖先的对象是否都在单独的内存区域中创建,并具有它们的所有字段?或者,在我看来更有可能的是,JVM 理解如果祖先有公共 getter,继承人可以访问其私有字段,因此在子对象的内存区域中创建其祖先的私有字段?
除其他信息外,类对象模块还包含有关继承层次结构、字段顺序及其大小的信息。也就是说,类加载后,元空间中的虚拟机总是有一个这个类的对象的“映射”,通过它可以计算出这个或那个字段距内存块开头的偏移量. 当创建一个新对象时,会分配一个足够大小的块来存储对象的标题、它的字段以及它所有超类的字段。此外,字段从继承根开始按顺序排列——首先是超类的字段,然后是子类的字段。由于这种安排,超类的“地图”适用于导航子类的对象。为了清楚起见,我们定义了一个原始类层次结构
该操作
B obj = new B()将在堆上分配这样一个块如果我们现在将对象类型转换为基本类型,那么虚拟机将对对象的字段执行访问操作,就好像没有尾部包含该字段一样
z。感谢Alexey Shipilev,我们可以使用 jol(Java 对象布局)工具看到这一点。
编译
发射
我们得到
对于每个类的方法,JVM 也有一个“映射”,指示特定方法的开始位置在内存中的偏移量。有两种方法可以使用这个“地图”——早期绑定和后期绑定(实际上更多,但在当前上下文中无关紧要)。普通方法调用
将被编译为字节码
该指令
invokevirtual使用后期绑定。也就是说,当一个方法被调用时,JVM 分析调用上下文(调用点),确定需要哪个方法并在所需的偏移处转移控制。因此,多态性是可能的。调用超类方法(以及调用构造函数和私有方法)
将被编译为字节码
该指令
invokespecial使用较早的绑定。也就是说,即使在加载类的阶段,也很清楚需要调用哪个特定类的哪个方法,JVM 会将这个方法的偏移量“缝合”到字节码中。收到控制权后,该方法
getX(当然是在 JVM 的帮助下)将计算从传递的引用指向的内存块的开头到x类中字段应位于的位置所需的偏移量A,读取其值,将其放在堆栈顶部并将控制权返回给调用代码。甚至没有意识到这个引用this实际上指向了一个更大的对象,也没有意识到子类中是否有与它同名的方法。