JVM 深度分析】类加载和类加载器
本文思维导图:
类生命周期 7 个阶段
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking)
阶段顺序
加载、校验(验证)、准备、初始化和卸载这五个阶段的顺序是确定的,但是对于“解析”阶段则不一定,它在某些情况下可以在初始化之后再开始,这样做是为了支持 java 的运行时绑定特征(也称为动态绑定或晚期绑定)。
一、加载的时机
什么是需要开始类第一个阶段“加载”,虚拟机规范没有强制约束,这点交给虚拟机的具体实现来*把控。JVM 虚拟机的实现都是使用的懒加载,就是什么时候需要这个类了我才去加载,并不是说一个 jar 文件里面有 200 多个类,但实际我只用到了其中的一个类,我不需要把 200 多个类全部加载进来。(如果你自己写一个 JVM 倒是可以这么干!)
“加载 loading”阶段是整个类加载(class loading)过程的一个阶段。
加载阶段虚拟机需要完成以下 3 件事情:
一、通过一个类的全限定名来获取定义此类的二进制字节流。
二、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
三、在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
注意:比如“通过一个类的全限定名来获取定义此类的二进制字节流”没有指定一定得从某个 class 文件中获取,所以我们可以从 zip 压缩包、从网络中获取、运行时计算生成、数据库中读取、或者从加密文件中获取等等。我们也可以通过前面的工具 JHSDB 可以看到,JVM 启动后,相关的类已经加载进入了方法区,成为了方法区的运行时结构。
JHSDB 怎么用?具体见 JHSDB 工具
1、Attarch 上 JVM 启动的进程
2、打开 Class Browser
3、可以看到很多 class 已经被加载进来了
4、找到 JVMObject, 注意!这里已经是内存了,所以说相关的类已经加载进入了方法区,成为了方法区的运行时结构。
二、验证
是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体
上看,验证阶段大致上会完成下面 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
文件格式验证(非重点)
第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:
一、是否以魔数 OxCAFEBABE 开头。
二、主、次版本号是否在当前 Java 虚拟机接受范围之内。
三、常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
四、指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
五、CONSTANT Utf8 info 型的常量中是否有不符合 UTF-8 编码的数据。
六、Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。
......
以上的部分还只是一小部分,没必要进行深入的研究。
总结一下:这阶段的验证是基于二进制字节流进行的 , 只有通过了这个阶段的验证之后 , 这段字节流才被允许进人 Java 虚拟机内存的方法区中进行存储 , 所以后面的三个验证阶段全部是基于方法区的存储结构(内存)上进行的,不会再直接读取、操作字节流了。
元数据验证(非重点)
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java 语言规范》的要求,这个阶段可能包括的验证点如下:
一、这个类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)。
二、这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
三、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
四、类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
......
以上的部分还只是一小部分,没必要进行深入的研究。
元数据验证是验证的第二阶段,主要目的是对类的元数据信息进行语义校验,保证不存在与《Java 语言规范》定义相悖的元数据信息。
字节码验证(非重点)
字节码验证第三阶段是整个验证过程中最复杂的一一个阶段, 主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:
一、保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表中”这样的情况。
二、保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
三、保证方法体中的类型转换总是有效的,例如可以把-个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
......
以上的部分还只是一小部分,没必要进行深入的研究。
如果一个类型中有方法体的字节码没有通过字节码验证,那它肯定是有问题的。
符号引用验证(非重点)
最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段一解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。本阶段通常需要校验下列内容。
一、符号引用中通过字符串描述的全限定名是否能找到对应的类。
二、在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
三、符号引用中的类、字段、方法的可访问性( private、 protected. public、 <package> )
四、是否可被当前类访问。
......
符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,将会抛出异常。
验证(总结)
验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、 但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html (官方文档)
三、准备
准备阶段是正式为类中定义的变量(被 static 修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下:
首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:public static int value=123;那变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 123 是后续的初始化环节。
基本数据类型的零值表
编辑
四、解析
解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程。
符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
直接引用的对象都存在于内存中,你可以把通讯录里的女友手机号码,类比为符号引用,把面对面和你吃饭的女朋友,类比为直接引用。
解析大体可以分为:(不重要)
一、类或接口的解析
二、字段解析·········
三、类方法解析
四、接口方法解析
我们了解几个经常发生的异常,就与这个阶段有关。
java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)
java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)
java.lang.NoSuchMethodError 找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)
五、初始化(重点)
初始化主要是对一个 class 中的 static{}语句进行操作(对应字节码就是 clinit 方法)。
<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
初始化阶段,虚拟机规范则是严格规定了有且只有 6 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
一、遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的Java 代码场景是:
(一)使用 new 关键字实例化对象的时候。
(二)读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候
(三)调用一个类的静态方法的时候。
二、使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
三、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
四、当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类。
五、当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
六、当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
案例分析:
/** * 初始化的场景 * 通过VM参数可以观察操作是否会导致子类的加载-XX:+TraceClassLoading * * @author macfmc * @date 2020/8/22-19:47 */ public class Initialization { public static void main(String[] args) { Initialization initialization = new Initialization(); initialization.M1();//打印子类的静态字段 initialization.M2();//使用数组的方式创建 initialization.M3();//打印一个常量 initialization.M4();//.如果使用常量去引用另外一个常量 } public void M1() { //如果通过子类引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化(但是子类会被加载) System.out.println(SubClaszz.value); } public void M2() { //使用数组的方式,不 会触发初始化(同样不会触发子类加载) SuperClazz[] sca = new SuperClazz[10]; } public void M3() { //打印一个常量,不会触发初始化(同样不会触类加载) System.out.println(SuperClazz.HELLOWORLD); } public void M4() { // 如果使用常量去引用另外一一个常量(这个值未知,所以必须要触发初始化) System.out.println(SuperClazz.WHAT); } } /** * @author macfmc */ public class SuperClazz { static { System.out.println("SuperClazz init! "); } public static int value = 10; public static String HELLOWORLD = "123"; public static final int WHAT = 20; } package main.java.JVM; /** * @author macfmc */ public class SubClaszz extends SuperClazz { static { System.out.println("SubClaszz init! "); } }
线程安全
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。所以类的初始化是线程安全的,项目中可以利用这点。
类加载器
整个类加载过程任务非常繁重,虽然这活儿很累,但总得有人干。类加载器做的就是上面 5 个步骤的事(加载、验证、准备、解析、初始化)。
JDK提供的三层类加载器(重点)
Bootstrap ClassLoader
这是加载器中的扛把子,任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。这个加载器是 C++ 编写的,随着 JVM 启动。
Extention ClassLoader
扩展类加载器,主要用于加载 lib/ext 目录下的 jar 包和 .class 文件。同样的,通过系统变量 java.ext.dirs 可以指定这个目录。这个加载器是个 Java 类,继承自 URLClassLoader。
Application ClassLoader
这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader。一般用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码,会首先尝试使用这个类加载器进行加载。
Custom ClassLoader
自定义加载器,支持一些个性化的扩展功能。
类加载器的问题
如果你在项目代码里,写一个 java.lang 的包,然后改写 String 类的一些行为,编译后,发现并不能生效。JRE 的类当然不能轻易被覆盖,否则会被别有用心的人利用,这就太危险了。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。这里所指的“相等”,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
双亲委派机制(重点)
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
编辑
我们可以翻阅 JDK 代码的 ClassLoader#loadClass 方法,来看一下具体的加载过程。和我们描述的一样,它首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。同时,我们也注意到,这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效。
编辑
自定义类加载器
Tomcat 类加载机制
编辑
tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。简单看一下 tomcat 类加载器的层次结构。
对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。
这个加载器用来隔绝不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。
如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。
那么 tomca
推荐阅读
-
JVM] 类文件格式、JVM 加载类文件过程、JVM 运行时内存区域、对象分配内存过程
-
说明 css3:核心技术和案例分析2.8 结构伪类选择器
-
双亲委托模型和 Flink 的类加载策略--父类优先的类加载策略
-
[姿势估计] 实践记录:使用 Dlib 和 mediapipe 进行人脸姿势估计 - 本文重点介绍方法 2):方法 1:基于深度学习的方法:。 基于深度学习的方法:基于深度学习的方法利用深度学习模型,如卷积神经网络(CNN)或递归神经网络(RNN),直接从人脸图像中学习姿势估计。这些方法能够学习更复杂的特征表征,并在大规模数据集上取得优异的性能。方法二:基于二维校准信息估计三维姿态信息(计算机视觉 PnP 问题)。 特征点定位:人脸姿态估计的第一步是通过特征点定位来检测和定位人脸的关键点,如眼睛、鼻子和嘴巴。这些关键点提供了人脸的局部结构信息,可用于后续的姿势估计。 旋转表示:常见的旋转表示方法包括欧拉角和旋转矩阵。欧拉角通过三个旋转角度(通常是俯仰、偏航和滚动)描述头部的旋转姿态。旋转矩阵是一个 3x3 矩阵,表示头部从一个坐标系到另一个坐标系的变换。 三维模型重建:根据特征点的定位结果,三维人脸模型可用于姿势估计。通过将人脸的二维图像映射到三维模型上,可以估算出人脸的旋转和平移信息。这就需要建立人脸的三维模型,然后通过优化方法将模型与特征点对齐,从而获得姿势估计结果。 特征点定位 特征点定位是用于检测人脸关键部位的五官基础部分,还有其他更多的特征点表示方法,大家可以参考我上一篇文章中介绍的特征点检测方案实践:人脸校正二次定位操作来解决人脸校正的问题,客户在检测关键点的代码上略有修改,坐标转换部分客户见上图 def get_face_info(image). img_copy = image.copy image.flags.writeable = False image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = face_detection.process(image) # 在图像上绘制人脸检测注释。 image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) box_info, facial = None, None if results.detections: for detection in results. for detection in results.detections: mp_drawing.Drawing.detection = 无 mp_drawing.draw_detection(image, detection) 面部 = detection.location_data.relative_keypoints 返回面部 在上述代码中,返回的数据是五官(6 个关键点的坐标),这是用 mediapipe 库实现的,下面我们可以尝试用另一个库:dlib 来实现。 使用 dlib 使用 Dlib 库在 Python 中实现人脸关键点检测的步骤如下: 确保已安装 Dlib 库,可使用以下命令: pip install dlib 导入必要的库: 加载 Dlib 的人脸检测器和关键点检测器模型: 读取图像并将其灰度化: 使用人脸检测器检测图像中的人脸: 对检测到的人脸进行遍历,并使用关键点检测器检测人脸关键点: 显示绘制了关键点的图像: 以下代码将参数 landmarks_part 添加到要返回的关键点坐标中。
-
JVM 类加载子系统
-
01-JVM 学习记录-类加载器
-
JVM (V) - 类加载阶段 - II.
-
Android 开发中 nodpi、xhdpi、hdpi、mdpi、ldpi 的概念 - 术语和概念 屏幕尺寸 屏幕的物理尺寸,基于屏幕的对角线长度(如 2.8 英寸、3.5 英寸)。 简而言之,安卓系统将所有屏幕尺寸简化为三大类:大、普通和小。 程序可以为这三种屏幕尺寸提供三种不同的布局选项,然后系统会以合适的方式将布局选项呈现到相应的屏幕上,这个过程不需要程序员用代码进行干预。 屏幕纵横比 屏幕的物理长度与物理宽度之比。程序只需使用系统提供的资源分类器 long(长)和 notlong(不长),就能为具有特定长宽比的屏幕提供配制材料。 分辨率 屏幕的像素总数。请注意,分辨率并不意味着长宽比,尽管在大多数情况下,分辨率表示为 "宽度 x 长度"。在安卓系统中,程序一般不直接处理分辨率。 密度 根据屏幕分辨率,沿屏幕宽度和长度排列的像素数量。 密度较低的屏幕在长度和宽度方向上的像素都相对较少,而密度较高的屏幕通常会在同一区域内排列很多甚至非常非常多的像素。屏幕的密度非常重要;例如,一个界面元素(如按钮)的长度和宽度以像素为单位,在低密度屏幕上会显得很大,但在高密度屏幕上就会显得很小。 独立于密度的像素(DIP)是指程序用来定义界面元素的抽象意义上的像素。它作为一个与实际密度无关的单位,帮助程序员构建布局方案(界面元素的宽度、高度和位置)。 与密度无关的像素在逻辑上与像素密度为 160 DPI 的屏幕上的像素大小相同,而 160 DPI 是安卓平台默认的显示设备。在运行时,平台会以目标屏幕的密度为基准,"透明 "地处理所有所需的 DIP 缩放操作。要将与密度无关的像素转换为屏幕像素,可以使用一个简单的公式:像素 = DIP * (密度 / 160)。例如,在 240 DPI 的屏幕上,1 个 DIP 等于 1.5 个物理像素。强烈建议使用 DIP 来定义程序界面的布局,因为这样可以确保用户界面在所有分辨率的屏幕上都能正常显示。 为了简化程序员在面对各种分辨率时的麻烦,也为了让各种分辨率的平台都能直接运行这些程序,Android 平台将所有屏幕以密度和分辨率作为分类方式,分别分为三类:- 三大尺寸:大、普通、小;- 三种不同密度:高(hdpi)、中(mdpi)和低(ldpi)。DPI 表示 "每英寸点数",即每英寸的像素数。如果需要,程序可以为不同的屏幕尺寸提供不同的资源(主要是布局),为不同的屏幕密度提供不同的资源(主要是位图)。除此之外,程序无需对屏幕尺寸或密度进行任何额外处理。执行时,平台会根据屏幕本身的尺寸和密度特性自动加载相应的资源,并将其从逻辑像素(DIP,用于定义界面布局)转换为屏幕上的物理像素。
-
JVM 类加载机制
-
jvm 的 java 类加载机制和类加载器(ClassLoader)的详细信息