Java虚拟机只与Class文件相关联,它规定了Class文件应该具有的格式,而不论该文件是由什么语言编写并编译而来。所以,任何语言只要能够最终编译成符合Java虚拟机要求的Class文件,就可以运行在Java虚拟机上面。就是说,不论是使用Java, Scala, Kotlin, Groovy还是其他语言,只要编译出的Class文件符合虚拟机规范,那么都可以被虚拟机执行。所以,实际上Java规范就是由Java语言规范和Java虚拟机规范两个独立的部分组成。

Class类文件是一种二进制文件,它包含了Java虚拟机指令集和符号表以及若干其他辅助信息。Class文件格式采用类似于C的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。无符号数属于基本数据类型,以 u1 , u2 , u4 , u8 分别代表1字节、2字节、4字节和8字节的无符号数,无符号数可以用来描述数字、索引、数量值或者按照utf-8编码构成的字符串。

表是由多个无符号数或者其他作为表作为数据项构成的复合数据结构,所有表习惯性地以"_info"结尾,整个Class文件本质上是一张表。而所谓的表就对应于C++中的一个结构体,比如整个Class文件对应的结构体就是:

struct ClassFile {
u4 magic; // 识别Class文件格式,具体值为0xCAFEBABE,
u2 minor_version; // Class文件格式副版本号,
u2 major_version; // Class文件格式主版本号,
u2 constant_pool_count; // 常量表项个数,
cp_info **constant_pool; // 常量表,又称变长符号表,
u2 access_flags; // Class的声明中使用的修饰符掩码,
u2 this_class; // 常数表索引,索引内保存类名或接口名,
u2 super_class; // 常数表索引,索引内保存父类名,
u2 interfaces_count; // 超接口个数,
u2 *interfaces; // 常数表索引,各超接口名称,
u2 fields_count; // 类的域个数,
field_info **fields; // 域数据,包括属性名称索引,
u2 methods_count; // 方法个数,
method_info **methods; // 方法数据,包括方法名称索引,方法修饰符掩码等,
u2 attributes_count; // 类附加属性个数,
attribute_info **attributes; // 类附加属性数据,包括源文件名等。
};
复制代码

上面结构体的各个变量的定义的顺序与Class文件中的存储顺序是一致的。下面我们用一个二进制编辑器打开Class文件并简单看下,Class文件中的存储如何与上面的结构体对应的。

根据结构体的定义,首先是 magic 字段,它是u4类型的,即4字节,对应于上图中的 CAFEBABE ;紧接着两个u2类型的字段, minor_version 和 major_version ,用来表示Class文件的版本号,对应于上图中的 00000031 ;然后是一个u2类型的 constant_pool_count ,用来表示常数表项个数,这里是 0027 ,也就是有38个常量,因此常量从1开始计数的;接着常量个数的是变长符号表,这个符号表的长度就是之前常量的长度,即38,然后我们要按照常量的规则找到38个常量之后就是u2类型的访问标志。

那这38个常量又如何寻找呢?实际上,Class文件中总计规定了14种常量结构体,每种结构体都包含一个 tag 字段,它是u1类型的,即1个字节,并且存储在该结构体的首位。我们需要先根据该 tag 字段找到该常量的定义,然后才能确定接下来的几个字节是属于该常量的。比如上图中的第一个常量的 tag 是 0A ,我们可以查询相关的表知道它是 CONSTANT_Fleddef_info 类型的常量,该常量有3个字段:u1类型的 tag, u2类型的 index , u2类型的 index ,故我们可以确定常量长度之后的5个字节是属于第一个常量的。依次,分析下去我们可以得到第二个,第三个等常量的信息。得到了常量的信息之后,就可以获取上面结构体中其他的字段的信息。

其实,按照上面的分析,我们可以看出分析Class文件时一个非常机械的过程,因为它有固定的规则在里面,所以我们可以使用命令行工具 javap 来获取以上的信息。在实际开发过程中从字节码的角度分析问题的情形可能并不多,但是了解字节码中的一些指令,尤其是并发相关的指令,对学习和分析问题都大有裨益。

2、类加载机制

2.1 类加载过程

Java程序中的类加载是在运行期间完成的,我们可以使用Java预定义的加载器或者自定义加载器动态从各种渠道加载类并使用。最初将类加载器从虚拟机中分离出来是为了Applet等的动态加载,但是后来随着技术发展,动态加载被应用到各种场景中,比如移动端的插件化、热补丁、Tomcat的类加载等各种场景中,所以类加载是非常重要的一块内容。

一个类从被加载到虚拟机内存到卸载的整个生命周期包括: 加载-验证-准备-解析-初始化-使用-卸载 7个阶段。其中 验证-准备-解析 3个阶段称为连接。

加载发生在类被使用的时候,如果一个类之前没有被加载,那么就会执行加载逻辑,比如当使用new创建类、调用静态类对象和使用反射的时候等。加载过程主要工作包括:1).从磁盘或者网络中获取类的二进制字节流;2).将该字节流的静态存储结构转换为方法取的运行时数据结构;3).在内存中生成表示这个类的Class对象,作为方法区访问该类的各种数据结构的入口。

验证阶段会对加载的字节流中的信息进行各种校验以确保它符合JVM的要求。

准备阶段会正式为 类变量 分配内存并设置类变量的初始值。注意这里分配内存的只包括类变量,也就是静态的变量(实例变量会在对象实例化的时候分配在堆上),并且这里的设置初始值是指‘零值’,比如int类型的会被初始化为0,引用类型的会被初始化为null,即使你在代码中为其赋了值。

解析阶段是将常量池中的符号引用替换为直接引用的过程。符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,只要能正确定位到它们在内存中的位置就行。直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化是执行类构造器 方法的过程。 方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证 方法执行之前,父类的 方法已经执行完毕。

2.2 类加载机制 双亲委派模型

类加载器用来根据类的全限定名获取描述此类的二进制字节流。我们可以把类加载器分成以下4种:

  1. 启动类加载器:存在于虚拟机中,使用C++编写,负责将 /lib 下面的类库加载到内存中(比如rt.jar);
  2. 拓展类加载器:它负责将 /lib/ext 或者由系统变量 java.ext.dir指定位置中的类库加载到内存中;
  3. 应用程序类加载器:它负责将用户类路径中指定的类库加载到内存中;
  4. 自定义类加载器:就是指用户自己定义的类加载器。

在Java种存在以上多种类加载器,它们之间通过一定的规则相互配合,这个规则就是 双亲委派模型 :每个类加载器收到类加载请求时,都会先将请求委派给父类加载器去完成,所以,加载请求会一直传递到最顶层的类加载器。只有当类父类加载器无法完成加载请求的时候,该加载器才会自己去加载。当然,这不是强制的,我们也可以完全使用自己的一套逻辑。但双清委派模型的好处就在于,假如你自定义了一个类 java.lang.Object,那么当使用双亲委派模型来加载的时候,会由子加载器不断向上传递加载请求到启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。这保障了Java系统的稳定性。

3、虚拟机字节码执行引擎

虚拟机是相对于物理机而言的,它们的区别是物理机的执行引擎直接建立在处理器、硬件、指令集和操作系统层面上,而虚拟机的执行引擎是自己实现的。所以,执行引擎也是Java虚拟机最核心的组成部分之一。

执行引擎用来执行我们写的业务逻辑,而业务逻辑就是指一些方法,所以虚拟机执行引擎就是用来执行各个方法的。而方法是通过栈帧来描述的,方法的执行是用栈帧入栈和出栈来描述的,栈帧中存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。所以说,执行引擎就是用来执行各个栈帧的。在虚拟机执行的时候,只有最顶层的栈帧是有效的,与之关联的方法就称为当前方法,并且执行引擎运行的所有字节码都是针对当前栈帧的。

方法的信息存储在栈帧中,而栈帧中的方法信息是从Class文件中读取来的。回到之前的Class类文件结构部分,每个方法是通过结构体 method_info 来描述的:

struct method_info
{
u2 access_flags; //方法修饰符掩码
u2 name_index; //方法名在常数表内的索引
u2 descriptor_index; //方法描述符,其值是常数表内的索引
u2 attributes_count; //方法的属性个数
attribute_info **attributes; //方法的属性数据,
};
复制代码

在 method_info 中又包含了一个属性表集合 attribute_info 类型的 attributes ,方法的局部变量表需要的空间大小和操作栈的深度等就记录在其中。局部变量表用于存放方法参数和方法内的局部变量,当方法是实例方法的时候,局部变量表的第0位会被用来传递方法所属对象的引用,即 this 。Java虚拟机执行引擎是基于栈的执行引擎,这里的栈就是操作数栈。操作数栈的深度也是记录在方法属性集合的Code属性中的。

执行引擎的执行过程

我们可以用下面的一个程序来说明以下在实际的执行过程中,Java执行引擎是如何工作的。

public int cal() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c; // 1
}
复制代码

首先会在编译期确定方法的栈深度和局部变量表的长度,栈深度是由计算的过程得到的,而局部变量表的长度等于1个 this 加3个局部变量即4。当程序执行到1处时,局部变量表内会被填充为 this 、 100 、 200 和 300。程序计数器会随着代码当前执行到的位置而不断更新。而此时因为没有进行任何计算,所以栈还是空的。

接下来就开始进行计算:首先会把a压入栈中(其实时把局部变量表里的值压入栈中);然后把b压入栈中;接着将栈顶的两个元素先出栈,相加之后再入栈,此时栈中只有一个计算结果300;接下来再把c压入栈中;然后,把栈顶的两个元素出栈,执行完乘法之后再入栈,所以最终栈中只有一个90000;最后,使用 ireturn 指令结束方法并将栈顶的元素返回给方法的调用者。

总结

以上就是虚拟机执行子系统的一个过程,包含了从编译出的Class文件,到被加载到内存中、验证、初始化等,到最终在虚拟机中被执行等完整的过程。这里只是总结和梳理了相关的基础的知识点,在虚拟机中实际的执行过程肯定远比我们上述的内容更加复杂和精彩。

jvm虚拟机---执行引擎子系统的更多相关文章

  1. 执行引擎子系统——JVM之五

    一.JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用的是自己的一套指令系统,每个线程在创建后,都会产生一个程序计数器(pc)和栈(Stack). pc:存放了下一条将要执行的指令: Sta ...

  2. 重读《深入理解Java虚拟机》五、虚拟机如何执行字节码?程序方法如何被执行?虚拟机执行引擎的工作机制

    Class文件二进制字符流通过类加载器和虚拟机加载到内存(方法区)完成在内存上的布局和初始化后,虚拟机字节码执行引擎就可以执行相关代码实现程序所定义的功能.虚拟机执行引擎执行的对象是方法(均特指非本地 ...

  3. Java虚拟机执行引擎

    执行引擎 关于执行引擎相关的部分, 在之前的博文里 Java内存区域中已经有所提及. 回顾一下: 也只有几个概念, JVM方法调用和执行的基础数据结构是 栈帧, 是内存区域中 虚拟机栈中的栈元素, 每 ...

  4. JVM虚拟机-类加载器子系统

    转自博客:http://www.cnblogs.com/muffe/p/3541189.html   还有一些自己补充的知识点 一.类加载器基本概念 顾名思义,类加载器(class loader)用来 ...

  5. JVM(八)执行引擎相关内容

    一:两种解释器 JAVA字节码解释器: java字节码===>c++代码==>硬编码. 首先.java文件编译成字节码,遍历每行的字节码指令,因为每个字节码指令的含义都是固定的所以可以根据 ...

  6. jvm虚拟机--垃圾回收子系统

    转载自cyc2018的github:https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E8%99%9A%E ...

  7. 深入理解java虚拟机(5)---字节码执行引擎

    字节码是什么东西? 以下是百度的解释: 字节码(Byte-code)是一种包含执行程序.由一序列 op 代码/数据对组成的二进制文件.字节码是一种中间码,它比机器码更抽象. 它经常被看作是包含一个执行 ...

  8. JVM总结(五):JVM字节码执行引擎

    JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...

  9. 【深入理解JAVA虚拟机】第三部分.虚拟机执行子系统.3.函数调用与执行

    这章原名叫“虚拟机字节码执行引擎”,实际就是讲的函数如何调用和执行的. 1.概述 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器. 硬 ...

随机推荐

  1. HISAT,sTRINGTIE,ballgown三款RNA-seq信息分析软件

    HISAT,sTRINGTIE,ballgown三款RNA-seq信息分析软件 2015年04月02日 11:35:47 夜丘 阅读数:8940 标签: 生物 更多 个人分类: 论文笔记   Bowt ...

  2. 洛谷4782 【模板】2-SAT 问题

    原题链接 \(2-SAT\)模板 #include<cstdio> using namespace std; const int N = 2e6 + 10; int fi[N], di[N ...

  3. 在nodejs里面是用类似配置文件的方法

    1.a.js exports.MYSQLIP = '127.0.0.1'; exports.MYSQLPORT = 1336; 2.b.js const C = require('./config/c ...

  4. 用VS2010打开VS2012项目

    1.修改解决方案文件,即.sln文件: 用记事本打开.sln文件,把其中的 Microsoft Visual Studio Solution File, Format Version 12.00 # ...

  5. MySQL学习笔记-锁相关话题

    在事务相关话题中,已经提到事务隔离性依靠锁机制实现的.在本篇中围绕着InnoDB与MyISAM锁机制的不同展开,进而描述锁的实现方式,多种锁的概念,以及死锁产生的原因.   Mysql常用存储引擎的锁 ...

  6. Python之路(第六篇)Python全局变量与局部变量、函数多层嵌套、函数递归

    一.局部变量与全局变量 1.在子程序中定义的变量称为局部变量,在程序的一开始定义的变量称为全局变量.全局变量作用域是整个程序,局部变量作用域是定义该变量的子程序. 全局变量没有任何缩进,在任何位置都可 ...

  7. vue input,textarea失去焦点调用函数方法

    <input type="number" class="num" value="1" @blur.prevent="chan ...

  8. 自学java坎坷之路——20155312张竞予

    20155312 2006-2007-2 <Java程序设计>第一周学习总结 教材学习内容总结 第一周并没有在课堂上对教材内容进行学习,学习内容概括如下 课程分数构成,其中包括课堂测验(每 ...

  9. Django框架之验证码生成示例

    一.生成随机颜色的图片 1)前端代码展示 <!DOCTYPE html> <html lang="en"> <head> <meta ch ...

  10. lambda表达式和表达式树(深入理解c#)

    1.Lambda形式 1). Lambda表达式最冗长的形式: (显式类型的参数列表)=>{语句} 2). 大多数时候,都可以用一个表达式来表示主体,该表达式的值是Lambda的结果,在这些情况 ...