?类加载的过程分为 5 个阶段:加载、验证、准备、解析、初始化。
所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,(一个Java文件从编码完成到最终执行,一般主要包括两个过程:编译和运行,其中编译就是把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件,然后运行则是把编译声称的.class文件交给Java虚拟机(JVM)执行。而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。)并在内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用
类加载需要类加载器来完成,类加载器介绍:
引导类加载器 Bootstrap-ClassLoader基于C/C++实现,负责加载Java的核心类库JAVA_HOME\jre\lib\rt.jar,该加载器不继承自ClassLoader抽象类,并且只加载包名为java、javax、sun等开头类,起到对核心源码的保护作用。
扩展类加载器 Extension-ClassLoader,基于Java语言,由sun.misc.Launcher$ExtClassLoader实现,派生于ClassLoader抽象类,从java.ext.dirs系统变量指定的路径中的加载类库,或者JDK安装目录jre\lib\ext目录下加载。
系统类加载器 Application-ClassLoader,基于Java语言,由sun.misc.Launcher$ExtClassLoader实现,它负责加载环境变量ClassPath指定的类库,如果在应用程序中没有自定义类加载器,一般情况下作为程序中默认的类加载器。
类加载阶段,JVM 需要完成 3 个步骤:
? 通过类的全限定名来获取这个类的二进制字节流。
??将字节流转化为方法区的运行时数据结构。
??在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类在方法区的访问入口。
外部可以通过访问代表Person类的 Class 对象来获取 Person的类数据结构(看上图)
Java 编译成字节码后,要运行需要通过 类加载器,将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 fifield(域) 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 class 暴露给 java 使用
_super 即父类
_fifields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
如果这个类还有 父类 没有加载,先加载父类;
加载和链接可能是交替执行的
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
2、链接
2.1验证
① 文件格式验证
四个验证过程中,只有格式验证是建立在二进制字节流的基础上的。格式验证就是对文件是否是0xCAFEBABE开头、class文件版本等信息进行验证,确保其符合JVM虚拟机规范。
这一阶段具体可能包括下面这些验证点:
是否以魔数0xCAFEBABE开头。
主、次版本号是否在当前虚拟机处理范围之内。
常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量。
CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
实际上,第一阶段的验证点还远不止这些,上面这些只是从HotSpot虚拟机源码中摘抄的一小部分内容,该验证阶段的主要目的是保证输入的字节流能正确的解析并存储于方法区之内,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面3个验证阶段全部是基于方法区的存储结果进行的,不会再直接操作字节流。
②元数据验证
元数据验证是对源码语义分析的过程,验证的是子类继承的父类是否是final类;如果这个类的父类是抽象类,是否实现了其父类或接口中要求实现的所有方法;子父类中的字段、方法是否产生冲突等,这个过程把类、字段和方法看做组成类的一个个元数据,然后根据JVM规范,对这些元数据之间的关系进行验证。所以,元数据验证阶段并未深入到方法体内。
这一阶段具体可能包括下面这些验证点:
这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
③字节码验证
既然元数据验证并未深入到方法体内部,那么到了字节码验证过程,这一步就不可避免了。字节码主要是对方法体内部的代码的前后逻辑、关系的校验,例如:字节码是否执行到了方法体以外、类型转换是否合理等。
这一阶段具体可能包括下面这些验证点:
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况
保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
当然,这很复杂。所以,即使是到了如今jdk1.8,也还是无法完全保证字节码验证准确无遗漏的。而且,如果在字节码验证浪费了大量的资源,似乎也有些得不偿失。
④符号引用验证
符号引用的验证其实是发生在符号引用向直接引用转化的过程中,而这一过程发生在解析阶段。
这一阶段具体可能包括下面这些验证点:
符号引用中通过字符串描述的全限定名是否能找到对应的类。
在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问。
验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了
2.2 准备
为 static 变量分配空间,设置默认值
static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
如果 static 变量是 final 的,但属于引用类型,即 new 对象,那么赋值也会在初始化阶段完成
2.3 解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
1、符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
2、直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
符号引用与虚拟机实现的内存布局无关,直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。
如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行
3、初始化
初始化即调用 < cinit >()V 方法,虚拟机会保证这个类的 【构造方法】的线程安全
发生的时机
会导致 类初始化 的情况
main 方法所在的类,总会被首先初始化
首次访问这个类的 静态变量 或 静态方法 时
子类初始化,如果父类还未初始化,会引发
子类访问父类的静态变量,只会触发父类的初始化
Class.forName
new 会导致初始化
不会导致 类初始化 的情况
访问 类的 static final 静态变量(基本类型和字符型)不会触发初始化
类对象.class 不会触发初始化
创建该类的数组不会触发初始化
类加载的 loadClass 方法
Class.forName 的参数2 为 false 时
==================================
字符串常量(string pool也有叫做string literal pool): 位置是对中
字符串常量池里的内容是类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到字符串常量池(String Pool)中。
字符串常量池中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
在HotSpot VM里实现的String Pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了驻留字符串的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
Class常量池(class constant pool)
当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池。
.class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量( 文本字符串、被声明为final的常量、基本数据类型的值 )和符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)。
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。
运行时常量池(runtime constant pool)
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM就会将Class常量池中的内容存放到运行时常量池中。
由此可知,运行时常量池也是每个类都有一个。在上面我也说了,Class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
5.5 字符串常量池的位置(重要)
JDK1.7之前运?时常量池逻辑包含让字符串常量池存放在?法区发, 此时hotspot虚拟机对?法区的实现为永久代。
JDK1.7把 字符串常量池、静态变量拿出来放在了堆中,但是其他的仍在方法区(永久代)
JDK1.8 hotspot移除了永久代?元空间(Metaspace)取?代之,这时候字符串常量池、静态变量还在堆,运?时常量池还在?法区。
5.6 JVM 常量池中存储的是对象还是引用呢?
只要是对象都在在堆中(针对HotSpot虚拟机)
5.7 方法区中的垃圾回收?(常量池中废弃的常量和不再使用的类型)
方法区内常量池之中主要存放的两大类常量:字面量和符号引用。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
————————————————
版权声明:本文为CSDN博主「一个小码农的进阶之旅」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36389060/article/details/122830479