从JVM角度看继承

从JVM角度看继承

最近重读了周志明老师的《深入理解JAVA虚拟机》一书,看完大有收获,但仍对继承情况下对象内存布局有所疑惑,所以查阅资料,结合本书进行分析

参考文档:

【深入理解JVM】:Java类继承关系中的初始化顺序

从JVM内存机制理解 java 的继承

继承的对象内存布局

对象在堆内存中的存储布局可以划分为三个部分:

对象头(Header)

实例 数据(Instance Data)

对齐填充(Padding)

书中原文:

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会 受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存 放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的 +XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空 隙之中,以节省出一点点空间。

可以看到继承下来的非静态成员变量是存在于子类的,而静态成员变量存在于方法区

初始化的时机

原文:

关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行
强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》
则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之
前开始):
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
· 使用new关键字实例化对象的时候。
· 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
· 调用一个类型的静态方法的时候。
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

对于这六种会触发类型进行初始化的场景,《Java虚拟机规范》中使用了一个非常强烈的限定语——“有且只有”,这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。

执行顺序

  1. 父类静态代码区和父类静态成员
  2. 子类静态代码区和子类静态成员
  3. 父类非静态代码区和普通成员
  4. 父类构造函数
  5. 子类非静态代码区和普通成员
  6. 子类构造函数

原文:

()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法) 不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的()方法执行前,父类的0方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。
由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作

ok 1,2步的顺序没问题

原文:

例如前文多次登场的实例构造器()方法和类构造器()方法就是在这个阶段被添加到语
法树之中的。请注意这里的实例构造器并不等同于默认构造函数,如果用户代码中没有提供任何构造
函数,那编译器将会添加一个没有参数的、可访问性(public、protected、private或)与当前
类型一致的默认构造函数,这个工作在填充符号表阶段中就已经完成。()和()这两个构造
器的产生实际上是一种代码收敛的过程,

编译器会把

语句块(对于实例构造器而言是“{}”块,对于类构造器而言是“static{}”块)、

变量初始化(实例变量和类变量)、

调用父类的实例构造器(仅仅是实例构造器,()方法中无须调用父类的()方法,Java虚拟机会自动保证父类构造器的正确执行,但在()方法中经常会生成调用java.lang.Object的()方法的代码)

等操作收敛到()和()方法之中,并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序进行,上面所述的动作由Gen::normalizeDefs()方法来实现。

除了生成构造器以外,还有其他的一些代码替换工作用于优化程序某些逻辑的实现方式,如把字符串的加操作替换为StringBuffer或StringBuilder(取决于目标代码的版本是否大于或等于JDK 5)的append()操作,等等。

由上文可以看到,和的包括范围,有些人认为阶段仅仅是执行构造方法,这明显是错误的,原文中已经表明()和()方法是一些操作的集合

方法:()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的

方法:除了()方法包括的操作,剩下的包括实例变量初始化,非静态代码块,构造方法(先执行父类,再执行子类)

4,5步的顺序也ok

先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序

1,2,3,5步骤中代码块和成员变量的初始化也有先后顺序,一般是先初始化变量,后静态代码块

至于和的顺序,则是因为是执行在类加载的的初始化流程中,可以说初始化的过程就是,而的执行必须在类加载之后,所以这两者的顺序也是确定的

默认构造器

image-20230908104039730

自定义构造器

image-20230907181016769

可以看到,无论是默认构造器,还是用户定义构造器,均先调用父类的构造函数Object

L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V

调用构造函数是否一定会产生实例?

我认为不是

比如子类调用抽象父类的构造函数,父接口的构造函数,会产生子类实例,但会产生父级的实例吗?

这显然是不会的

抽象类的构造方法为自定义构造器时,子类需要显式调用

image-20230908160437762

image-20230908160327720

关于java构造函数 的错误 there is no default constructor available in

B类继承A类,在B类的构造器中,会隐式存在 super(),用来调用父类无参构造器
而父类A中没有无参构造器,因为A中已经定义了有参构造器(在A中,如果没有定义有参构造器,就会有默认的无参构造器;但如果定义了有参构造器,就没有默认的无参构造器)。所以会显示上述错误。

解决方法:

**法一:**在A类(即父类)中,添加一个无参构造器
**法二:**在B类(子类)的有参构造器中,添加一个super(m);即可
B(int m){ super(m); i = 2; }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值