类的属性加载和初始化顺序

Java虚拟机加载.class过程

虚拟机把Class文件加载到内存,然后进行校验,解析和初始化,最终形成java类型,这就是虚拟机的类加载机制。加载,验证,准备,初始化这4个阶段的顺序是确定的,类的加载过程,必须按照这种顺序开始。注意,这里写的是按部就班的开始,而不是按部就班的进行或完成,强调这点是因为这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段进行的过程中调用、激活另外一个阶段。解析阶段在某些情况下,可以在初始化阶段之后再开始—为了支持java语言的运行时绑定。

1、加载

加载就是通过指定的类全限定名,获取此类的二进制字节流,然后将此二进制字节流转化为方法区的数据结构,在内存中生成一个代表这个类的Class对象。

2、验证

验证是为了确保Class文件中的字节流符合虚拟机的要求,并且不会危害虚拟机的安全。加载和验证阶段比较容易理解,这里就不再过多的解释。

4、解析

解析阶段比较特殊,解析阶段是虚拟机将常量池中的符号引用转换为直接引用的过程。如果想明白解析的过程,得先了解一点class文件的一些信息。class文件采用一种类似C语言的结构体的伪结构来存储我们编码的java类的各种信息。其中,class文件中常量池(constant_pool)是一个类似表格的仓库,里面存储了我们编写的java类的类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。在java虚拟机将class文件加载到虚拟机内存之后,class类文件中的常量池信息以及其他的数据会被保存到java虚拟机内存的方法区。我们知道class文件的常量池存放的是java类的全名,接口的全名和字段名称描述符,方法的名称和描述符等信息,这些数据加载到jvm内存的方法区之后,被称做是符号引用。而把这些类的全限定名,方法描述符等转化为jvm可以直接获取的jvm内存地址,指针等的过程,就是解析。虚拟机实现可以对第一次的解析结果进行缓存,避免解析动作的重复执行。在解析类的全限定名的时候,假设当前所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或者接口C的直接引用,具体的执行办法就是虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C。这块可能不太好理解,但是我们可以直接理解为调用D类的ClassLoader来加载N,然后就完成了N—>C的解析,就可以了。

3、准备阶段

之所以把在解析阶段前面的准备阶段,拿到解析阶段之后讲,是因为,准备阶段已经涉及到了类数据的初始化赋值。和我们本文讲的初始化有关系,所以,就拿到这里来讲述。在java虚拟机加载class文件并且验证完毕之后,就会正式给类变量分配内存并设置类变量的初始值。这些变量所使用的内存都将在方法区分配。注意这里说的是类变量,也就是static修饰符修饰的变量,不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中 ,在此时已经开始做内存分配,同时也设置了初始值。比如在 Public static int value = 123 这句话中,在执行准备阶段的时候,会给value分配内存并设置初始值0, 而不是我们想象中的123. 那么什么时候 才会将我们写的123 赋值给 value呢?就是我们下面要讲的初始化阶段。

初始化阶段

类初始化阶段是类加载过程的最后阶段。在这个阶段,java虚拟机才真正开始执行类定义中的java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器()方法的过程。Java虚拟机是怎么完成初始化的呢?这要从编译开始讲起。在编译的时候,编译器会自动收集类中的所有静态变量(类变量)和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是根据语句在java代码中的顺序决定的。收集完成之后,会编译成java类的 static{} 方法,java虚拟机则会保证一个类的static{} 方法在多线程或者单线程环境中正确的执行,并且只执行一次即是线程安全的。在执行的过程中,便完成了类变量的初始化。值得说明的是,如果我们的java类中,没有显式声明static{}块,如果类中有静态变量,编译器会默认给我们生成一个static{}方法。

类加载的定义

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口.类的加载和类加载器详解

类加载的三种方式

1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()区别

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

初始化时机

1 初始化,也就是new时候会初始化类
2 访问类或者接口中的静态变量或者对其赋值
3 调用类的静态方法
4 反射(Class.forName(“com.geminno”);)
5 初始化它的子类,父类也会初始化
6 虚拟机启动时被标明是启动类的类(java Test),直接用java.exe运行某个类;
注意:此处的初始化和下面的实例化不是一回事,初始化不会执行构造器,实例化一定会执行构造器

实例化类时的执行顺序

1、 静态代码块优先非静态代码块优先构造函数,
2、 父类静态代码块→子类静态代码块→父类非静态代码块→父类构造方法→子类非静态代码块→子类构造方法,先执行父类的构造方法是因为子类的构造方法中调用了父类的构造方法,如果子类的构造方法中没有显示调用父类的构造方法,则默认调用父类的无参构造方法,如果显示调用了父类的构造方法则只调用被显示调用的父类构造方法,且显示调用父类的构造方法时, 必须在子类构造方法的第一行,如果静态代码块中引用了静态变量,则静态变量必须写在静态代码块的前面,否则无法编译通过
3、 非静态代码块每次创建实例时执行,而静态代码块只执行一次
4、 非静态代码块不会在调用方法与成员时执行.而静态代码块会执行

上案例

Animal类

public class Animal {
    Cat cat = new Cat();
    static {
        System.out.println("Animal的静态代码块打印Cat的静态变量" + Cat.i);
    }
    public Animal(){
        System.out.println("Animal的构造方法" + this);
    }
    {
        System.out.println("Animal的非静态代码块" + this);
    }
    public static void main(String[] args) {
        Animal animal = new Animal();
        System.out.println("main方法总animal:" + animal);
    }
}

Cat类

public class Cat{
    static Animal animal = new Animal();
    static int i = 1;
    static {
        System.out.println("Cat的静态代码块:" + animal);
    }

    public Cat(){
        System.out.println("CAT的构造方法" + this);
    }
}

执行的结果

CAT的构造方法Cat@12843fce
Animal的非静态代码块Animal@3dd3bcd
Animal的构造方法Animal@3dd3bcd
Cat的静态代码块:Animal@3dd3bcd
Animal的静态代码块打印Cat的静态变量1
CAT的构造方法Cat@97e1986
Animal的非静态代码块Animal@26f67b76
Animal的构造方法Animal@26f67b76
main方法总animal:Animal@26f67b76

根据前面知识自我总结

/**
 * 1、Animal的类构造器<clinit>并锁住,静态代码快,此时是main方法的animal,后面简称main.animal
 * 2、Cat的类构造器<clinit>并锁住,静态属性animal(后面简称animal),此时执行的是Cat.i的准备阶段,并未初始化,后面简称cat.i
 * 3、animal的属性cat,后面简称animal.cat
 * 4、因为此时Cat的类构造函数已被锁住,直接执行animal.cat的构造函数
 * 5、初始化animal.cat后返回并执行animal的非静态代码块
 * 6、animal的构造方法,此时animal初始化完成,回到cat.i继续执行
 * 7、cat.i的int静态变量,此时animal不再为null
 * 8、cat.i的静态代码块,cai.i类构造器执行完毕。此时animal也初始化完毕
 * 9、打印main.animal的静态代码块,接着回到了main.animal进行初始化,main.animal的cat变量,但是此时仍为null
 * 10、main.animal不再执行类构造器,直接执行到main.animal属性cat,初始化main.animal.cat,执行main.animal.cat实例的构造函数
 * 11、main.animal的非静态代码块,此时main.animal.cat属性不为null了
 * 12、main.animal的构造方法
 */
/**
 * 得出的结论
 * 1、执行了实例构造函数就完成了初始化,new的实例不再为null
 * 2、执行类构造器<clinit>并上锁是为了只执行一次,且如果此处阻塞住,第二次初始化对象的时候会跳过此处,往下执行,其余情况不会跳过代码,会顺序执行
 * 3、调用类的静态变量时,只会执行类构造器,不会执行实例构造器
 */

重点(承前启后)

上面的案例细心的同学可能已经发现我在Cat类里的Animal属性上加了static,
那如果不加会造成什么后果?会导致StackOverflowError,栈溢出,程序会一直初始化Cat里面的animal属性和Animal里面的cat属性造成一个递归的死循环,
那为什么加了static就不会造成这种异常呢?
因为加上static就属于了上面说到的类构造器,无论new多少实例都会只执行一次,而不加static就属于成员变量,每次new对象都会执行,通过上面代码可知,当第二次new对象的时候,即使第一次new的类构造器在阻塞执行,第二次也不会因此阻塞,而是按照实例化类时的执行顺序继续执行,当第二次执行到实例构造函数时就,则第二次初始化实例执行完毕,把实例引用赋值给第二次的类变量,在此就结束了循环嵌套的循环,
如果不加static关键字,
则实例变量执行一直会在实例构造函数前面,所以就造成了一个实例A还没执行构造函数初始化完成,就要执行引用其他类B的的变量的初始化,而此时这个变量B同样没有执行构造函数初始化完成,就要执行引用类A的变量的初始化,于是造成了死循环,此处只要知道了实例的初始化顺序就好理解了。
搞了几天才搞明白的,有转载的同学记得点个赞

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值