类加载机制

类的加载阶段

类的整个生存周期包括:加载、验证、准备、解析、初始化。

加载

加载时类的加载阶段的开始,包括三步

1)ClassLoader以全限定名形式从class文件中读取二进制数据流

2)将字节码转化为方法区的运行时数据结构

3)创建class文件对象,存放在堆中,作为程序访问方法区中字节码的入口

校验

验证输入的字节流是否符合java虚拟机规范以及java语义,要保证到达解析阶段时的正确性,包括:文件格式校验、元数据校验、字节码校验、符号引用校验

文件格式校验

例如有魔术校验、class文件版本是否被当前java虚拟机支持、常量池中的常量是否是支持的类型。文件格式校验主要是保证输入的字节流能被正确的解析并存储到方法区中,经过了这个阶段的验证,字节流才会被存储到方法区中。因此,后续的三个校验都是对方法区中数据进行校验的。

元数据校验

主要是进行语义校验,例如

1)是否有父类,父类是否是final类型

2)如果当前类不是抽象类,那么是否实现了父类或接口需要实现的所有方法

3)类中的字段、方法是否与父类发生冲突(final类型不允许重写;重写的方法是否符合规范等)

准备

准备阶段是为类的静态成员变量赋予初始值的一个过程,对应内存将在方法区中进行分配。

例如 public static int value = 1;

那么准备阶段value会被赋值为0,赋值为1的操作是在初始化阶段完成的。还有一种情况:

public static final int value = 1;此时在准备阶段value就会被赋值为1.

解析

解析就是将常量池中符号引用替换为直接引用的过程。解析主要针对:类、接口、字段、类方法、接口方法。



此时类方法解析的只是一部分在编译期确定,运行时已知的方法,包括构造方法、final修饰的方法、私有方法、静态方法、类方法。但是像重写的方法是无法做到这一点的,重写的方法需要在运行时做动态解析,举个例子:

输出结果很简单。分析下main()方法的执行过程:

java虚拟机中将Human man和Human woman称为静态变量,右侧的对象称为实际用例。如果是方法重载,例如a(Human human),a(Man man),a(Woman man),此时如果执行a方法传入man或者human,则都将调用a(Human human)方法,而不会调用另外两个,这是因为重载方法确认时是根据左侧的静态变量确定调用哪个方法的。但是对于本例的重写方法:

1:在操作数栈中压入Human man = new Man()对应的指令(其实是三个,申请堆内存、实例化、赋值)

2:同1,创建Human man = new Woman()

3:从操作数栈确定静态变量man对应的实际用例,然后执行实际用例的sayHello()方法,再将执行sayHello()对应指令压入栈

4:同3步骤。

这就是多态在运行时的动态链接过程,此过程无法在类的解析阶段进行,只能在运行时确定。

以上是java虚拟机一书中的解释。其实解析阶段就是对类或接口中的信息的解析,解析后从类或接口中获取字段或方法的直接引用,然后进行权限校验

初始化

初始化发生在加载、验证、准备、解析之后。在准备阶段会完成静态成员变量的赋初始值操作,而初始化阶段则是完成其赋值和静态代码块的执行,执行发生在构造器函数(并非构造方法)中。

类构造器

1)类构造器由编译器生成,其中包含静态成员变量的赋值和静态代码块的执行。顺序和其在源文件中出现的顺序一致,静态代码块可以访问和赋值出现在其之前的静态成员变量,对之后的静态成员变量只能赋值不能访问

2)虚拟机会保证父类的类构造器一定发生在子类构造器开始执行之前执行完毕

3)类构造器对于类或接口并不是必须的,如果类中没有静态成员变量或静态语句块儿,接口中没有成员变量,则在类和接口中并不会生成类构造器

4)接口的类构造器执行之前并不要求父接口构造器先执行。只有当父接口中的成员变量被访问时,父接口才会被初始化,即使接口的实现类在初始化时也不会执行父接口的类构造器的执行

5)虚拟机会保证类的类构造器方法在执行时,多线程情况下的同步。也就是说只有一个线程可以完成类构造器的执行,其他线程都要阻塞,直到构造器方法执行完成。因此在构造器方法中不能执行耗时操作,即静态代码块的耗时操作

触发初始化只有四个条件:

1)new 创建对象;访问类的静态成员变量、访问类的静态方法

2)以全限定名形式通过反射加载类

3)加载类时,如果其父类未初始化,那么先进行父类的初始化(继承的接口不符合此逻辑。接口是只有在访问到接口中成员变量时才会去加载,并不会随着实现类而加载)

4)application启动时,主类要进行初始化,就是包含main()方法的类。对于Android来说就是ActivityThread

说两种容易混淆的初始化场景:

静态成员变量的调用

上面的执行代码只会输出SuperClass init!,这是因为java虚拟机规定,对于静态成员变量,只会初始化直接定义它的类,因此只会输出SuperClass init!

常量的访问


执行结果并没有输出ConstClass init!,是因为在编译阶段ConstClass.HELLOWORLD的值在编译之后就被存入到NotIntialization的常量池中,因此编译之后,ConstClass和NotIntialization就没有任何关系了,因此不会输出ConstClass init!

你可能感兴趣的:(类加载机制)