深入理解JVM之一

158 阅读4分钟

JVM整体架构图


多线程共享方法区和堆
虚拟机栈和本地方法栈和程序计数器每个线程独有一份

源码通过编译器变成成字节码文件

类装载器主要作用将字节码文件加载到内存生成一个大的class对象(存放在方法区)
该过程分为 加载、链接、初始化 三个阶段
每个字节码文件都对应一个Java类
字节码文件也是跨平台的

然后由执行引擎(包括解释器和JIT即时编译器和垃圾回收器)的解释器进行解释运行

只用解释器运行体验差一些 对于反复执行的热点代码 希望提前编译出来即JIT编译器干的活

Java代码执行流程

操作系统只能识别机器指令 不能识别字节码指令
所以需要执行引擎将字节码翻译成机器码指令

解释器对字节码逐行解释执行 保证响应时间
JNI编译器对字节码中反复要执行的代码(即热点代码)直接编译成机器指令 同时在方法区缓存起来 保证执行的性能

主流的虚拟机都采用二者并存的方式

JVM指令集架构模型

Java编译器输入的指令流一种是基于栈的指令集架构 另一种是基于寄存器的指令集架构
HostSpot只有PC寄存器
HostSpot中任何操作都需要经过入栈和出栈的操作 栈来管运行 由此可见
HostSpot执行引擎架构是基于栈的指令集架构

两种架构区别

基于栈的指令集架构

  • 设计和实现更简单 适用于资源受限的系统 比如嵌入式的小型设备 机顶盒、打印机等
  • 避开了寄存器的分配难题 使用零地址指令的方式分配
  • 指令流中的指令大部分都是零地址指令 其执行过程依赖于操作栈 指令集更小(在字节码文件中每8位为一个单位进行对齐) 编译器容易实现
一个指令执行的时候 需要两部分 一个是地址 另一个是操作数
所谓的零地址指令 就是没有地址 只有操作数
一地址指令有一个地址
二地址指令有两个地址

栈只是一个入栈出栈的操作 只针对于栈顶的数据进行操作 其他的数据不操作 所以就不需要地址了
  • 不需要硬件支持(栈是内存层面的 不需要和硬件打交道) 可移植性更好 更容易实现跨平台

基于寄存器的指令集架构

  • 典型的应用是x86的二进制指令集 比如传统的PC以及Andriod的Davlik虚拟机
  • 指令集架构则完全依赖硬件(CPU) 可移植性差
  • 性能优化和执行更高效(CPU在高速缓冲区执行)
  • 花费更少的指令区完成一项操作
  • 在大部分情况下 给予寄存器架构的指令集往往都以一地址指令、二地址指令、三地址指令为主(采用十六位双字节的对齐方式进行对齐)

举例1

同样执行2+3这种逻辑操作,其指令分别如下

基于栈的指令集架构 指令集小 但完成同样的操作 指令多

iconst_2 //常量2入栈
istore_1 
iconst_3 //常量3入栈
istore_2
iload_1
iload_2
iadd //常量2、3出栈,执行想加
istore_0 //结果5入栈

基于寄存器的指令集架构 指令集大 完成同样的操作 指令少

mov eax,2 //将eax寄存器的值设为1
add eax,3 //使eax寄存器的值加3

源码

编译之后的字节码

进行反编译

javap -v StackStruTest.class

0、1、2是pc寄存器实际的地址
编译完之后就可以直接识别出2+3是5
和直接在源码中写 int i=5是一样的

如果源码这样写

反编译新的字节码文件

iconst 2 生成常量2 保存起来
istore 1是操作数栈的一个索引位置 保存到1的这个操作数栈中
iconst 3 定义一个常量3 
istore 2保存在索引为2的这个位置
iload_1
iload_2 把i和j分别加载进来
iadd 做一个求和的操作
istore_3 求和之后的k保存在3的这个操作数栈中

3行源码 对应的指令有8

基于栈的指令集主要特点:跨平台性、指令集小、指令多;执行性能比寄存器差