深入浅出JVM(三)之HotSpot虚拟机类加载机制

简介: 深入浅出JVM(三)之HotSpot虚拟机类加载机制

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

HotSpot虚拟机类加载机制

类的生命周期

什么叫做类加载?

类加载的定义: JVM把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终变成可以被JVM直接使用的Java类型(因为可以动态产生,这里的Class文件并不是具体存在磁盘中的文件,而是二进制数据流)

一个类型被加载到内存使用 到 结束卸载出内存,它的生命周期分为7个阶段: 加载->验证->准备->解析->初始化->使用->卸载

其中重要阶段一般的开始顺序: 加载->验证->准备->解析->初始化

验证,准备,解析合起来又称为连接所以也可以是加载->连接->初始化

注意这里的顺序是一般的开始顺序,并不一定是执行完某个阶段结束后才开始执行下一个阶段,也可以是执行到某个阶段的中途就开始执行下一个阶段

还有种特殊情况就是解析可能在初始化之后(因为Java运行时的动态绑定)

基本数据类型不需要加载,引用类型才需要被类加载

类加载阶段

接下来将对这五个阶段进行详细介绍

Loading

加载

  • 加载的作用
  1. 通过这个类的全限定名来查找并加载这个类的二进制字节流
  • JVM通过文件系统加载某个class后缀文件
  • 读取jar包中的类文件
  • 数据库中类的二进制数据
  • 使用类似HTTP等协议通过网络加载
  • 运行时动态生成Class二进制数据流
  1. 将这个类所代表的静态存储结构(静态常量池)转化为方法区运行时数据结构(运行时常量池)
  2. 在堆中创建这个类的Class对象,这个Class对象是对方法区访问数据的"入口"
  • 堆中实例对象中对象头的类型指针指向它这个类方法区的类元数据
  • 对于加载可以由JVM的自带类加载器来完成,也可以通过开发人员自定义的类加载器来完成(实现ClassLoader,重写findClass())

注意

  1. 数组类是直接由JVM在内存中动态构造的,数组中的元素还是要靠类加载器进行加载
  2. 反射正是通过加载创建的Class对象才能在运行期使用反射

Verification

验证

  • 验证的作用
    确保要加载的字节码符合规范,防止危害JVM安全
  • 验证的具体划分
  • 文件格式验证
    目的: 保证字节流能正确解析并存储到方法区之内,格式上符合Java类型信息
    验证字节流是否符合Class文件格式规范(比如Class文件主,次版本号是否在当前虚拟机兼容范围内...)
  • 元数据验证目的: 对类的元数据信息进行语义验证元数据:简单的来说就是描述这个类与其他类之间关系的信息元数据信息验证(举例):
  1. 这个类的父类有没有继承其他的最终类(被final修饰的类,不可让其他类继承)
  2. 若这个类不是抽象类,那这个类有没有实现(抽象父类)接口的所有方法
  • 字节码验证(验证中最复杂的一步)目的: 对字节码进行验证,保证校验的类在运行时不会做出对JVM危险的行为字节码验证举例:
  1. 类型转换有效: 子类转换为父类(安全,有效) 父类转换为子类(危险)
  2. 进行算术运算,使用的是否是相同类型指令等
  • 符号引用验证
    发生在解析阶段前:符号引用转换为直接引用
    目的: 保证符号引用转为直接引用时,该类不缺少它所依赖的资源(外部类),确保解析可以完成

验证阶段是一个非常重要的阶段,但又不一定要执行(因为许多第三方的类,自己封装的类等都被反复"实验"过了)

在生产阶段可以考虑关闭 -Xverify:none以此来缩短类加载时间

Preparation

准备

准备阶段为类变量(静态变量)分配内存并默认初始化

  • 分配内存
  • 逻辑上应该分配在方法区,但是因为hotSpot在JDK7时将字符串常量,静态变量挪出永久代(放在堆中)
  • 实际上它应该在堆中
  • 默认初始化
  • 类变量一般的默认初始化都是初始化该类型的零值
类型 零值
byte (byte)0
short (short)0
int 0
long 0L
float 0.0F
double 0.0
boolean false
char '\u0000'
reference null

  • 特殊的类变量的字段属性中存在ConstantValue属性值,会初始化为ConstantValue所指向在常量池中的值
  • 只有被final修饰的基本类型或字面量且要赋的值在常量池中才会被加上ConstantValue属性

image.png

Resolution

解析

  • 解析的作用将常量池中的常量池中符号引用替换为直接引用(把符号引用代表的地址替换为真实地址)
  • 符号引用
  • 使用一组符号描述引用(为了定位到目标引用)
  • 与虚拟机内存布局无关
  • 还是符号引用时目标引用不一定被加载到内存
  • 直接引用
  • 直接执行目标的指针,相对偏移量或间接定位目标引用的句柄
  • 与虚拟机内存布局相关
  • 解析直接引用时目标引用已经被加载到内存中
  • 并未规定解析的时间
    可以是类加载时就对常量池的符号引用解析为直接引用
    也可以在符号引用要使用的时候再去解析(动态调用时只能是这种情况)
  • 同一个符号引用可能会被解析多次,所以会有缓存(标记该符号引用已经解析过),多次解析动作都要保证每次都是相同的结果(成功或异常)
类和接口的解析

当我们要访问一个未解析过的类时

  1. 把要解析的类的符号引用 交给当前所在类的类加载器 去加载 这个要解析的类
  2. 解析前要进行符号引用验证,如果当前所在类没有权限访问这个要解析的类,抛出异常IllegalAccessError
字段的解析

解析一个从未解析过的字段

  1. 先对此字段所属的类(类, 抽象类, 接口)进行解析
  2. 然后在此字段所属的类中查找该字段简单名称和描述符都匹配的字段,返回它的直接引用
  • 如果此字段所属的类有父类或实现了接口,要自下而上的寻找该字段
  • 找不到抛出NoSuchFieldError异常
  1. 对此字段进行权限验证(如果不具备权限抛出IllegalAccessError异常)

确保JVM获得字段唯一解析结果

如果同名字段出现在父类,接口等中,编译器有时会更加严格,直接拒绝编译Class文件

方法的解析

解析一个从未解析过的方法

  1. 先对此方法所属的类(类, 抽象类, 接口)进行解析
  2. 然后在此方法所属的类中查找该方法简单名称和描述符都匹配的方法,返回它的直接引用
  • 如果此方法所属类是接口直接抛出IncompatibleClassChangeError异常
  • 如果此方法所属的类有父类或实现了接口,要自下而上的寻找该方法(先找父类再找接口)
  • 如果在接口中找到了,说明所属类是抽象类,抛出AbstractMethodError异常(自身找不到,父类中找不到,最后在接口中找到了,说明他是抽象类),找不到抛出NoSuchMethodError异常
  1. 对此方法进行权限验证(如果不具备权限抛出IllegalAccessError异常)
接口方法的解析

解析一个从未解析过的接口方法

  1. 先对此接口方法所属的接口进行解析
  2. 然后在此接口方法所属的接口中查找该接口方法简单名称和描述符都匹配的接口方法,返回它的直接引用
  • 如果此接口方法所属接口是类直接抛出IncompatibleClassChangeError异常
  • 如果此方法所属的接口有父接口,要自下而上的寻找该接口方法
  • 如果多个不同的接口中都存在这个接口方法,会随机返回一个直接引用(编译会更严格,这种情况应该会拒绝编译)
  1. 找不到抛出NoSuchMethodError

Initializtion

初始化

执行类构造器的过程

  • 什么是 ?
  • javac编译器 在编译期间自动收集类变量赋值的语句和静态代码块合并 自动生成的
  • 如果没有对类变量赋值动作或者静态代码块可能不会生成 (带有ConstantValue属性的类变量初始化已经在准备阶段做过了,不会在这里初始化)
  • 类和接口的类构造器

  • 又叫类构造器,与实例构造器不同,类构造器不用显示父类类构造器调用
    但是父类要在子类之前初始化,也就是完成类构造器
  • 接口
    执行接口的类构造器时,不会去执行它父类接口的类构造器,直到用到父接口中定义的变量被使用时才执行
  • JVM会保证执行在多线程环境下被正确的加锁和同步(也就是只会有一个线程去执行其他线程会阻塞等待,直到完成)
 public class TestJVM {
     static class  A{
         static {
             if (true){
                 System.out.println(Thread.currentThread().getName() + "<clinit> init");
                 while (true){
 ​
                 }
             }
         }
     }
     @Test
     public void test(){
         Runnable runnable = new Runnable() {
             @Override
             public void run() {
                 System.out.println(Thread.currentThread().getName() + "start");
                 A a = new A();
                 System.out.println(Thread.currentThread().getName() + "end");
             }
         };
 ​
         new Thread(runnable,"1号线程").start();
         new Thread(runnable,"2号线程").start();
     }
 ​
 }
 ​
 /*
 1号线程start
 2号线程start
 1号线程<clinit> init
 */

JVM规定6种情况下必须进行初始化(主动引用)

主动引用
  • 遇到new,getstatic,putstatic,invokestatic四条字节码指令
  • new
  • 读/写 某类静态变量(不包括常量)
  • 调用 某类静态方法
  • 使用java.lan.reflect包中方法对类型进行反射
  • 父类未初始化要先初始化父类 (不适用于接口)
  • 虚拟机启动时,先初始化main方法所在的类
  • 某类实现的接口中有默认方法(JDK8新加入的),要先对接口进行初始化
  • JDK7新加入的动态语言支持,部分....
被动引用
  1. 当访问静态字段时,只有真正声明这个字段的类才会被初始化

(子类访问父类静态变量)

 public class TestMain {
     static {
         System.out.println("main方法所在的类初始化");
     }
 ​
     public static void main(String[] args) {
         System.out.println(Sup.i);
     }
 }
 ​
 class Sub{
     static {
         System.out.println("子类初始化");
     }
 }
 ​
 class Sup{
     static {
         System.out.println("父类初始化");
     }
     static int i = 100;
 }
 ​
 /*
 main方法所在的类初始化
 父类初始化
 100
 */

子类调用父类静态变量是在父类类加载初始化的时候赋值的,所以子类不会类加载

  1. 实例数组
 public class TestArr {
     static {
         System.out.println("main方法所在的类初始化");
     }
     public static void main(String[] args) {
         Arr[] arrs = new Arr[1];
     }
 }
 ​
 class Arr{
     static {
         System.out.println("arr初始化");
     }
 }
 ​
 /*
 main方法所在的类初始化
 */

例子里包名为:org.fenixsoft.classloading。该例子没有触发类org.fenixsoft.classloading.Arr的初始化阶段,但触发了另外一个名为“[Lorg.fenixsoft.classloading.Arr”的类的初始化阶段,对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于Object的子类,创建动作由字节码指令anewarray触发. 这个类代表了一个元素类型为org.fenixsoft.classloading.Arr的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为public的length属性和clone()方法)都实现在这个类里。

创建数组时不会对数组中的类型对象(Arr)发生类加载

虚拟机自动生成的一个类,管理Arr的数组,会对这个类进行类加载

  1. 调用静态常量
 public class TestConstant {
     static {
         System.out.println("main方法所在的类初始化");
     }
     public static void main(String[] args) {
         System.out.println(Constant.NUM);
     }
 }
 ​
 class Constant{
     static {
         System.out.println("Constant初始化");
     }
     static final int NUM = 555;
 }
 ​
 /*
 main方法所在的类初始化
 555
 */

我们在连接阶段的准备中说明过,如果静态变量字段表中有ConstantValue(被final修饰)它在准备阶段就已经完成初始默认值了,不用进行初始化

  1. 调用classLoader类的loadClass()方法加载类不导致类初始化

image.png

卸载

方法区的垃圾回收主要有两部分: 不使用的常量和类

回收方法区性价比比较低,因为不使用的常量和类比较少

不使用的常量

没有任何地方引用常量池中的某常量,则该常量会在垃圾回收时,被收集器回收

不使用的类

成为不使用的类需要满足以下要求:

  1. 没有该类的任何实例对象
  2. 加载该类的类加载器被回收
  3. 该类对应的Class对象没在任何地方被引用

注意: 就算被允许回收也不一定会被回收, 一般只会回收自定义的类加载器加载的类

总结

本篇文章围绕类加载阶段流程的加载-验证-准备-解析-初始化-卸载 详细展开每个阶段的细节

加载阶段主要是类加载器加载字节码流,将静态结构(静态常量池)转换为运行时常量池,生成class对象

验证阶段验证安全确保不会危害到JVM,主要验证文件格式,类的元数据信息、字节码、符号引用等

准备阶段为类变量分配内存并默认初始化零值

解析阶段将常量池的符号引用替换为直接引用

初始化阶段执行类构造器(类变量赋值与类代码块的合并)

  • 参考资料
  • 《深入理解Java虚拟机》
  • 部分图片来源网络


相关文章
|
23小时前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
9 2
|
5天前
|
监控 算法 Java
Java虚拟机(JVM)使用多种垃圾回收算法来管理内存,以确保程序运行时不会因为内存不足而崩溃。
【6月更文挑战第20天】Java JVM运用多种GC算法,如标记-清除、复制、标记-压缩、分代收集、增量收集、并行收集和并发标记,以自动化内存管理,防止因内存耗尽导致的程序崩溃。这些算法各有优劣,适应不同的性能和资源需求。垃圾回收旨在避免手动内存管理,简化编程。当遇到内存泄漏,可以借助VisualVM、JConsole或MAT等工具监测内存、生成堆转储,分析引用链并定位泄漏源,从而解决问题。
16 4
|
3天前
|
存储 监控 Java
JVM:Java虚拟机探秘
JVM:Java虚拟机探秘
8 1
|
7天前
|
算法 Java
Java垃圾回收(Garbage Collection,GC)是Java虚拟机(JVM)的一种自动内存管理机制,用于在运行时自动回收不再使用的对象所占的内存空间
【6月更文挑战第18天】Java的GC自动回收内存,包括标记清除(产生碎片)、复制(效率低)、标记整理(兼顾连续性与效率)和分代收集(区分新生代和老年代,用不同算法优化)等策略。现代JVM通常采用分代收集,以平衡性能和内存利用率。
33 3
|
12天前
|
存储 Java 编译器
JVM系列7-虚拟机字节码执行引擎
JVM系列7-虚拟机字节码执行引擎
13 1
|
4天前
|
算法 Java 程序员
JVM虚拟机的故事
JVM虚拟机的故事
3 0
jvm虚拟机中运行时数据区域介绍
jvm虚拟机中,运行时数据区域包括七大部分 i. 程序计算器 i. 定义 1) 极小的内存空间; 2) 行号指示器,程序的分支、循环、跳转、异常处理、线程恢复等基本功能都需要依赖程序计算器; 3) 线程私有的。
6188 0
|
27天前
|
存储 SQL 数据挖掘
服务器数据恢复—误删除VMware虚拟机vmdk文件的数据恢复案例
服务器数据恢复环境: 某大厂PS4000服务器,服务器上部署VMware ESXi虚拟化平台。 服务器故障: 机房断电,重启后服务器中的某台虚拟机不能正常启动。管理员查看虚拟机配置文件,发现无法启动的虚拟机的配置文件除了磁盘文件以外其他配置文件全部丢失,xxx-flat.vmdk磁盘文件和xxx-000001-delta.vmdk快照文件还存在。联系VMware原厂工程师进行诊断,VMware原厂工程师尝试新建一个虚拟机,但发现存储空间不足,于是将故障虚拟机下的xxx-flat.vmdk磁盘文件删除了。VMware工程师重新建了一个虚拟机,分配了固定大小的虚拟磁盘,为虚拟机安装了Window
服务器数据恢复—误删除VMware虚拟机vmdk文件的数据恢复案例
|
5天前
|
存储 IDE 开发工具
【读书笔记】 玩转虚拟机基于Vmware+Windows 虚拟化技术
【读书笔记】 玩转虚拟机基于Vmware+Windows 虚拟化技术
|
6天前
|
虚拟化 UED
vmware-17虚拟机安装教程(保姆级,包含图文讲解,不需注册账户)
vmware-17虚拟机安装教程(保姆级,包含图文讲解,不需注册账户)