JVM能够跨越计算机体系结构来执行字节码,主要是因为JVM屏蔽了与各个机器平台相关的软件或者硬件之间的差异。使得与平台相关的耦合由JVM提供者来实现。
本文将介绍
1 jvm的总体设计结构
2 jvm的执行引擎如何工作
3 执行引擎如何模拟执行jvm指令
JVM体系结构
何谓JVM
JVM是Java虚拟机,它通过模拟一个计算机来达到一个计算机所具有的的计算功能。而真实的计算机具有哪些功能呢。
1 指令集,这是计算机能识别的机器语言的命令集合
2 计算单元
3 寻址方式
4 寄存器定义,包括操作数寄存器,变址寄存器等
5 存储单元,比如内存和磁盘
什么是指令集
指令集就是CPU中用来控制计算机的一套指令的集合,每个CPU在设计时规定了一系列与其他硬件电路配合的指令系统,通过指令集可以对应地操作硬件电路,简单的指令集可以理解为译码电路,每个指令集单元对应一个电路操作,比如打开,关闭,显示特定数字等功能。
计算机指令集可分为精简指令集和复杂指令集,桌面操作系统一般使用复杂指令集。
汇编,CPU架构与指令集
指令集和汇编语言一般是一对一的关系,指令集是机器码,010101表示,而汇编语言是人能够看懂的指令。
但是也不是所有汇编代码都能对应一条机器指令。
指令集和CPU的架构关系密切。汇编语言可以操作寄存器,而寄存器的设计是芯片架构的一部分,所以现在的CPU厂商都会使用兼容的指令集,让自己的CPU也能够识别别人的指令集。
同时,更重要的是,CPU厂商必须向操作系统提供商妥协,因为操作系统是计算机的应用入口,必须让操作系统支持自己的指令集,才有可能完成系统调用,正常地使用计算机。
JVM和实体机的区别
1 JVM只是一个抽象规范,约束了JVM到底是什么,有哪些组成部分。
2 JVM的具体实现是不同厂商根据规范,并且结合软硬件设计出来的具体产品。
3 JVM是一个运行中的实例,一个Java程序就代表了一个JVM实例。
JVM与实体机一样必须有一套合适的指令集,这个指令集我们称为jvm字节码指令集,符合class规范的字节码都可以被jvm执行,也就是和汇编对应机器码类似的关系,class代码也可以很好地对应jvm指令集。
JVM体系结构详解
JVM主要由四部分组成
类加载器
每个被jvm加载的类型都有一个对应的Class类实例来表示该类型,该实例可以唯一表示被jvm装载的class,也放在堆中。
执行引擎
执行引擎是JVM的核心部分,作用是解析JVM字节码指令,得到执行结果。
在jvm虚拟机规范中详细地定义了执行引擎遇到字节码时应该处理什么,得到什么样的结果,但是没有规定如何完成这一流程。
于是执行引擎的执行方式由JVM的实现厂家自己去实现。
1 直接解释执行
2 采用JIT技术转成本地代码(机器码)执行
3 采用寄存器这个芯片模式去执行
比如Sun的hotspot基于栈的执行引擎。google的dalvik基于寄存器的执行引擎
java内存管理
执行引擎在执行程序时需要存储一些东西,比如操作数,执行结构,class类的字节码还有类的对象信息都要在执行引擎执行之前保存好。
一个jvm实例会有一个方法区,Java堆,Java栈,pc寄存器和本地方法区。
方法区和java堆是线程共享的。
4 本地方法调用
jvm工作机制
机器如何执行代码
先来看看普通的实体机上程序如何执行。
计算机只接受机器指令,其他高级语言必须先经过编译期编译成机器指令才能被计算机正确执行。
高级语言一般是屏蔽所有底层的硬件平台甚至是软件平台的。高级语言之所以能屏蔽这些架构差异,是因为中间有一个转换的环节,这个环节就是编译。
与硬件耦合的麻烦就交给了编译器,所有不同的硬件平台使用的编译期通常也不相同。
在当前这种环境下硬件平台其实已经被上一层软件平台锁代替了,也就是操作系统。现在的操作系统几乎完全屏蔽了硬件,所以编译器和操作系统的关系很机密。
程序的编写到执行
通常会有几个步骤。
1 源代码
2 预处理
3 编译器编译
4 汇编程序
5 目标代码
6 链接器(连接多个文件的关联代码)
7 可执行程序
比如linux安装一个软件需要几个步骤
1 configure选择合适的编译器
2 make进行编译,得到可执行的目标文件
3 make install 将可执行文件安装到安装目录
4 make clean 删除编译时的临时文件
实际上在某些操作系统上需要进行动态链接,在windows是dll,linux是shared library,可以动态地把公共类库加入目标文件
指令集结构
回到让机器执行代码的主题,不管是何种指令集都只有几种基本的运算元素:加减乘,求余,求模等。
这些运算又可以进一步拆分成二进制运算,与或非,异或等。
指令集的核心目的就是确定运算的种类,预算需要的数据,以及去哪里取数,结果放在什么地方。
不同的操作方式可以划分为一地址指令,二地址指令等等。响应的指令集会有对应的架构实现,如基于寄存器和基于栈的架构实现。
jvm为何选择基于栈的架构
jvm执行字节码使用基于栈的架构,所有操作数需要入栈。
在jvm中使用一个栈帧来保存本地变量,操作数栈。
使用栈操作的效率明显不如使用寄存器,为什么jvm要用基于栈的架构:
1 jvm要设计成平台无关的,要保证很少或没有寄存器的机器也能够正确执行java代码,很多机器的寄存器没有规律。但是Google对于Android系统使用dalvik vm是基于寄存器架构的,因为其注重性能而非跨平台性。
2 为了指令的紧凑性。
执行引擎的架构设计
每当创建一个新的线程,jvm为其创建一个java栈,为这个线程分配一个pc寄存器,指向线程的第一行可执行代码。
每当调用一个新方法时会在这个栈上创建一个新的栈帧数据结构。
执行引擎的执行过程
字节码会对应到jvm指令集的出栈入栈以及方法调用等操作。
一般把运算结果存在当前栈的栈顶中。
当执行到return指令时,方法实行结束,这个方法对应的部件都会被jvm回收,局部变量的所有值被释放,pc寄存器被销毁,对应栈帧消失。
jvm方法调用栈
Java方法分为本地方法和java方法,我们讲的是java方法。
假设jvm执行main方法,main方法中有一个方法调用。
对应的class字节码有一条对应的jvm指令invokestatic,执行invokestatic指令时jvm会为新方法创建一个新的栈帧。
假设方法调用中带了两个参数,则这时新栈帧的局部变量表里已经有这两个参数了,pc寄存器此时则保存着当前栈帧的下一条地址指令。
新方法运行完后,把结果值放在栈顶,最后一条指令时ireturn,这条指令将当前栈帧中的栈顶元素返回到调用这个方法的栈中。此时新的栈帧销毁,计数器指向原方法指令。
此时回到了原方法,原方法的栈顶出现了新的结果值,然后继续进行运算。
JIT原理
工作原理
当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将字节码编译成本机机器代码。
通常javac将程序源码编译,转换成java字节码,JVM通过解释字节码将其翻译成相应的机器指令,逐条读入,逐条解释翻译。非常显然,经过解释运行,其运行速度必定会比可运行的二进制字节码程序慢。为了提高运行速度,引入了JIT技术。
在执行时JIT会把翻译过的机器码保存起来,已备下次使用,因此从理论上来说,採用该JIT技术能够,能够接近曾经纯编译技术。
2.相关知识
JIT是just in time,即时编译技术。使用该技术,可以加速java程序的运行速度。
JIT并不总是奏效,不能期望JIT一定可以加速你代码运行的速度,更糟糕的是她有可能减少代码的运行速度。这取决于你的代码结构,当然非常多情况下我们还是可以如愿以偿的。
从上面我们知道了之所以要关闭JITjava.lang.Compiler.disable(); 是由于加快运行的速度。由于JIT对每条字节码都进行编译,造成了编译过程负担过重。为了避免这样的情况,当前的JIT仅仅对常常运行的字节码进行编译,如循环等
总结
JVM的设计非常复杂,jvm的执行引擎和指令集也很有难度。
jvm在执行字节码时可能有一些优化方式,比如使用JIT编译部分代码成为本地代码。
JVM在执行程序时会记录某个方法的执行次数,如果执行次数达到一个阈值(客户端一般1500次,服务端一般10000次)时,JIT就会便以这个方法为本地代码
微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。(关注公众号后回复”Java“即可领取 Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南、Java程序员面试指南等干货资源)