类加载过程
加载 [验证 准备 解析](连接) 初始化 卸载
加载
将 .class 文件读取到内存中,并创建一个 class 对象。
- 根据类的全限定名称来获取二进制字节流
- 将字节流的静态存储结构转化为方法区的运行时数据结构
- 在 Java 堆中生成一个 class 对象作为方法区中的访问入口
JVM 会在一开始就预先加载某个类,如果在预先加载的过程中遇到了 .class 文件损坏或者存在错误,类加载器不会立马报出错误,而是在程序首次主动使用该类才报出错误,如果这个类一直都没有被程序主动使用,那么类加载器就不会报出错误。(如果 class 文件有错,只有当程序首次主动使用该类,他才会报错。)
连接
验证
验证被加载的类是否有正确的内部结构。
- 文件格式验证
- 是否以魔数开头
- 代码中的魔数:代码中没有解释的字符串或数字常量,又叫魔法值
- 判断文件类型的魔数:是否以一段固定开头(有意填充或者本该如此)的内容,可以通过这几个字节的内容来确定文件类型
- 主次版本号是否在 JVM 处理范围内
- 常量池中是否含有不被支持的常量类型(检查常量tag标志)
- 指向常量的各种索引值中是否含有指向不存在的常量或者不符合类型的常量
- constant_utf8_info型的常量是否存在不符合utf8编码的数据
- class文件中各个部分以及文件本身是否有被删除的或者附加的其他信息
- 是否以魔数开头
- 元数据验证
- 这个类是否包含父类(除了java.lang.object外,所有类都要有父类)
- 这个类的父类是否继承了不允许被继承的类(final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或者接口中要求实现的所有方法
- 类中的字段,方法是否与父类中产生矛盾,(例如覆盖了父类的final字段,或者出现了不符合规范的方法重载)
- 字节码验证:这一阶段是验证阶段最为复杂的阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的.这阶段主要是校验class文件的code属性.
- 保证任意时刻操作数栈的数据类型和指令代码序列都能配合工作,例如不会出现类似于“在操作数栈放置了一个int类型的数据,使用的时候按long类型来加载入本地变量表中”这样的情况
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
- 保证方法体之内的类型转换都是有效的,比如,可以把一个子类赋值给父类数据类型,这是安全的,但是把父类赋值给子类数据类型,甚至是毫无关系的数据类型,这是危险的,不合法的.
- 符号引用验证
- 符号引用中通过字符串的全限定名能否找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的字段和方法
- 符号引用中的类,字段,方法的可访问性是否能被当前类访问
准备
在类加载阶段给静态变量分配内存,并设置初始值,也就是零值。需要注意的是:
-
这时候进行内存分配的是类变量(就是静态变量,被 static 修饰),而不是实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
-
JDK7 之前,HotSpot 使用永久代来实现方法区时,类变量使用的内存都在方法区中分配;但是从 JDK8 之后,HotSpot 将原来放在永久代的字符串常量池、静态变量等都移动到了堆中,这时类变量会跟着 Class 对象存放到 Java 堆中。
-
初始值默认情况下是数据类型默认的零值(0、null等)。
public static int i = 4;
在准备阶段 i 的值为 0 为不是 4(初始化阶段才为 4);特殊情况:如果是public static final int i = 4;
在编译时 Javac 将会为 i 生成 ConstantValue 属性,在准备阶段 JVM 就会根据 ConstantValue 的设置将 i 设置为 4,我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
解析
JVM 将常量池内的符号引用替换为直接引用,解析动作主要针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
- 符号引用:一组符号来描述目标,可以是任何字面量。
- 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
给类的静态变量赋予正确的初始值
JVM 初始化步骤:
- 类还没有被加载和连接,那么程序先加载并连接该类
- 如果类的父类还没有被初始化,那么先初始化其父类
- 如果类中有初始化语句,那么先依次执行这些初始化语句
只有主动使用类时才会初始化:
- new(创建类的实例)
- 访问某个类或接口的静态变量,或者对静态变量赋值
- 调用类的静态方法
- 反射
- 初始化某个类的子类,则父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
使用
类访问方法区内的数据结构的接口,对象是** Heap 区(堆)的数据**
卸载
卸载类即该类的 Class 对象被 GC。
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
类加载方式
- 使用命令行启动由 JVM 初始化加载
- 使用 Class.forName() 方法动态加载
- 使用 ClassLoader.loadClass() 方法动态加载
类加载机制
1、全盘负责
当一个类加载器负责加载某一个 class 时,他会将 class 所依赖的和引用的其他 class 一同加载,除非显示其他类加载器来加载。
2、父类委托
先让父类加载器加载,如果父类加载器无法加载,则尝试在自己的类路径中加载该类。
3、缓存机制
保证所有被加载的类都有缓存。当程序需要用到该类时,类加载器会先去缓存区中查找有没有该类,只有缓存区没有,系统才会读取该类的二进制数据,并将其转换为 class 对象,存入缓存区。
4、双亲委派机制
自下向上,将请求委托给父类加载器加载。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
优点
- 系统类防止内存中有多个相同的字节码(安全)
- 保证程序安全稳定的运行
过程:
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
JDK 三个默认的类加载器:AppClassLoader、Ext ClassLoader、BootStrap ClassLoader。这三不是继承关系,而是组合
打破双亲委派机制:自定义 ClassLoader,重写 loadClass 方法,只要不按照上面那三个就行。
eg:TomCat,WebAppClassLoader 类加载器
参考
https://www.pdai.tech/md/java/jvm/java-jvm-classload.html#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E7%9A%84%E5%B1%82%E6%AC%A1
https://javaguide.cn/java/jvm/class-loading-process.html#%E5%8D%B8%E8%BD%BD
最后
该文章是自己写的小总结,可能存在错误,欢迎大佬指出!感激不尽!