转载于:https://www.itzhai.com/articles/how-class-file-load-into-jvm.html
类的生命周期:其中如果动态绑定或者晚期绑定,解析阶段不会再准备阶段之后立即执行
这个时候通过类的全限定名称获取类的二进制字节流。
此时这个字节流为静态存储结构,需要转换为方法区的运行时数据结构。结构如上图方法区中所示。每个类生成一个对应的结构,结构里面的信息详细介绍参考此文:The Java Virtual Machine
其中:
ClassLoader的引用
指的是加载这个Class文件的ClassLoader实例的引用;
Class实例引用
指的是类加载器在加载类信息并放到方法区之后,然后创建对应的Class类型的实例,并把该实例的引用保存到Class实例引用中。
主要有如下几种获取方式:
类加载器:
启动类加载器:由C++实现,没有对应的Java对象,在Java中只能用null表示
java.lang.ClassLoader:除了启动类加载器之外,其他加载器类都是其子类,有对应的Java对象,因此这些类加载器类需要通过另外的类加载器加载到jvm中,例如启动类加载器
扩展类加载器(extension class loader):其父类加载器是启动类加载器,负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)
应用类加载器(application class loader):父类加载器是扩展类加载器,负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的
自定义的类加载器:自己定义的类加载器,可以对class文件进行加解密
双亲委派模型:
每当一个类加载器接受到类加载请求时,会将它先请求转发到父类加载器,当父类加载器查找不到所请求的类时,此时才轮到该类加载器尝试去加载
命名空间:在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类
Java 9之前:
启动类加载器复制加载最基础、最重要的类,例如放在JRE的lib目录下jar包的类(以及由虚拟机参数 -Xbootclasspath 指定的类)
Java 9之后:
扩展类加载器改名为平台类加载器,Java SE除了少数几个关键模块是由启动类加载器加载,其余都是由平台加载器加载
**概念:**将创建的类合并至Java虚拟机中,使其能够执行的过程,分为验证、准备、解析三个阶段
class文件是可以被人为篡改的,虚拟机如果直接拿来执行,可能会把系统给搞崩溃了,所以一定要先对Class文件做严格的验证
确保被加载的类能够满足虚拟机的约束条件,验证内容如下:
文件格式的验证:bytecode 的完整性(integrity)
元数据验证:主要是语义校验,如:没有父类,继承了final类,接口的非抽象类实现没有完整实现方法等。
符号引用验证:解析阶段发生的验证,当把符号引用转化为直接引用的时候进行验证。这主要是对类自身以外的信息进行匹配性校验,包括:
为被加载的类的静态字段分配内存,赋默认初值(零值),还会在此阶段构造其他跟类层次相关的数据结构,例如用来实现多态方法调用的动态绑定的方法列表
method table
的数据结构,它包含了指向所有类方法(也包括也从父类继承的方法)的指针,这样再调用父类方法时就不用再去搜索了
注意:static final类型的常量value会在准备阶段被初始化为常量指定的值。
静态变量存储在内存的PremGen(方法区域)空间中,其值存储在Heap中
解析阶段主要将常量池内的符号引用替换为直接引用
class文件被加载入jvm之前,这个类无法知道其他类及其方法对应的地址,此时编译器会生成一个符号引用,而链接阶段需要做的就是将符号引用指向真正的地址,如果符号引用还未被加载,此时则会触发这个类的加载,解析阶段未必会在链接时触发,但是在执行字节码之前,如果包含了符号引用,那就会触发解析
符号引用:字面量,引用目标不一定已经加载到内存中
直接引用:直接指向目标的指针,或者相对偏移量,或是一个能简介定位到目标的句柄。直接引用和虚拟机实现的内存布局相关
解析主要针对以下七类符号引用进行:
符号引用解析的过程或校验的过程中,可能又会触发另一个类的加载。
类中的静态字段会在链接的准备阶段赋上初始值,然后在初始化阶段才会真正的赋值
也就是说除了代码中定义的常量(static final修饰的基本类型)的直接赋值操作,以及所有静态代码块中的代码,会被Java编译器防置于同一方法中,命名为 < clinit >
类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit >
方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit >
方法仅被执行一次。
<CLINIT>
方法概念:编译器收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的方法,主要给类变量做初始化工作的方法
<CLINIT>
方法的注意事项:顺序问题:静态语句块后面的静态变量,静态语句块中可以赋值,但不可以访问
继承顺序问题:无需显示调用,虚拟机会保证子类的<clinit>
方法执行前,父类的<clinit>
方法已经执行完毕
接口的<clinit>
方法:然接口不能有静态语句块,但是可以给静态变量初始化值,所以也可以生成<clinit>
方法;
接口继承:除非使用到父接口的变量,否则执行子接口的<clinit>
方法不需要先执行父接口的<clinit>
方法
除此之外,还有
< init >
方法:init是对象构造器方法句、调用的构造方法合并产生init和clinit方法执行目的不同
init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。
<init>
方法是在一个类进行对象实例化时调用的。实例化一个类有四种途径:1 调用new操作符
2 调用Class或java.lang.reflect.Constructor对象的newInstance()方法
3 调用任何现有对象的clone()方法
4 通过java.io.ObjectInputStream类的getObject()方法反序列化
那么,类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况
除了上述的情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”
通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化。代码如下:
通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”条款,属于“被动引用”。
静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类,这是一个特例,需要特别记忆,不会触发类的初始化,也就会说static final修饰的String类型和基本类型的常量不会被初始化
从上面我们可以明白clinit是在类初始化时候调用的,init是在类实例化的时候调用的,初始化通过lvm的锁机制保证只会执行一次,但是实例化可以执行多次,因此clinit只会执行一次,而init会执行多次,这也是最大的区别。并且这两个方法都是在编译为字节码的时候就确立好了。
总结下:
1,
<init>
方法等同于类的构造函数,在遇到new指令时,自然会调用该方法
2,java类中存在static成员变量,或者有static{}的代码块时,jvm加载java类时,会触发方法,完成static成员变量的初始化,或者执行static{}的代码逻辑
最后我们来总结下类的初始化顺序,由JVM的类加载顺序可以得到:<clinit>()
方法先于<init>()
方法,因此:
单纯类的情况下的执行顺序(无继承):
1.静态变量或静态代码块(看源码顺序)
2.实例变量或实例代码块(看源码顺序)
3.构造方法
有父类情况下的执行顺序:
1.父类静态变量或静态代码块(看源码顺序)
2.子类静态变量或静态代码块(看源码顺序)
3.父类实例变量或实例代码块(看源码顺序)
4.父类构造函数
5.子类实例变量或实例代码块(看源码顺序)
6.子类构造函数
Update your browser to view this website correctly. Update my browser now