3)JVM执行子系统

概述 : 了解虚拟机如何执行程序, 虚拟机怎样运行一个Class文件的概念模型, 可以更好的理解怎样写出优秀的代码
 
一 : 类文件结构
 
  • 无关性基石: 《Java虚拟机规范》对class文件的定制的规范, 成为了JVM语言无关性的基石, 即只要满足规范, 不限制语言种类, 编译器能够将语言转换为满足虚拟机规范的的字节码文件即可, 实现语言无关性的基础是虚拟机和字节码的存储格式


 
JVM语言无关性
 
class文件结构 : 
类型
名称
数量
含义
u4(代表4个字节的无符号数)
magic
1
魔数(文件的头4个字节, 固定0XCAFFBABE)
u2
minor_version
1
次版本号
u2
major_version
1
主版本号
u2
constant__pool_count
1
常量池容量计数值
cp_info
constant__pool
c onstant__pool_count-1
常量池(主要是两类: 字面量和符号引用)
u2
access_flags
1
访问标志
u2
this_class
1
类索引
u2
super_class
1
父类索引
u2
interface_count
1
接口数量
u2
interfaces
interface_count
接口集合
u2
fields_count
1
字段个数
field_info
fields
fields_count
字段表集合
u2
method_count
1
方法个数
method_info
methods
method_count
方法表集合
u2
attribute_count
1
属性个数
attribute_info
attributes
attributes_count
属性表集合
    1). 魔数与Class文件版本 : 魔数可简单理解为一个符合JVM规范的class文件约定的常量, 版本号为随着JDK更新的版本号标识
    2). 常量池 : 可理解为class文件的资源仓库, 与其他项目关联最多的类型, 占用空间较大, 主要包含两类字面量和符号引用
    3). 访问标识 : 用于识别一些类或者接口层次的访问信息
    4). 类索引, 父类索引和接口索引集合 : 类索引, 父类索引和接口索引集合与常量池关联
    5). 字段表集合 : 描述接口或类中声名的变量, 包括类级别和实例级别, 不包括方法中的变量
    6). 方法表集合 : 描述方法的一些摘要信息包括访问标识, 名称索引, 描述符索引, 属性表集合等, 方法具体内容存在属性表集合中的Code属性里
    7). 属性表集合 : 在class文件, 字段表, 方法表都可以携带自己的属性集合, 用于描述默写场景的专有信息
 
字节码指令简介 :  
    由于限制了JVM操作码的长度为一个字节(即0-255), 这意味着操作吗总数不能超过256, 又由于class文件格式放弃了编译后代码的操作数长度对齐, 意味着虚拟机在处理超过一个字节的数据时候, 不得不在运行时从字节中重建出具体的数据结构, 这种操作会在执行字节码时损失些性能, 但同时也可以省略很多填充和间隔符号, 也是为了尽可能的获得短小精干的编译代码, 追求尽可能小的数据量, 高效率传输
    1). 字节码与数据类型 : 对于大部分与数据类型相关的字节码指令, 它们的操作码助记符中都有特殊的字符来表名专门为哪种数据类型提供服务, 大多数对于boolean, buyte, short和char类型数据的操作, 实际上都是用相应的int类型作为运算类型, 这样可尽量节省操作码数量成本
    2). 加载和存储指令 : 大致可分为四类 : 将一个局部变量加载到操作数栈 eg: dload ; 将一个数值从操作数栈存储到局部变量表 eg: istore; 将一个常量加载到操作数栈 eg: dconst_<d>; 扩充局部变量表的访问索引的指令 : wide 
    3). 运算指令 : 算术指令包括如下类别 : 加法, 减法, 乘法, 除法, 求余, 取反, 位移, 按位或, 按位与, 按位异或, 局部变量自增指令, 比较指令
    4). 类型转换指令 : JVM直接支持宽化转换, 处理窄化类型转换时, 必须显示使用转换指令来完成, 尽管窄化发生上限溢出,下限溢出和精度丢失等情况, 但JVM规范中明确规定窄化转换过程中永远不会抛出运行时异常
    5). 对象创建与访问指令 : 分为: 创建类实例的指令, 创建数组的指令, 访问类字段和实例变量的指令, 把一个数组元素加载到操作数栈的指令, 讲一个操作数栈的值存储到数组元素中的指令, 取数组长度的指令, 检查类实例类型的指令
    6). 操作数栈管理指令 : 将操作数栈的栈顶元素出栈, 将栈顶的两个元素值互换, 复制栈顶的数值并将复制的值重新压入栈顶
    7). 控制转移指令 : 分为: 条件分支, 复合条件分支, 无条件分支
    8). 方法调用和返回指令 : invokevirtual, invekeinterface, invokespecial, invokesatic, invekedynamic, ireturn...
    9). 异常处理指令 : 现在是使用异常表完成的
    10). 同步指令 : 使用管程来支持的, 正确使用synchronized需要javac编译器和JVM两者协作支持, synchronized为内置锁会保证执行monitorexit指令释放锁
小结 : 
    对所有class文件格式的改进, 都集中在访问标识, 属性表这些设计上就可以扩展的数据结构中添加内容
 
二 : 虚拟机类加载机制
  •     JVM把描述类的数据从class文件加载到内存, 并对数据进行校验, 转换解析和初始化最终形成可被虚拟机使用的java类型, 这就是虚拟机的类加载机制, 在java里面, 类型的      加载, 连接和初始化过程都是在程序运行期间完成的, 这种策略虽然会令类加载时候增加一些性能开销, 但是会为java程序提供高度的l灵活性, java里天生可以动态扩展的语      言特性就是依赖运行期动态加载和动态连接实现的
 
类加载的时机 : 
    类从加载到虚拟机到被卸载出内存为止, 整个生命周期包括: 加载, 验证, 准备, 解析, 初始化, 使用, 卸载, 其中 加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的, 类在加载过程必须按照这种 顺序开始, 而解析阶段不一定: 它在某些情况下可以在初始化后再开始, 之所以强调按顺序开始, 而不是按顺序进行或完成, 是因为这些阶段通常是互相交叉的混合式进行的, 通常会在一个阶段执行的过程中调用另外一个阶段, 而解析阶段则例外, 可能在初始化之后再开始(支持动态绑定)
类加载的过程 : 
    1). 加载 : JVM规范没有明确规定时机, 各JVM实现自己决定加载时机
              类加载是整个类装载的第一个阶段, 此阶段需完成三件事情
              ①: 通过一个类的全限定名来获取此类的二进制字节流
            ②: 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
            ③: 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据访问入口
             * 数组类本身并不通过类加载器创建,而是通过Java虚拟机直接创建 
    2). 验证 : 
             ①: 文件格式验证
             ②: 元数据验证(语义分析)
             ③: 字节码验证
             ④: 符号引用验证
    3). 准备 : 为类变量分配内存并设置类变量初始值(零值)的阶段
    4). 解析 : 将符号引用转会为直接引用
    5). 初始化 : 有且只有几种情况下当虚拟机没有对类进行初始化时必须进行初始化动作, 
              ①: 遇到new, getstatic, putstatic, invokestatic这四个字节码指令时, 落到代码的场景通常是: new关键字对对象进行实例化时, 读取或者设置一个类的静态字段时, 以及调用一个类的静态方法时;
               ②: 当对类进行反射时
               ③: 当初始化一个类时, 如果发现其父类还没初始化, 先触发其父类的初始化
               ④: 虚拟机启动时, 用户需要制定一个执行的主类
             ⑤: 当Method实例最后解析结果REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄, 并且这个方法句柄所对应的类没有进行初始化
 
类加载器 :  
    1). 类与类加载器 : 比较两个类是否相等, 只有这两个类是由同一个类加载器加载的前提下才有意义, 即当两个类来源一致且由相同的类加载器加载才'相等'
    2). 双亲委派模型 : 
             ①: 启动类加载器 : BootstrapClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
             ②: 扩展类加载器 : ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
             ③: 应用类加载器 : ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    
双亲委派机制 : 
    1).当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成
    2).当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成
    3).如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;
    4).若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException


 
   双亲委派模型
 
 
 
三 : 虚拟机字节码执行引擎
 
  • 运行时栈桢结构 : 
    1). 局部变量表 : 用于存放方法参数和方法内部定义的局部变量.在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量
    2). 操作数栈 : 是一个后入先出(Last In First Out,LIFO)栈.同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中.操作数栈的每一个元素可以是任意的Java数据类型,包括long和double.32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2.在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值.
    3). 动态链接 : 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking).我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数.这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析.另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接.(静态分派,动态分派)
    4). 方法返回地址 : 方法退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等
    5). 附加信息 : 虚拟机自定义实现


 
运行时栈桢结构
 
方法调用 : 
    1). 解析 : 解析调用时一个静态的过程, 在编译期间就完全确定, 在类装载的解析阶段就会把涉及的符号引用全部转换为直接引用(invokestatic, invokespecial都可在解析阶段确定调用版本)
    2). 分派 : 分为静态分派和动态分派, 以及单分派和多分派(依据宗量数)invokevirtual指令大部分场景
                     所有依赖静态类型来定位方法执行版本的分派动作称为静态分派.其典型应用是方法重载(根据参数的静态类型来定位目标方法).
                     静态分派发生在编译阶段.因此确定静态分派的动作实际上不是由虚拟机执行的.
                  在运行期根据实际类型确定方法执行版本
    3). 动态类型语言支持 : invokedynamic指令
 
基于栈的字节码解释执行引擎 : 
    1). 解释执行 :


 
解释执行
    2). 基于栈的指令集和基于寄存器的指令集 : 
         java编译器输出的指令流, 基本上都是基于栈的指令集(ISA), 不同于基于寄存器的指令集, 基于栈的指令集的一个优点是可移植, 因为寄存器由硬件提供,程序依赖于寄存器不能避免会受到硬件的约束, 主要缺点是相对来说慢一些, 因为要进行很多入栈出栈等操作
    3). 基于栈的解释器的执行过程 :    
         主要是由栈桢中的程序计数器, 局部变量表, 操作数栈协同工作, 实际的虚拟机实现会做出一些优化来提升性能, 实际的运作过程可能不是很符合模型概念的描述, 主要的原因是虚拟机中的解释器和即时编译器都会对输入的字节码进行优化
 
四 : 类加载及执行子系统的案例
 
  • class文件和执行引擎部分中, 能通过程序进行操作的, 主要是字节码生成与类加载器这两部分功能
 
Tomcat: 正统的类加载器架构 : 


 
OSGI: 灵活的类加载器架构 : 


 
字节码生成技术与动态代理的实现 :  
         ASM生成字节码以及CGlib和JDK 动态代理的实现
 
  • last : 真实的虚拟机运作过程由于会做出各种优化, 和概念模型可能会有很大的差异, 但从最终执行结果来看应该是一致的
 
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值