学习自:哔哩哔哩尚硅谷
JVM基础中篇-字节码与类的加载
1、Class文件结构
1.1、概述
Java语言:跨平台的语言
字节码文件的跨平台性
Java虚拟机:跨语言的平台
想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码
- 前端编译器的主要任务就是负责将符合Java语言规范的Java代码转换为符合JVM规范的字节码文件
- javac是一种能将Java源码编译为字节码的前端编译器
- javac编译器在将java源码编译为一个有效字节码文件过程中经历了4个步骤,分别是:词法解析、语法解析、语义解析以及生成字节码
1.2、Class文件
字节码文件里是什么?
- 源代码经过编译器编译之后会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM指令
什么是字节码指令(byte code)?
- 操作码(+ 操作数)
- Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码
1.3、Class文件结构
Class类的本质:
- Class文件是一组以8字节为基础单位的二进制流
- Class文件并不一定是以磁盘文件的形式存在
Class文件格式:
- 没有任何的分隔符号。其中的数据项,无论是字节熟顺序还是数量,都是被严格限定的,长度多少、先后顺序都是不允许被改变的
- Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表
无符号数属于基本数据类型
表是由多个无符号数或者其他表作为数据项构成的复合数据类型
Class文件的总体结构:
- magic(魔数)
- Class文件版本
- 常量池
- 访问标志
- 类索引、父类索引,接口索引集合
- 字段表集合
- 方发表集合
- 属性表集合
1、Magic Number(魔数)
- 每个Class文件开头的4个字节的无符号整数称为魔数
- 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符
- 魔数值固定为0xCAFEBABE,不会改变
- 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候会直接抛出以下错误:
- 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为扩展名可以随意地改动
2、Class文件的版本号
- 紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号mino_version,而第7个和第8个字节就是编译主版本号major_version
- 它们共同构成了class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件格式版本号就确定为M.m
- 版本号与Java编译器的对应关系如下:
- Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布朱版本号向上加1
- 不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件,否则会JVM抛出UnsupportedClassVersionError异常
- 在实际应用中,由于开发环境和生产环境不同,可能会导致该问题的发生。因此,需要特别注意版本的一致性
3、常量池:存放所有常量
- 常量池是Class文件内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用
- 随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石
- 在版本号后,紧跟着的是常量池的数量,以及若干个常量池表项
- 常量池中常量的数量是不固定的,所有在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1开始而不是0开始的
由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合 - 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载进入方法区的运行时常量池中存放
3.1、常量池计数器(constant_pool_count)
3.2、常量池表(constant_pool)
常量类型和结构:
总结:
4、访问标识
- 在常量池后,紧跟着访问标记,该标记使用两个字节标识,用于识别一些类或者接口层次的访问信息,包括:这个是类还是接口;是否定义为抽象类型;如果是类的话,是否被声明为final等。各种访问标记如下图所示:
- 类的访问权限通常为 ACC_ 开头的常量
- 每一种类型的表示都是通过访问标记的32位中的特定为来实现的。比如,若是public final 类,则该类标记为ACC_PUBLIC | ACC_FINAL
- 使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记
5、类索引,父类索引,接口索引集合
- 在访问接口后,会指定该类的类别、父类类别以及实现的接口,格式如下:
- 这三项数据来确定这个类的继承关系
1、类索引用于确定这个类的全限定名
2、父类索引用于确定这个类的父类全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有java类的父类索引都不为0
3、接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在这个接口索引集合中
6、字段表集合
-
字段计数器:
-
字段表:
7、方法表集合
- 方法计数器
- 方法表
方法计数器:
方法表:
8、属性表集合
- 属性计数器
- 属性表
属性表
code属性:
1.4、使用javap指令解析Class文件
1、解析字节码的作用:
2、javac -g操作:
3、javap的用法
4、javap指令小结:
2、字节码指令集与解析
2.1、概述
执行模型:
字节码与数据类型:
指令分类:
2.2、加载与存储指令
回顾虚拟机栈:
回顾局部变量表:
- 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
- 在方法执行时,虚拟机使用局部变量表完成方法的传递
1、局部变量压栈指令:
2、常量入栈指令:
3、出栈装入局部变量表指令:
2.3、算术指令
计算运算指令:
比较运算指令:
2.4、类型转换指令
宽化类型转换:
窄化类型转换:
2.5、对象的创建与访问指令
1、创建指令:
2、字段访问指令:
3、数组操作指令:
4、类型检查指令:
2.6、方法调用与返回指令
1、方法调用指令
2、方法返回指令:
2.7、操作数栈管理指令
2.8、控制转移指令
比较指令
1、条件跳转指令:
2、比较条件跳转指令
3、多条件分支跳转
4、无条件跳转指令
2.9、异常处理指令
1、抛出异常指令:
2、异常处理和异常表:
3、同步控制指令
java虚拟机支持两种同步结构:方法级的同步 和 方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的
1、方法级的同步:
2、方法内指定指令序列的同步:
3、类的加载过程详解
3.1、概述
从java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载
按照java虚拟机规范,从class文件到加载内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
- 其中,验证、准备、解析3个部分称为链接(Linking)
从程序中类的使用过程看:
一些相关的面试题:
- 描述一下JVM加载Class文件的原理机制
- 类加载过程
- 类加载的时机
- java类加载过程
- JVM中类加载机制,类加载过程?
3.2、过程1:Loading(加载)阶段
1、加载完成的操作:
2、二进制流的获取方式:
3、类模型与Class实例的位置:
外部可以通过访问代表Order类的Class对象来获取Order的类数据结构
4、数组类的加载:
3.3、过程2:Linking(链接)阶段
1、验证阶段:
当类加载到系统后,就开始链接操作,验证是链接操作的第一步
它的目的是保证加载的字节码是合法、合理并符合规范的
2、准备阶段:
- 默认赋值
注意:如果使用字面量的方式定义一个字符串常量的话,也是在解析环节直接进行显式赋值。
3、解析阶段:
在准备阶段完成后,就进入了解析阶段
解析阶段,简言之,将类、接口、字段和方法的符号引用转为直接引用
3.4、过程3:Initialization(初始化)阶段
1、static 与 final 的搭配问题:
初始化阶段,简言之,为类的静态变量赋予正确的初始值
在链接阶段的准备环节赋值的情况:
- 对于基本数据类型的字段来说,如果使用
static final
修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行 - 对于
String
来说,如果使用字面量的方式赋值,使用 static final 修饰的话,则显式赋值通常是在链接阶段的准备环节进行
在初始化阶段<clinit>()
中赋值情况:
- 上述两种情况之外
最终结论:
- 使用
static final
修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String
类型的显示赋值,是在链接阶段的准备环节进行
2、<clinit>()
的线程安全性:
3、类的初始化情况:主动使用vs被动使用:
3.5、过程4:类的Using(使用)
3.6、过程5:类的Unloading(卸载)
4、类的加载器
4.1、概述
类加载器是JVM执行类加载机制的前提
1、面试题:
什么是双亲委派机制?优点是什么?讲一下双亲委派机制
2、类加载的分类:
3、类加载器的必要性:
4、类加载器的命名空间:
5、类加载机制的基本特征:
4.2、类的加载器分类
JVM支持两种类型的类加载器:
- 引导类加载器(启动类加载器)
- 自定义类加载器:一般是指程序中开发人员自定义的一类类加载器,但是Java虚拟机中是将所有派生于抽象类ClassLoader的类的加载器都划分为自定义类自定义类加载器
- 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器
- 不同类加载器看似是继承关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用
引导类加载器(启动类加载器):
扩展类加载器:
应用程序类加载器(系统类加载器):
用户自定义类加载器:
4.3、测试不同类的加载器
每个Class对象都包含一个定义它的ClassLoader的一个引用。
获取ClassLoader的途径:
- 说明
4.4、ClassLoader源码解析
Class.forName()与ClassLoader.loadClass()
:
4.5、双亲委派模型
1、定义与本质:
2、双亲委派机制的优势与劣势:
破坏双亲委派机制1:
破坏双亲委派机制2:线程上下文加载器
破坏双亲委派机制3:
热替换的实现:
4.6、沙箱安全机制
沙箱安全机制
- 保证程序安全
- 保护Java原生的JDK代码
4.7、自定义类的加载器
实现方式:
4.8、Java9新特性
左图:JDK 9之前;右图:JDK9