类加载过程
类加载器子系统
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
加载
类加载过程的第一步,主要完成下面3件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区访问这些数据的访问入口
Java加载.class文件的几种方式:
- 从本地加载.class文件
- 从网络加载.class文件
- 从本地加载.zip、.jar等归档文件
- 从专用数据库提取.class文件
- 从内存中加载.class文件(.java文件动态编译)
- 从加密文件中获取,防止class文件被反编译的保护措施
连接阶段
验证
确保当前class文件的字节流所包含的内容符合当前JVM的规范要求,并且不会出现危害JVM自身安全的代码,当前字节流不符合规范会抛出VerifyError的异常,或者子异常.
验证的信息有:
- 文件格式:验证二进制文件是什么类型,验证是否符合当前JVM规范
- 元数据验证:检查类是否有父类、接口/验证其父类、接口的合法性、验证被final修饰的类、验证是否是抽象类,是否实现了父类的抽象方法或者接口中的方法、 验证方法的重载。
- 字节码验证:主要验证程序的控制流程比如循环、分支等
- 符号验证:主要验证符号引用转换为直接引用时的合法性
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
- 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),这里不包含final修饰的static,因为final在编译时就分配了,准备阶段会显示初始化。
基本数据类型的零值:
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器
clinit ()方法的过程。
注意:
- clinit()方法在编译过程中自动生成,此方法不需要定义,此方法中包含了所有类变量的赋值以及静态代码语句块的执行代码
- 编译器收集的顺序是由执行语句在源文件中的出现顺序来决定的,静态语句块只能对后面的静态变量进行赋值,而不能对其进行访问
- clinit()不同于类的构造器。(构造器是VM视角下的init()方法)
- 若该类具有父类,JVM保证子类的clinit()执行前,父类的clinit()已经执行完毕
- VM必须保证一个类的clinit()方法在多线程下被同步加锁。
对于clinit() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 clinit() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
在同一时间,只能有一个线程执行到静态代码块中的内容,并且静态代码块仅仅执行一次,JVM保证了clinit()方法在多线程的执行环境下的同步语义。