46.【面试宝典】面试宝典-虚拟机类加裁机制

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
全局流量管理 GTM,标准版 1个月
简介: 【面试宝典】面试宝典-虚拟机类加裁机制

前文如上:

39.【面试宝典】面试宝典-redis过期k值回收策略,缓存淘汰策略

40.【面试宝典】面试宝典-redis持久化

41.【面试宝典】面试宝典-redis常用数据类型概述

42.【面试宝典】面试宝典-redis缓存穿透,击穿,雪崩

43.【面试宝典】面试宝典-redis缓存穿透之布隆过滤器

44.【面试宝典】面试宝典-redis分布式锁

45.【面试宝典】面试宝典-另外两种分布式锁

合集参考:面试宝典


文档参考:《深入理解Java虚拟机 JVM高级特性与最佳实践》第3版_周志明

image.png


1. 类加载机制


虚拟机把描述类的数据从 class 文件加载到内存,并对数据进行校验、转换解析和初始 化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。 与那些在编译时需要进行连接工作的语言不同,在 Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。


2. 类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载( Loading )、验证( Verification )、准备( Preparation )、解析( Resolution )、初始化 (Initialization )、使用( Using )和卸载( Unloading) 个阶段 其中验证、准备、解析部分统称为连接( Linking ),这 个阶段的发生顺序,如图所示


网络异常,图片无法展示
|


加载、验证、准备、初始化和卸载这个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化 阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定) 注意,这里笔者写的是按部就班地“开始”,而不是按部就班地“进行”或“完成”,强调这点 是因为这些阶段通常都是五相交叉地混合式进行的,通常会在这个阶段执行的过程中调用、 激活另外一个阶段


什么情况下需要开始类加载过程的第一个阶段:加载?


Java 虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握 但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)


  1. 遇到 new gets ta tic putstatic invokestatic 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化 生成这条指令的最常见的 Java 代码场景是:使用 new 关键宇实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java lang.invoke.MetbodHandle 实例最后的解析结果 REF_getStatic REF_putStatic REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。


对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语: “有且只有”,这种场景中的行为称为对一个类进行主动引用。 除此之外,所有引用类的方式都不会触发初始化,称为被动引用。举例如下:


被动引用的例子之一   通过子类引用父类静态字段,不会导致子类初始化

网络异常,图片无法展示
|


上述代码运行之后,只会输出“ SuperClass init !”,而不会输出“ SubClass init !” 。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现 对于 Sun HotSpot 虚拟 机来说,可通过-XX:+TraceClassLoading 参数观察到此操作会导致子类的加载。


被动引用的例子之二  数组定义类,不会出发此类初始化

网络异常,图片无法展示
|


这块复用了上面例子1的中的 Superclass ,运行之后发现没有输出 “ SuperClass init !”,说明并没有触发类org.fenixsoft.classloading.SuperClass的初始化阶段。但是这段代码里面触发了另外一个名为

网络异常,图片无法展示
|
的类的初始化阶段,对于用户代码来说,这并不是一个合法的类名称,它是个由虚拟机自动生成的、直接继承于java.lang.Object 子类,创建动作由字节码指令 newarray 触发。 \

这个类代表了一个元素类型为 org.fenixsoft.classloading.SuperClass 一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为 public length 属性和 clone()方法)都实现在这个类里。 Java 语言中对数组的访问比 C++ 相对安全是因为这个类封装了数组元素的访问方法。 C++直接翻译为对数组指针的移动.在Java语言中,当检查发生数组越界时 抛出java.lang.Array IndexOutOfBoundsException异常。


被动引用的例子之三  直接引用某个类的 final修饰常量,不会触发此类初始化


网络异常,图片无法展示
|


上述代码运行之后, 没有输 “ConstClass init !”,这是因为虽然在 Java 源码中引用了 ConstClass 中的常量 HELLOWORLD ,但其实在编译阶段通过常量传播优化,已经将此常量的值“hello world” 储到 Notlnitialization 常量池中。以后Notlnitalization 对常量 ConstClass.HELLOWORLD 的引用, 被转化为 Notlnitialization 自身常量池的引用。也就是说,实际上 Notlnitialization Class 件之中并没有 ConstClass 符号引用入口,这两个类在编译成Class后就不存在任何联系


网络异常,图片无法展示
|


3. 类加载过程


接下来分析一下Java虚拟机中类加载的全过程,即加载( Loading )、验证( Verification )、准备( Preparation )、解析( Resolution )、初始化 (Initialization )五个阶段。


3.1 加载


加载的理解

所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。 所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用

反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则 JVM 在运行期也无法反射


加载完成的操作

加载阶段,简言之,查找并加载类的二进制数据,生成 Class 的实例


在加载类时,Java 虚拟机必须完成以下3件事情:

  • 通过类的全名,获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
  • 创建 java.lang.Class 类的实例,表示该类型。作为方法区这个类的各种数据的访问入口


二进制流的获取方式

对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合 JVM 规范即可)

  • 虚拟机可能通过文件系统读入一个 Class 后缀的文件(最常见)
  • 读入 jar、zip 等归档数据包,提取类文件
  • 事先存放在数据库中的类的二进制数据
  • 使用类似于 HTTP 之类的协议通过网络进行加载
  • 在运行时生成一段 Class 的二进制信息等

在获取到类的二进制信息后,Java 虚拟机就会处理这些数据,并最终转为一个 java.lang.Class 的实例

如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError


类模型与 Class 实例的位置


  1. 类模型的位置

加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(JDK 1.8之前:永久代;JDK 1.8之后:元空间)

  1. Class 实例的位置

类将 .class 文件加载至元空间后,会在堆中创建一个 java.lang.Class 对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象

  1. 图示

网络异常,图片无法展示
|

外部可以通过访问代表 Order 类的 Class 对象来获取 Order 的类数据结构


再说明

Class 类的构造方法是私有的,只有 JVM 能够创建

java.lang.Class 实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过 Class 类提供的接口,可以获得目标类所关联的 .class 文件中具体的数据结构:方法、字段等信息


数组类的加载

创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称 A)的过程:

  1. 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组 A 的元素类型
  2. JVM 使用指定的元素类型和数组唯独来创建新的数组类

如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public


3.2 验证


验证的理解


当类加载到系统后,就开始链接操作,验证是链接操作的第一步

它的目的是保证加载的字节码是合法、合理并符合规范的验证的步骤比较复杂,实际要验证的项目也很繁多,大体上 Java 虚拟机需要做以下检查,如图所示


网络异常,图片无法展示
|


整体说明:


验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等

  • 其中格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中
  • 格式验证之外的验证操作将会在方法区中进行

链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查


具体说明:

  1. 格式验证:是否以魔数 0xCAFEBABE 开头,主版本和副版本号是否在当前 Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等
  2. Java 虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:

  3. 是否所有的类都有父类的存在(在 Java 里,除了 Object 外,其他类都应该有父类)
  4. 是否一些被定义为 final 的方法或者类被重写或继承了
  5. 非抽象类是否实现了所有抽象方法或者接口方法
  6. 是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;absract 情况下的方法,就不能是final 的了)
  7. Java 虚拟机还会进行字节码验证,字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:
  8. 在字节码的执行过程中,是否会跳转到一条不存在的指令
  9. 函数的调用是否传递了正确类型的参数
  10. 变量的赋值是不是给了正确的数据类型等


栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是,100%准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的

在前面3次检查中,已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的


  1. 校验器还将进行符号引用的验证。Class 文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,虚拟机就会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出 NoClassDefFoundError,如果一个方法无法被找到,则会抛出 NoSuchMethdError


此阶段在解析环节才会执行


3.3 准备


准备的理解

准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值

当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。


默认初始值配置

Java 虚拟机为各类型变量默认的初始值如表所示:

网络异常,图片无法展示
|


注意:Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故对应的,boolean 的默认值就是 false


注意:

  1. 这里不包含基本数据类型的字段用 static final 修饰的情况,因为 final 在编译的时候就会分配了,准备阶段会显式赋值
  2. 注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中
  3. 在这个阶段不会像初始化阶段中那样会有初始化或者代码被执行


/**
 * <p>
 * 基本数据类型:非 final 修饰的变量,在准备环节进行默认初始化赋值
 * final 修饰以后,在准备环节直接进行显式赋值
 * <p>
 * 拓展:如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节直接进行显式赋值
 */
public class LinkingTest {
    private static long id;
    private static final int num = 1;
    public static final String constStr = "CONST";
    public static final String constStr1 = new String("CONST");
}


3.4 解析


解析的理解

在解析阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用


具体描述


符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存分布无关。比较容理解的就是在 Class 类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下 println() 方法被调用时,系统需要明确知道该方法的位置


举例:输出操作 System.out.println() 对应的字节码:


invokevirtual #24

网络异常,图片无法展示
|


以方法为例,Java 虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用


小结


所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构

不过 Java 虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在 HotSpot VM 中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着 JVM 在执行完初始化之后再执行


字符串的复习


最后,再来看一下 CONSTANT_String 的解析。由于字符串在程序开发中有着重要的作用,因此,读者有必要了解一下 String 在 Java 虚拟机中的处理。当在 Java 代码中直接使用字符串常量时,就会在类中出现 CONSTANT_String,它表示字符串常量,并且会引用一个 CONSTANT_UTF8 的常量项。在 Java 虚拟机内部运行中的常量池,会维护一张字符串拘留表(intern),它会保存所有出现过的字符串常量,并且没有重复项。只要以 CONSTANT_String 形式出现的字符串也都会在这张表中。使用 String.intern() 方法可以得到一个字符串在拘留表中的引用,因为该表中没有重复项,所以任何字面相同的字符串的 String.intern() 方法返回总是相等的


3.5 初始化


类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行 Java 字节码。


初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块) 中的语句合并产生的。


  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。


公众号,感谢关注

网络异常,图片无法展示
|


相关文章
|
2月前
|
安全 Java 容器
【Java集合类面试二十七】、谈谈CopyOnWriteArrayList的原理
CopyOnWriteArrayList是一种线程安全的ArrayList,通过在写操作时复制新数组来保证线程安全,适用于读多写少的场景,但可能因内存占用和无法保证实时性而有性能问题。
|
2月前
|
Java
【Java集合类面试二十八】、说一说TreeSet和HashSet的区别
HashSet基于哈希表实现,无序且可以有一个null元素;TreeSet基于红黑树实现,支持排序,不允许null元素。
|
2月前
|
Java
【Java集合类面试二十六】、介绍一下ArrayList的数据结构?
ArrayList是基于可动态扩展的数组实现的,支持快速随机访问,但在插入和删除操作时可能需要数组复制而性能较差。
|
1月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
1天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
9 1
|
3天前
|
监控 架构师 Java
从蚂蚁金服面试题窥探STW机制
在Java虚拟机(JVM)中,垃圾回收(GC)是一个至关重要的机制,它负责自动管理内存的分配和释放。然而,垃圾回收过程并非没有代价,其中最为显著的一个影响就是STW(Stop-The-World)机制。STW机制是指在垃圾回收过程中,JVM会暂停所有应用线程的执行,以确保垃圾回收器能够正确地遍历和回收对象。这一机制虽然保证了垃圾回收的安全性和准确性,但也可能对应用程序的性能产生显著影响。
11 2
|
5天前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
|
7天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
10 1
|
1月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
56 8
|
2月前
|
存储 Java
【Java集合类面试二十九】、说一说HashSet的底层结构
HashSet的底层结构是基于HashMap实现的,使用一个初始容量为16和负载因子为0.75的HashMap,其中HashSet元素作为HashMap的key,而value是一个静态的PRESENT对象。