当前位置: 首页 > news >正文

JVM—虚拟机类加载时机与过程

参考资料:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明

1. 类加载的时机

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析统称为链接。

除了初始化外,其他阶段的顺序是按部就班的开始,注意是开始而不是执行,因为这些阶段都是混合交叉的进行。

关于什么阶段加载、JVM规范没有强制约束,而是交给虚拟机自由把握,但是JVM规范严格规定了六种情况必须立即对类进行初始化(加载、验证、准备、解析自然在此之前)

  1. 遇到new、static、putstatic、invokestatic这四条字节码指令时。

  2. 反射调用时

  3. 初始化类时,其父类还未初始化,则需要先触发父类的初始化。

  4. JVM启动时用户指定执行的主类(包含main方法的类)

  5. 接口中定义JDK8加入的默认方法(被Default方法修饰的接口方法),这个接口类必须在实现类之前初始化。

  6. JDK7之后的动态语言支持.......

被动引用的例子如下:

1、通过子类引用父类的静态属性,子类不会触发初始化,只会触发父类的初始化。

public class SuperClass {static {System.out.println("SuperClass init!");}public static int value = 123;
}public class SubClass extends SuperClass{static {System.out.println("SubClass init!");}
}public class NotInitialization {public static void main(String[] args) {System.out.println(SubClass.value);}
}
// 输出结果:
SuperClass init!
123

2、用数组定义引用类不会触发初始化

class NotInitialization {public static void main(String[] args) {SuperClass[] classes = new SuperClass[10];}
}
//该代码没有任何输出

3、常量在编译阶段进入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的初始化。

class ConstClass {static {System.out.println("ConstClass init!");}public static final String HELLOWORLD = "hello world";
}class NotInitialization {public static void main(String[] args) {System.out.println(ConstClass.HELLOWORLD); //hello world}
}

2. 类加载过程

2.1 加载

加载阶段是整个类加载过程中的一个阶段需要注意。

在加载阶段JVM需要完成以下三件事:

  1. 通过一个类的全限定名获取定义此类的二进制字节流

  2. 将这个字节流的静态存储结构转化为方法区运行时数据结构

  3. 在内存中生成一个代表类的Class对象,作为方法区这个类的各种数据访问入口。

对于数组类不是通过类加载器创建的,而是由JVM在内存中动态构建的。但是数组类最终还是由类加载器完成加载。

2.2 验证

为了确保Class文件的字节流中包含的信息符合JVM的规范约束要求,确保不会危害虚拟机。

验证阶段会完成以下四个检验动作。

2.2.1 文件格式规范

验证字节流是否符合Class文件格式规范,例如:魔点、主次版本号、常量池等等,参考类文件结构

只有通过验证字节流才会进入JVM内存的方法区读取,所以后面三个验证阶段都是基于方法区内存结构进行的

2.2.2 元数据验证

对字节码描述的信息进行语义分析校验,这个阶段验证点如下:

  1. 验证是否具有父类

  2. 是否继承不允许继承的类(final修饰的类)

  3. 如果这个类不是抽象类,是否实现了父类/接口中的所有方法

  4. 类的字段、方法是否与父类矛盾

2.2.3 字节码验证

这是验证过程中最复杂的,主要目的是通过数据流分析和控制流分析,确定程序语义合法性

在第二阶段对元数据信息中数据类型校验完毕后对类的方法体(Class文件中的Code属性)进行校验分析。例如:

保证方法体中的类型转化总是有效的,例如把子类对象赋值给父类数据类,但不能把父类赋值给子类,或者毫不相干的数据类型。

2.2.4 符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段—解析过程中发生。

  • 符号引用验证就是验证该类是否缺少或者被禁止访问它依赖的某些外部类,方法,字段等资源。

符号引用验证需要检验以下内容:

  1. 通过字符串描述的全限定名是否可以找到对应的类

  2. 指定类是否存在符合方法的字段描述符

  3. 符号引用中类、字段、方法的可访问性,是否可被当前类访问。

2.3 准备

准备阶段是正式为类中定义的类变量(static变量)分配内存并设置初始值的阶段,这些在JDK7及之前放在了方法区中,JDK8及之后放在了Java堆中。

  • 需要注意的是这里分配的内存仅仅是给类变量分配内存,而实例变量在运行时随着对象实例化才会分配内存。

  • 这里所说的初始状是0,例如public static int i = 123,在准备阶段完成后初始值是0而不是123。

  • 如果类字段的字段属性表中存在ConstantValue的属性,那么就会被初始化为指定的值,例如:public static final int i = 123

2.4 解析

解析阶段是将常量池内的符号引用替换为直接引用的过程,解析过程的符号引用和直接引用关联如下:

  • 符号引用:符号引用是以一组符号来描述引用的目标,可以是任何形式的字面量。和JVM内存布局无关。

  • 直接引用:直接引用是可以指向目标的指针,相对偏移量或者一个句柄。和JVM内存布局有关,同一个符号在不同虚拟机翻译的直接引用一般不同。

2.4.1 类或接口的解析

假设当前为D类,如果把一个未解析过的符号引用N解析为一个类或者接口C的直接引用,那么JVM需要完成以下3个步骤:

  1. 如果C不是一个数组类型,虚拟机会将代表N的全限定名传递给D的类加载器,去加载这个类。在加载过程中可能触发其他类的加载,一旦出现问题那么解析失败。

  2. 如果C是一个数组类型,并且元素类型为对象。那么就会按照第一点的方式去加载这个类。

  3. 前两步没有异常。那么C一个在JVM成为了一个有效的类/接口,但在解析完成前还需进行符号引用验证。

2.4.2 字段解析

在解析一个字段之前,首先对字段表内的符号引用解析,也就是字段所属的类或者接口的符号引用。接下来按照如下步骤对C后续的字段搜索:

  1. 如果C本身包含简单名称和字段描述符都与目标匹配的字段,那么直接返回这个字段的直接引用。

  2. 否则、如果C中实现接口,那么会按照继承关系从下往上一个一个搜索,如果接口出现了相匹配的字段,那么返回这个直接引用。

  3. 否则、如果C不是Object的话,也会按照继承关系向上搜索,直到出现匹配的字段。

  4. 否则、查找失败抛出NoSuchAccessError

2.4.3 方法解析

第一个步骤和字段解析一样,解析方法所属的类/接口的引用。解析成功依然用C表示这个类。

2.4.4 接口方法解析

和上面一样。

2.5 初始化

初始化是类加载过程中最后一个动作,在初始化中JVM才将开始执行类中编写的java代码逻辑,将控制权交给应用程序。

在准备阶段时,变量已经赋值过一次系统要求的初始零值,而在初始化阶段就复制为编码制定的值。

1、非法向前引用—Illegal forward reference

static {i = 0;System.out.println(i);
}static int  i = 1;

2、<clinit>方法的执行顺序

<clinit>方法与实例构造器<init>方法不同,它不需要显示的调用父类的构造器。

JVM会保证父类的<clinit>方法一定先于子类执行,也就是父类静态代码块执行先于子类,在下面这个例子中也就是说父类静态代码块优于子类的赋值操作。

public class Main {public static void main(String[] args) {System.out.println(Sub.B); // 2}
}class Parent{public static int A = 1;static {A = 2;}
}class Sub extends Parent{public static int B = A;
}

3、字段解析

JVM必须保证一个类的<clinit>方法在多线程下被正确的加锁同步,如果多个线程同时初始化一个类,那么只有一个线程才去执行这个类的<clinit>方法,其他线程都被阻塞。

  • 所以一个类的<clinit>方法中有很多耗时操作就会导致多个线程阻塞。

public class DeadLoopClass {static {if (true) {System.out.println(Thread.currentThread() + "init DeadLoopClass");while (true);}}public static void main(String[] args) {Runnable script = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread() + " start");new DeadLoopClass();System.out.println(Thread.currentThread() + " run over");}};new Thread(script).start();new Thread(script).start();}
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • C++之类和对象(上)
  • uniapp打开地图直接获取位置
  • SpringBoot中的server.context-path
  • Spring Boot 整合 Dubbo3 + Nacos 2.4.0【进阶】+ 踩坑记录
  • docker部署hadoop集群
  • 3D,从无知到无畏
  • 使用ChatGPT4o+colab+gradio+huggingface1小时内,完成快速搭建任何AI应用程序或网站【详细教程步骤】建议收藏
  • 11个行为型模式
  • 【Python问题集锦】如何对不规则的时间序列进行对齐和插值
  • Sqlserver 备份表
  • 扩散模型系列笔记(一)——DDPM
  • 拦截指定http请求头,请求参数,请求方法,保存到本地
  • 左神学习笔记-岛屿数量问题(java版算法)
  • 堆排序以及向上、向下调整算法的时间复杂度推导及实现(超详细)
  • 五种创建springBoot项目的方法(本质上是三种)
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • Babel配置的不完全指南
  • C++回声服务器_9-epoll边缘触发模式版本服务器
  • CSS盒模型深入
  • CSS魔法堂:Absolute Positioning就这个样
  •  D - 粉碎叛乱F - 其他起义
  • ESLint简单操作
  • Git初体验
  • React-redux的原理以及使用
  • Sass Day-01
  • Vue2.0 实现互斥
  • Vue实战(四)登录/注册页的实现
  • 电商搜索引擎的架构设计和性能优化
  • 码农张的Bug人生 - 初来乍到
  • 前言-如何学习区块链
  • 浅析微信支付:申请退款、退款回调接口、查询退款
  • 如何利用MongoDB打造TOP榜小程序
  • 如何在GitHub上创建个人博客
  • 我感觉这是史上最牛的防sql注入方法类
  • 我有几个粽子,和一个故事
  • 在Mac OS X上安装 Ruby运行环境
  • #Ubuntu(修改root信息)
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • #数据结构 笔记一
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (C++二叉树05) 合并二叉树 二叉搜索树中的搜索 验证二叉搜索树
  • (LNMP) How To Install Linux, nginx, MySQL, PHP
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (附源码)spring boot北京冬奥会志愿者报名系统 毕业设计 150947
  • (回溯) LeetCode 40. 组合总和II
  • (解决办法)ASP.NET导出Excel,打开时提示“您尝试打开文件'XXX.xls'的格式与文件扩展名指定文件不一致
  • (蓝桥杯每日一题)love
  • (四)linux文件内容查看
  • (转)大型网站架构演变和知识体系
  • (转)四层和七层负载均衡的区别
  • (转)用.Net的File控件上传文件的解决方案
  • (转载)OpenStack Hacker养成指南
  • (自适应手机端)响应式服装服饰外贸企业网站模板
  • *** 2003
  • .cfg\.dat\.mak(持续补充)