深入浅出AndroidAndroid技术知识

Java类加载机制

2017-10-24  本文已影响246人  Vinctor

基于JVM的语言,如java,kotlin,groovy等语言,在各自编译器编译完成之后,都会编译为.class文件,用JVM加载。而class文件只有被确的加载到JVM正中才能运行和使用。虚拟机是如何在家这些文件呢?本文将详细讲解。

类的生命周期

一个类从被加载到虚拟机到最后被卸载,生命周期包括:加载验证准备解析初始化使用,卸载7个阶段。其中验证,准备,解析3个部分称为连接阶段。

类的生命周期

这7个阶段在实际JVM中并不是按照图中所示的顺序来开始运行的,里面存在时间上的交叉进行。但是其中加载,验证,准备,初始化,卸载5个结算的顺序是确定的。

加载

这是类生命周期的第一个阶段,那么加载的是什么呢?加载的应该是一个字节码文件的二进制字节流。那么此二进制流如何得来呢?java虚拟机规范并没有强制要求,我们可以灵活运用这一特性实现很多的加载源:

在此阶段,开发人员可以使用系统的类加载器进行加载,也可以使用自己定义的类加载器来自定义获取字节码流的方式(重写来加载器的loadClass方法)。

加载字节码文件结束后,虚拟机将字节流存储在方法区中,同时在内存中(Hot Spot中实在方法区中)实例化一个Class对象,外部可以同过此实例访问该类对象。

在此阶段运行中,验证阶段就已开始,交叉进行。只有通过通过了验证阶段,只有通过了验证阶段,字节流才会进入内存的方法区中进行存储。

验证

验证阶段的主要任务是:确保字节码流中包含的信息符合当前版本虚拟机的要求,并不会有危害虚拟机自身安全的行为。

如:将一个对象强转为一个未声明实现的类型,执行一个虚方法,执行一个并不存在的方法。在我们平时编码的经验中,虽然以上这些错误会在编译时报出,无法通过编译;但是,我们上面提到过,class文件是由多种方式得来,对于直接生成.class文件、无需编译的方式,验证这一阶段对于虚拟机的保护就显得尤其重要。

简要的概述,虚拟机对类的验证阶段分为以下4个方面,这四个方面层层深入:

文件格式的验证

针对类文件(字节码流)的验证

验证字节码流是否符合java虚拟机规范中规定的class文件格式,如:

通过了验证阶段,字节流会进入内存的方法区中进行存储。以后的验证和其他操作都针对于内存方法区中的数据进行操作,而不针对字节码流。

元数据验证

针对数据类型的验证

该阶段是进行语义分析验证,以保证其信息符合Java语言规范的要求,比如:

字节码验证

针对方法体的验证

此阶段通过数据流和控制流分析,检查程序的语义是合法的,符合逻辑的。保证程序逻辑的正确运行,检验的内容如:

符号引用的验证

针对常量池匹配的验证

此阶段检查是为了:确保在后续的解析阶段,虚拟机可以顺利的将符号引用转化为直接引用。(关于符号引用与直接引用的概念,祥见下文解析过程)见下图,讲解一下验证内容:

常量池

准备

针对类变量(static)

经过验证阶段,虚拟机从文件,数据类型,方法逻辑,符号引用等各个方面对类进行了验证,已确保代码的正确性。接下来开始为代码的运行做准备,进入准备阶段。

准备阶段是为正式类变量(注意,不是实例变量)分配内存并设置类变量初始值的阶段,注意,此初始值并不是我们java代码中所写的初始值(如 int a=123;),而是java虚拟机规范中规定的初始值,
java体系中各种类型的初始值如下:

各类型的初始值

如果一个变量声明为static int a=123,则在此阶段,声明a的值为0;

注意:如果类变量被final修饰,如

final static int a=123;

这种情况下,javac编译阶段,将为此变量生成ConstantValue属性,在此准备阶段直接将其赋值为123;

解析

针对常量池

解析阶段是将常量池中符号引用转化成直接引用的过程。主要针对常量池中的类或接口,字段,类方法,接口方法,方法类型,方法句柄,调用限定符

直接引用指向的目标必须真实存在于内存之中的。在代码运行过程中,会不断产生新对象,故而解析这一过程并不是一次就完成的,其发生的时机不固定。

java虚拟机规范中规定了只有执行了以下字节码指令前才会将所用到的符号引用转化为直接引用:

在解析过程中,如果需要解析类或接口的的字段,方法,则先查找该字段,方法所属的类或接口是否被解析,如果没有,则先解析类或接口,然后在查找当前的类或接口中是否有该字段或方法,如果没有,则递归向上到父类或父接口中寻找该字段或接口。

初始化

至此,程序终于开始执行我们开发人员写的代码了(等了好久)。此阶段是为类设置类变量的值和一些其他初始化操作的阶段(如执行static{ }静态代码块)。

在类编译过充中,编译器为每一个方法生成了一个<clinit>()类初始化方法,初始化阶段也是此方法的执行阶段。

注意<clinit>()并不是默认构造方法,前者是类的初始化方法,后者是实例的初始化方法。我们此文讨论的是类的生命周期,而不是实例的生命周期。

<clinit>()是如何生成的呢?其中又包含什么呢?

<clinit>()方法是在编译阶段,编译器收集整个类中的类变量的赋值以及静态代码块而形成的。顺序是按照赋值以及静态代码在源文件中出现的顺序生成的。同时,如果一个类有父类,则虚拟机会保证父类的初始化先于子类的初始化执行。

使用

至此 一个类已经具备我们使用的条件了,我们可以对这个类进行实例化和其他操作了。

github上的地址:DevelopBlog

上一篇下一篇

猜你喜欢

热点阅读