本文在尚硅谷的JVM视频教程和一些优秀CSDN博主以及GitHub上的优秀作者的创作基础上加入了自己的理解。由于引用来源的较多,请相关部分作者联系本人,以注明出处。
这是参考的一些链接:
https://github.com/youthlql/JavaYouth、
https://blog.csdn.net/weixin_43591980
第2章-类加载子系统
内存结构概述
简图
详细图
英文版
中文版
注意:方法区只有HotSpot虚拟机有,J9,JRockit都没有
如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
- 类加载器
- 执行引擎
类加载器子系统
类加载器子系统作用:
-
类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
-
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。(相亲对象给你加载好了,你能不能成看你了)
-
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader角色
- class file(在下图中就是Car.class文件)存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file加载到JVM中,被称为DNA元数据模板(InstanceKClass类实例),放在方法区。
- 在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
有关KClass和类加载的知识可以见:https://blog.csdn.net/qq_43631716/article/details/115611129
类加载过程
概述
public class HelloLoader {
public static void main(String[] args) {
System.out.println("谢谢ClassLoader加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
它的加载过程是怎么样的呢?
- 执行 main() 方法(静态方法)就需要先加载main方法所在类 HelloLoader
- 加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main
- 加载失败则抛出异常
完整的流程图如下所示:
加载阶段
加载:
-
通过一个类的全限定名获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
1、通过类的全限定名获取存储该类的class文件(没有指明必须从哪里获取);
2、解析运行时数据区,即instanceKlass实例,存放在方法区;
3、在堆区生成该类的Class对象,即instanceMirrorKlass对象。有关KClass和类加载的知识可以见:https://blog.csdn.net/qq_43631716/article/details/115611129
加载class文件的方式:
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
-
_java_mirror
即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用 -
_super
即父类 -
_fields
即成员变量 -
_methods
即方法 -
_constants
即常量池 -
_class_loader
即类加载器 -
_vtable
虚方法表 -
_itable
接口方法表 -
如果这个类还有父类没有加载,则先触发父类的加载。
-
加载和链接可能是交替运行的。
注意:
- instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但
_java_mirror
是存储在堆中- 可以通过前面介绍的 HSDB 工具查看
链接阶段
链接分为三个子阶段:验证 -> 准备 -> 解析
验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
举例
使用 BinaryViewer软件查看字节码文件,其开头均为 CAFE BABE ,如果出现不合法的字节码文件,那么将会验证不通过。
准备(Prepare)
- 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值,初始化阶段被显式赋值
- 这里不包含用final修饰的static,因为final static在编译的时候就会分配好了默认值,准备阶段会显式初始化
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
- 注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,(JDK7及其之后,静态变量、字符常量是放在java的堆中)而实例变量是会随着对象一起分配到Java堆中
举例
代码:变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1
public class HelloApp {
private static int a = 1;//prepare:a = 0 ---> initial : a = 1
public static void main(String[] args) {
System.out.println(a);
}
}
针对情况3举例
public class JVMtest1 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
}
E的字节码
main的字节码,可以看出final static修饰的变量直接用值就能替换
解析(Resolve)
-
将常量池内的符号引用转换为直接引用的过程
-
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
-
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
-
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
符号引用
- 反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用
初始化阶段
类的初始化时机
- 创建类的实例,new
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.atguigu.Test”))
- 初始化一个类的子类,那么此类会先初始化
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段(不会调用 clinit() 方法和 init() 方法)
不会导致类初始化的情况:
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
clinit()
-
初始化阶段就是执行类构造器方法
<clinit>()
的过程 -
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法,没有类变量(static变量、静态变量)的时候,就没有clinit方法。
-
<clinit>()
方法中的指令按语句在源文件中出现的顺序执行 -
<clinit>()
不同于类的构造器。(关联:构造器是虚拟机视角下的<init>()
) -
若该类具有父类,JVM会保证子类的
<clinit>()
执行前,父类的<clinit>