JAVA类的实例化顺序

类加载及初始化详解

一. 源码到字节码
在这里插入图片描述
使用工具javac编译的过程中,其实会经历非常复杂的过程,这里不用去深入研究。我们只要关心编译之后的class字节码文件.

*** 字节码反汇编工具***
查看字节码文件如果直接使用二进制工具去读,可读性极差。可以借助一些工具帮我们查看类中的信息,而且还能看到反编译后的汇编指令。 通过汇编代码,可以深入的了解java代码的工作机制.

javap
javap是JDK自身携带的反编译工具,可以直接使用,很方便;在cmd或者在IDEA的Terminal中可以查看javap的帮助信息:

java -help

在这里插入图片描述
可以根据自己的需要给javap加入参数使用,例如:

javap -p -v  xxxx.class  // 显示所有类和成员的

这个是最直接,最方便的查看工具了,但是可读性没有一些插件好.

IDEA插件工具
在这里插入图片描述
ASMPlugin:主要可以用来查看汇编指令,格式会比较友好.

二. 类加载机制
在这里插入图片描述
Java虚拟机会动态的加载、链接与初始化类和接口。

1. 加载
加载是根据特定的名称查找类或者接口类型的二进制表示,并由此二进制形式来创建类或接口的所对应Class对象的过程。
步骤:
①通过一个类的全限定名获取定义此类的二进制字节流。
②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
③在Java堆中生成一个代表这个类的java.lang.Class对象, 作为对方法区中这些数据的访问入口
2. 链接
链接过程中要做的三步:
①验证:保证被加载类的正确性
文件格式验证
元数据验证
字节码验证
符号引用验证

②准备
为类的静态变量分配内存, 并将其初始化为默认值
例如:

class A{
  static int a=10;
}
//准备阶段会为a静态变量分配4个字节的空间,并初始值为0。

③解析
把类中的符号引用转换为直接引用
类中的符号引用就是class字节码中的原始信息,比如变量名,方法名,类名等,需要解析成为运行时内存中相关的地址引用,方便使用。
例如:调用一个方法时,在java语言层面就是一个方法名这个符号,但是在底层应该要根据这个符号找到内存中的直接地址来调用。
3. 初始化
对类的静态变量, 静态代码块执行初始化操作

 class A{
  static int a = 10;
}
//到了初始化阶段,a的值就可以赋值为10了

三.类的初始化过程
一个类要实例化,必须先要让自己先初始化,类初始化过程主要是对静态成分的初始化。如下:

public class Student{
  public static String name="myoga"; //静态变量
  static{//静态代码块
    name = "myoga_cc";
  }
}

使用反编译工具将Student类字节码反编译,如下:

// class version 52.0 (52)
// access flags 0x21
public class Student {

  // compiled from: Student.java

  // access flags 0x9
  public static Ljava/lang/String; name

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LStudent; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 2 L0
    LDC "myoga"
    PUTSTATIC Student.name : Ljava/lang/String;
   L1
    LINENUMBER 4 L1
    LDC "myoga_cc"
    PUTSTATIC Student.name : Ljava/lang/String;
   L2
    LINENUMBER 5 L2
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0
}

1. 方法详解
当一个类编译之后,字节码文件中会产生一个类构造器方法:<cinit>(),而这个方法中的逻辑就是当前类中的静态变量和静态代码块的初始化逻辑整合
在这里插入图片描述

汇编指令含义:
LDC:将整数,浮点数或者字符串常量冲常量池中推送到栈顶
PUTSTATIC:将栈顶的值,赋值给指定类的静态字段

2 静态变量和静态代码块初始化顺序

①静态变量直接赋值底层原理:
当字节码在加载过程中的链接阶段时,有三个步骤,分别是验证,准备,解析。在准备阶段,为类的静态变量分配内存, 并将其初始化为默认值。只有到了初始化阶段才会正式赋值。

public static int a = 10;
//在链接的准备阶段:开辟空间存储a的数据,初始为0
//在出初始化阶段:将静态变量a赋值为10

②静态代码块
静态代码块的逻辑也将会在类的初始化阶段执行。

例子一
静态变量的赋值和静态代码块中的逻辑,都会整合到<cinit>方法中执行,那么问题来了,如果多个静态代码块和静态变量的赋值,那么他的初始化顺序又会是怎样的呢?例如:

public class Student2 {
    static int height;//0
    static int age = 10;

    static {
        age = 20;
    }

    static {
        name = "Jack";
    }

    static String name = "Rose";

    public static void main(String[] args) {
        System.out.println(height);//0
        System.out.println(name);//Rose
        System.out.println(age);//20
    }
}

要解答这个问题,你要观察编译后的字节码中<cinit>方法中具体指令就可以了。如下:

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 3 L0
    BIPUSH 10  //将单字节的常量值10推入到栈顶
    PUTSTATIC Student2.age : I  //将栈顶的值10取出赋值给 Student2.age
   L1
    LINENUMBER 6 L1
    BIPUSH 20
    PUTSTATIC Student2.age : I
   L2
    LINENUMBER 10 L2
    LDC "Jack"  //将字符串常量“Jack”推入栈顶
    PUTSTATIC Student2.name : Ljava/lang/String;
   L3
    LINENUMBER 13 L3
    LDC "Rose"
    PUTSTATIC Student2.name : Ljava/lang/String;
    RETURN  //结束
    MAXSTACK = 1
    MAXLOCALS = 0

结论:

  1. 如果静态变量只有定义,没有做赋值初始化,那么只有默认值,在<cinit>方法中不会看到赋值的指令。
  2. 静态代码块和静态变量直接赋值初始化顺序是按照代码顺序从上到下完成的。静态变量的值以最后一次赋值为准。

如果没有静态变量的直接赋值,也没有静态代码块,那么就不会产生<cinit>方法了,这个可以在反编译工具中验证。

3 继承中类初始化分析
当一个类存在父类时,一定是先对父类进行初始化然后再初始化子类的。

class Fu {
  
    static int a = getNum1();

    static {
        System.out.println("1");
    }

    private static int getNum1() {
        System.out.println("2");
        return 10;
    }
}

class Zi extends Fu {
    static int b = getNum2();

    static {
        System.out.println("3");
    }
  
    public static int getNum2() {
        System.out.println("4");
        return 20;
    }

    public static void main(String[] args) {
			//main方法执行
    }
}

在这里插入图片描述
Zi类反编译代码

class Zi extends Fu {

  // compiled from: Fu.java

  // access flags 0x8
  static I b

  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 15 L0
    ALOAD 0
    INVOKESPECIAL Fu.<init> ()V  //父类<init>
    RETURN

对象创建和初始化过程

定义一个Teacher类,里面含有实例变量,实例代码块,构造方法。定义另外一个测试类Test,在main方法中创建Teacher对象,研究其过程。

public class Teacher {
    int age = 10;
 
    {
        age = 20;
    }

    public Teacher() {

    }

    public Teacher(int age) {
        this.age = age;
    }
}

class Test{
    public static void main(String[] args) {
        Teacher t = new Teacher();
    }
}

1 .new对象底层字节码指令分析

// access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 19 L0
    NEW Teacher  //创建Teacher对象,并将引用值压入栈顶
    DUP   //复制操作数栈顶值,并将其压入栈顶,也就是说此时操作数栈上有连续相同的两个对象地址;
    INVOKESPECIAL Teacher.<init> ()V   //调用无参构造器进行初始化
    ASTORE 1    //将栈顶引用值赋值给变量表中第二个变量
   L1
    LINENUMBER 20 L1
    RETURN
   L2	//本地变量表
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE t LTeacher; L1 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,类中静态相关成分就在这个过程中完成初始化,这个过程可以体现在<cinit>方法中.

​ 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。对象内存分配同时,实例变量也会被赋予默认值(零值)。

​ 在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序员的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化实例代码块初始化 以及 构造函数初始化,不管是哪一种方式的初始化,编译之后都会在方法 <init>中统一执行。

2 对象初始化过程详解
①<init>方法详解
在这里插入图片描述
图中可以看出,开辟对象空间所用的指令是 new 完成的。当给实例变量直接赋值,或者使用实例代码块,构造方法去给实例变量初始化值,编译器最后都会统一的放到<init>方法完成。

<init> 方法 和 java中的构造方法是一一对应的,有多少个构造方法就有多少个<init>方法,请看示例:

public class A {
    int c;

    {
        b = 200;
    }

    int b = 20;

    int a = 10;

    public A() { }

    public A(int c) {
        this.c = c;
    }
}

反编译字节码,观察<init>方法:

// class version 52.0 (52)
// access flags 0x21
public class A {

  // compiled from: A.java

  // access flags 0x0
  I c

  // access flags 0x0
  I b

  // access flags 0x0
  I a

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 12 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 5 L1
    ALOAD 0
    SIPUSH 200
    PUTFIELD A.b : I
   L2
    LINENUMBER 8 L2
    ALOAD 0
    BIPUSH 20
    PUTFIELD A.b : I
   L3
    LINENUMBER 10 L3
    ALOAD 0
    BIPUSH 10
    PUTFIELD A.a : I
   L4
    LINENUMBER 12 L4
    RETURN
   L5
    LOCALVARIABLE this LA; L0 L5 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public <init>(I)V
   L0
    LINENUMBER 14 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 5 L1
    ALOAD 0
    SIPUSH 200
    PUTFIELD A.b : I
   L2
    LINENUMBER 8 L2
    ALOAD 0
    BIPUSH 20
    PUTFIELD A.b : I
   L3
    LINENUMBER 10 L3
    ALOAD 0
    BIPUSH 10
    PUTFIELD A.a : I
   L4
    LINENUMBER 15 L4
    ALOAD 0
    ILOAD 1
    PUTFIELD A.c : I
   L5
    LINENUMBER 16 L5
    RETURN
   L6
    LOCALVARIABLE this LA; L0 L6 0
    LOCALVARIABLE c I L0 L6 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

从反编译后的结果可以推导出以下结论:

  1. 每一个构造方法都会对应一个<init>方法
  2. 每个<init>方法内部都会先执行父类的<init>方法

②实例变量初始化顺序分析
观察上面的代码实例变量直接赋值,及在实例代码块中赋值,在构造方法中赋值这三种方式顺序特点。
可以得到以下结论:

  • 执行完父类的<init>方法后按编码顺序,从上到下再执行直接赋值或者实例代码块,最后执行构造方法内部的赋值语句。
  • <init>方法的模型:
    在这里插入图片描述
    ③继承中实例变量初始化顺序
    从上一节中模型图中还可以看出,父类中的实例变量赋值,实例代码块逻辑,构造方法执行是优先于子类完成的,如下:
    在这里插入图片描述

类实例化顺序总结

  1. 类初始化阶段
  • 类初始化阶段底层有<cinit>方法完成,<cinit>方法是有静态变量赋值语句和静态代码块的结合而形成的。
  • 静态变量赋值语句和静态代码块的执行顺序有其本身编码顺序决定,自上往下执行。
  • 如果父类存在静态资源的初始化,那么父类会优先与子类完成执行<cinit>方法。因为子类是依赖父类而存在的。
  • 一个类中如果没有静态变量的赋值也没有静态代码块,不会存在<cinit>方法
  1. 对象的创建和初始化阶段
  • 对象的创建及初始化阶段,对象空间的开辟是由new指令完成的。对应空间中实例变量的初始化则由<init>方法完成的。
  • <init>方法由实例变量赋值语句,实例代码块,构造方法结合而成,每个java中的构造方法都对应了一个<init>方法。
  • <init>方法中先执行父类的<init>方法,然后执行本类的<init>方法剩下的内容。
  1. 类初始化和对象初始化顺序探究
    大部分情况下,我们都会看到一个现象,类初始化会优先于对象的初始化。例如,
public class A {
  //静态变量
    static int a = getA();

    private static int getA() {
        System.out.print(1);
        return 10;
    }
	//静态代码块
    static {
        System.out.print(2);
        a = 20;
    }
  
  //非静态
    int b = getB();
    private int getB() {
        System.out.print(3);
        return 20;
    }
	//实例代码块
    {
        System.out.print(4);
        b = 40;
    }
	//构造方法
    public A() {
        System.out.print(5);
    }
	
    public static void main(String[] args) {
        System.out.print(6);
        new A();
    }

}

先完成类初始化,然后执行main方法,main方法中创建对象初始化对象。
我们很轻松就能得到结果: 126345

然而这不是一个绝对的情况,类初始和实例化可能会混合在一起完成==。如下:
增加一个静态变量,类型是本身,并创建对象直接赋值。

public class Aa {
    //静态变量
    static int a = getAa();

    static Aa obj = new Aa();//******新加入********


    private static int getAa() {
        System.out.print(1);
        return 10;
    }

    //静态代码块
    static {
        System.out.print(2);
        a = 20;
    }

    //非静态
    int b = getB();

    private int getB() {
        System.out.print(3);
        return 20;
    }

    //实例代码块
    {
        System.out.print(4);
        b = 40;
    }

    //构造方法
    public Aa() {
        System.out.print(5);
    }

    public static void main(String[] args) {
        System.out.print(6);
        new Aa();
    }

}

当类加载后完成第二个阶段链接,其实就可以投入使用了。在初始化阶段中,如果涉及到本类的对象实例化也是可以完成的。我们可以从底层代码论证:

static <clinit>()V
   L0
    LINENUMBER 3 L0
    INVOKESTATIC Aa.getAa ()I
    PUTSTATIC Aa.a : I
   L1
    LINENUMBER 5 L1
    NEW Aa
    DUP
    INVOKESPECIAL Aa.<init> ()V   //在执行<cinit>方法的过程中执行了<init>方法
    PUTSTATIC Aa.obj : LAa;
   L2
    LINENUMBER 15 L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ICONST_2
    INVOKEVIRTUAL java/io/PrintStream.print (I)V
   L3
    LINENUMBER 16 L3
    BIPUSH 20
    PUTSTATIC Aa.a : I
   L4
    LINENUMBER 17 L4
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值