JMV-虚拟机执行器
- 总纲
Class结构
- Class文件是一组以八个字节为基础单位的二进制流
- Class文件有两种基础数据类型
- 无符号数:基本的数据类型,由u1,u2,u4,u8代表1,2,4,8个字节的无符号数;可用于描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型
- Class具体结果如下:
- Class文件各数据项的具体含义
类型 | 名称 | 数量 | 含义 |
u4 | magic | 1 | (魔数)判断Class文件是否被JVM接受? |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量池容量计数器 |
cp_info | constant_pool | constant_pool_count-1 | 常量池 |
u2 | access_flags | 1 | 识别类或者接口层次的访问信息 |
u2 | this_class | 1 | 类索引 |
u2 | super_class | 1 | 父类索引 |
u2 | interfaces_count | 1 | 接口索引数目 |
u2 | interfaces | interfaces_count | 该类实现的接口索引 |
u2 | fields_count | 1 | 字段数目 |
field info | fields | fields_count | 接口或者类中申明的变量 |
u2 | methods_count | 1 | 方法数目 |
method_info | methods | methods_count | 该类中的方法 |
u2 | attributes_count | 1 | 属性表数目 |
attribute info | attributes | attributes count | 属性表集合 |
- 常量池
- 字面量:字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
- 符号引用:符号引用则属于编译原理方面的概念
- JVM类加载机制
- 什么是JVM的类加载机制?
- 把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
- JVM加载流程;如图:
- 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的
- 解析阶段的顺序不一定;这是为了支持Java语言运行时绑定的特性
- 何时进行初始化?:
- 遇到new、getStatic、putStatic或invokeStatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段.
- 反射调用时,若该对象未初始化
- 初始化类时,父类未初始化
- JVM启动时,优先初始化指定的类
- 接口中定义了default方法,若接口的实现类发生初始化,则接口优先初始化
- 加载(Loading)阶段:
- 通过类的全限定类名获取字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成该类的java.lang.Class对象,作为方法区此类的各种数据的访问入口
- 验证阶段(Verification):
- 验证Class文件是否符合JVM规范;是否对JVM产生不利影响
- 包含四个阶段:
- 文件格式验证:验证魔数、版本号、常量池的常量类型等......
- 元数据验证:验证类的父类存在与否、是否重写final类、是否实现父类的抽象方法等......
- 字节码验证:分析数据流以及控制流,检查逻辑、语义是否合法
- 符号引用验证:符号引用转化为直接引用的时候,发生在解析阶段
- 准备阶段(Preparation):
- 为类变量(static修饰的变量)初始化,分配内存并且赋予初始值(并非硬编码设置的值,而是系统的值)的阶段
- 类变量理论上应该存在方法区,但是实际上在JDK8中却存在于堆中
- 解析阶段(Resolution):
- 将常量池中类变量的符号引用转变成为直接引用
- 符号引用(与内存无关):以一组符号来描述所引用的目标,符号可以是任何 形式的字面量,只要使用时能无歧义地定位到目标即可
- 直接引用(与内存有关):可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄
- 初始化阶段(Initialization):
- 依据编码的主观计划去初始化变量以及资源
- 初始化阶段是执行<clinit>方法的过程
- <clinit>是由类中的static代码块、变量组成的
- <clinit>、<init>方法的区别
- <init>对于非static变量赋值,<clinit>对static对象赋值
- <init>对象在实例化对象时发生;<clinit>在JVM类加载的初始化阶段发生
- 类加载器
- 双亲委派模型
- 什么是双亲委派模型?
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,只有当父加载器反馈认为无法加载时,才会自己加载
- 双亲委派模型如图所示:
- 双亲委派各级加载器之间不是继承关系,而是包含关系
- Java语言的双亲委派实现
- 首先确定是否存在目标类
- 存在则直接返回
- 否则确定父级Class加载器是否存在(递归过程),如果存在父级加载器则使用父级加载器实现
- 如果不存在父类加载器,则证明自身为根加载器,则使用跟加载器加载Class
- 如果以上加载过程都不能加载Class,则使用自身的ClassLoader加载Class
- 双亲委派适用于用户需要依赖于核心文件;不适用于核心文件依赖用户资源(如下:)
- 双亲委派的缺陷:父加载器无法访问子加载器(如:DriverManager需要根据用户的需求加载不同的Class)
- 为什么需要双亲委派模型?
- 确保每一个Class文件对于每一个Class加载器都是唯一的;保证Class文件的唯一性;
- 防止自定义Class文件污染JVM自带的Class;保证JVM的安全性
- 各级类加载器的区别
- 启动类加载器(Bootstrap Class Loader):负责加载存放在 <JAVA_HOME>\lib目录;由C++实现,属于JVM本身的一部分
- 扩展类加载器(Extension Class Loader):负责加载<JAVA_HOME>\lib\ext目录
- 应用程序类加载器(Application Class Loader):它负责加载用户类路径 (ClassPath)上所有的类库
- 如何破除双亲委派?
- 由Java源码可知,如果父类加载器加载失败,会触发findClass(name)方法,因此重写findClass(name)即可
- SPI(Service Provider Interface)机制的引入,由于系统自带的ClassLoader不认识SPI中的全路径类名;因此需要其余的类加载器去加载SPI的类,引入了线程上下文类加载器 (Thread Context ClassLoader)