JVM - 类加载机制
所谓类加载机制,就是将 class 文件加载进入 JVM 的内存当中,然后通过检验、转化、解析、初始化等过程,使 class 文件变为可直接使用的 Java 类型的过程。
类加载的过程如下所示:
加载:将 class 文件加载进入 JVM 中
链接:解析 Class 文件,这里面又具体分了三步:
校验:验证 Class 文件的正确性,比如文件格式、字节码等正确性。
准备:为类变量(静态变量)分配内存,并设置类变量的初始值(即零值,真正给类变量赋值要到执行类构造器的
<clinit>()
方法方法),类变量的内存被分配在堆中(JDK1.8 时修改的)。解析:将常量池的符号引用替换为直接引用
初始化:对类变量进行初始化,即执行类构造器的
<clinit>()
方法使用
卸载
创建一个对象的执行过程。
父类静态代码块-->子类静态代码块-->父类代码块-->父类构造方法-->子类代码块-->子类构造方法。
代码块就是每个类中只有
{}
包裹的代码,会在每次对象创建的时候执行,执行时机在构造函数之前。
加载
加载阶段,JVM 共完成如下的三件事
通过类的全限定名获取类的二进制字节流
将字节流转换为方法区的运行时结构
内存中生成 Class 对象,作为方法区中这个类的数据访问入口
类加载器
类加载器的设计目的就是为了完成通过类的全限定名获取类的二进制字节流这项工作。
这样设计的好处是可以让应用程序自己决定该如何获取所需要的类。
作用:
加载类的二进制字节流
通过类加载器和类本身来确定一个类在 JVM 中的唯一性。
数组类的加载
数组类本身不通过类加载器进行加载,它由 JVM 直接创建,但数组类的元素类型仍靠类加载器区创建。
验证
验证是链接阶段的第一步,在加载之后开始,但链接与加载过程是交替进行的,可能加载阶段尚未完成,链接阶段就已经开始。
作用:确保 class 文件的字节流符合要求,且不危害虚拟机安全。
准备
作用:为类变量(静态变量)分配内存,并设置类变量的初始值(即零值,真正给类变量赋值要到执行类构造器的 <clinit>()
方法方法),类变量的内存被分配在堆(JDK 1.8 的改动)中。
例外:
如果类变量被标记为 final ,则在准备过程中就会被赋值为实际的值。
换言之,如果子类或者父类调用 final 变量,不会对父类和子类进行初始化。
解析
作用:将常量池的符号引用替换为直接引用。
初始化
JVM 对于何时开启类的初始化有详细的规定,有且仅有 5 种情况会导致立即开始初始化:
当 JVM 启动时,如果用户指定了一个要执行的主类,即 包含 main() 方法的那个类,虚拟机会先初始化这个类。
使用 new 实例化对象
初始化子类时,会先初始化其父类。
读取一个类的静态字段、设置一个类的静态字段、调用一个类的静态方法
使用 reflect 方法对类进行反射调用,且类未经初始化
作用:对类变量进行初始化,即执行类构造器的 <clinit>()
方法
<clinit>()
方法
生成
<clinit>()
方法由编译器自动收集类中所有类变量的赋值语句和静态语句块(static{})中的语句合并产生。执行顺序为源文件中语句出现的顺序。
如果一个类中没有静态语句块,也不需要对静态变量赋值,则可以不生成该方法。
执行
JVM 会保证在子类的 <clinit>()
方法执行前,父类的 <clinit>()
方法已经被执行,因此父类的静态语句块顺序要优先于子类。
<clinit>()
方法在多线程环境下也可以正确被加锁、同步。由于同一个类加载器下,一个类型只会初始化一次,因此如果多个线程同时去初始化一个类,你们只有一个线程会去执行 <clinit>()
方法,其他线程会阻塞等待,直至 <clinit>()
方法执行完毕,所有阻塞线程才会继续执行。
评论