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

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 深入浅出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虚拟机》
  • 部分图片来源网络


相关文章
|
3月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
1月前
|
Java
jvm复习,深入理解java虚拟机一:运行时数据区域
这篇文章深入探讨了Java虚拟机的运行时数据区域,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、元空间和运行时常量池,并讨论了它们的作用、特点以及与垃圾回收的关系。
63 19
jvm复习,深入理解java虚拟机一:运行时数据区域
|
26天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
41 1
|
1月前
|
算法 Java
谈谈HotSpot JVM 中的不同垃圾回收器
【10月更文挑战第5天】理解 HotSpot JVM 中的不同垃圾回收器(如 CMS、G1 和 ZGC)的区别,需要深入了解它们的设计原理、工作方式和应用场景。以下是对这三个垃圾回收器的简要概述以及一个示例 Java 程序,虽然示例程序本身不能直接展示垃圾回收器的内部机制,但可以帮助观察不同垃圾回收器的行为。
25 1
|
1月前
|
存储 Java C语言
【JVM】类加载机制
【JVM】类加载机制
20 1
|
3月前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
58 0
|
3月前
|
安全 前端开发 Java
【JVM 探秘】ClassLoader 类加载器:揭秘 Java 类加载机制背后的秘密武器!
【8月更文挑战第25天】本文全面介绍了Java虚拟机(JVM)中的类加载器,它是JVM的核心组件之一,负责将Java类加载到运行环境中。文章首先概述了类加载器的基本工作原理及其遵循的双亲委派模型,确保了核心类库的安全与稳定。接着详细阐述了启动、扩展和应用三种主要类加载器的层次结构。并通过一个自定义类加载器的例子展示了如何从特定目录加载类。此外,还介绍了类加载器的完整生命周期,包括加载、链接和初始化三个阶段。最后强调了类加载器在版本隔离、安全性和灵活性方面的重要作用。深入理解类加载器对于掌握JVM内部机制至关重要。
151 0
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
9天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。