jvm的类加载机制-类的生命周期

64 阅读8分钟

1.类的生命周期

类从被加载到内存中开始,到卸载出内存为止。它的生命周期总共七个阶段

image.png

阶段的顺序是一定的,除了解析阶段为了支持java动态绑定在某些情况下可以在初始化阶段之后开始。另外这几个阶段是按顺序开始,而不是按顺序进行或完成,

因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。其中,验证,准备,解析合称为链接。

加载:查找并加载类的二进制数据。

此阶段,虚拟机要做的事:

1)通过一个类的名字(绝对路径的名字)来获取定义此类的二进制字节流。 

2)将这个字节流所代表的静态存储结构转化为元空间的运行时数据结构。 

3)在内存中生成一个代表这个类的java.lang.Class对象,作为元空间这个类的各种数据的访问入口。

验证:这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。如果验证失败,会抛出java.lang.VerifyError异常。

1)文件格式验证:验证是否符合类文件格式规范

2)元数据验证:主要是对字节码描述的信息进行语义分析,包括是否有父类、是否是抽象类、是否是接口、是否继承了不允许被继承的类(final类)、是否实现了父类或者接口的方法等等。

3)字节码验证:验证变量是否在使用之前进行初始化、方法调用与对象引用类型是否要匹配、是否违背访问私有数据和方法的规则、对本地变量的访问是否在运行的堆栈内以及运行时堆栈是否溢出

4)符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(连接第三阶段-解析阶段进行符号引用转换为直接引用),符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,则会抛出java.lang.IncompatibleClassChangeError异常的子类异常,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

元数据验证栗子1

package com.example.verify; public final class PerClass { }

--------------------------------------

package com.example.verify;
public class VerifyTest extends PerClass{ } 

此时,编译器会提示:cannot inherit from final com.example.verify.PerClass

字节码验证栗子2

 package com.example.verify;
 public class PerClass { private int value = 123; } 
 
 -------------------------------------- 
 package com.example.verify;
 public class VerifyTest extends PerClass{
 public static void main(String[] args)
 {
 VerifyTest test = new VerifyTest();
 System.out.println(test.value);
 } 
 } 
 此时,编译器会提示:'value' has private access in com.example.verify.PerClass

符号引用验证栗子3

image.png

image.png 符号引用(Symbolic References):

  符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。

Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的

符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用: 直接引用可以是

(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向元空间的指针)

(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

(3)一个能间接定位到目标的对象的引用。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

准备:正式为类变量分配内存并设置类变量初始值。这些变量所使用的内存都将在元空间中进行分配。进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。初始值“通常情况下“是数据类型的零值。

对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,

总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。· 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。

如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

解析:虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法等。

image.png

image.png

初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

类的初始化步骤:

        1)如果这个类还没有被加载和链接,那先进行加载和链接

        2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)

        3)假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下几种:

          1)使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)、调用类的静态方法

image.png

           2) 反射,使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

package com.example.classloader; public class InitTest { public static void main(String[] args) { try { // 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化 Class.forName("com.example.classloader.Test"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } 运行结果:Test!

image.png            3)初始化某个类的子类,如果其父类没有进行过初始化,则需要先触发其父类的初始化。

package com.example.Sample; public class SuperClass { static{ System.out.println("superClass init!"); } } ---------------------------------- package com.example.Sample; public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } ----------------------------------- package com.example.Sample; public class Test { public static void main(String[] args) { new SubClass(); } } 运行结果: superClass init! SubClass init!

image.png            4)Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。

package com.example.classloader; public class InitTest { static { System.out.println("当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类"); } public static void main(String[] args) { } } 运行结果:当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类

image.png            5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。getstatic、putstatic:访问类字段的指令(static字段)、invokestatic:调用类方法的指令(static方法) image.png

image.png

image.png

image.png

2.类加载:Java虚拟机类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。

方式:

   1)命令行启动应用时候由JVM初始化加载

   2)通过Class.forName()方法动态加载

   3)通过ClassLoader.loadClass()方法动态加载

静态加载类:编译时刻加载。

动态加载类:运行时刻加载。动态加载可以将源码功能分隔开,不让编译期错误导致程序整体运行失败。

new方式创建对象是静态加载类,在编译时刻就需要加载所有的可能使用到的类。

image.png

image.png