必备知识 - JVM虚拟机

135 阅读6分钟

必备知识 - 初始JVM虚拟机

JVM概要 (内存方面)

1、JVM是什么

  • JVM是一个规范,是对汇编语言的规范和处理,JAVA与JVM没有特别大的联系,它只是与特定的二进制文件格式.class有联系。
  • 它将上层高级语言翻译成硬件所支持的机器码汇编
  • 任何可以编译成class字节码文件的语言都可以在JVM上运行
  • jvm基于语言开发,因为C内存管理太麻烦了,它在C中申请了一块空间,自己做内存管理(分配和销毁)
  • 由此延伸出HotSpot(java)和Art、Dalvik(Android)

2、JVM架构

  • 类加载器
    • 将class加载到内存,并解析,将类信息存放到方法区
  • 运行时数据区
    • 存放运行时所产生的数据
    • 方法区:存放加载后类信息,常量池、静态变量表、配置信息
    • 堆区:存放对象实例
    • 栈区:负责函数的调用与执行 (局部变量表、操作数栈、动态链接、地址返回)
  • 执行引擎
    • 负责翻译字节码指令,并交给CPU执行对应的操作
    • 负责垃圾回收GC

3、JVM内存结构分配

image.png

3.1、线程共享区

  • 所有线程共享区域,生命周期与JVM保持一致
  • 方法区:
    • 存放解析class之后所产生的数据
    • 类信息:父类、接口、方法表、字段、配置信息,字节码信息
    • 常量、静态变量
  • 堆区
    • 存放对象实例
    • GC重点关照区域

3.2、线程私有区

  • 线程私有是每条线程都会新开辟一块空间,都是独立的
  • 虚拟机栈
    • 函数执行的容器,负责函数的执行与调用,入参与出参的控制
    • 栈帧:
      • 代表一个函数,一个函数的执行就是入栈出栈的过程
      • 局部变量表: 顾名思义,存放布局变量信息
      • 操作数栈:负责运算
      • 动态链接: 负责将字节码符号转为真实地址(编译期无法确认函数指令地址,所以拿符号代替,等指令加载到内存时,通过动态链接去方法区查找指令地址,开始执行)
      • 方法返回:记录函数被调用的位置,函数执行完成之后从该位置继续向下执行
  • 本地方法区
    • 存放native函数从参数与局部变量表
  • 程序计数器
    • 记录当前线程执行位置
    • 方便线程暂停/恢复或因为CPU分配的原因切到其他线程又切回来可以从上一次执行位置继续执行
    • 我们常用的debug可以是根据这个来的

3.3、JVM内存分布

  • CPU和内存不能直接进行交互,因为内存速度比CPU慢很多,所以中间加了高速缓存区
  • 高速缓存区位于CPU与主存之间,它的容量比内存小,但是交换速度快,存取速度与CPU持平,由于内存执行速度相比CUP慢很多,需要将数据提前加到高速缓冲区中。
  • 方法区和堆区运行在主存
  • 栈区运行在高速缓存区
  • 高速缓冲区大小有限,一般12M,高速缓冲区也称三级缓存

Java内存分配.png

3.4、JVM内存分布在字节码中的体现

  • 结合上面理论,我们在字节码中印证一下上面结论(以art字节码为例)
  • Art、Davlik虚拟机是基于寄存器架构(指令数量少,执行更快,有引用概念,后面会详细介绍)
  • 先来一段示例代码
public class CodeTest {
    int value = 0;

    /**
     * 构造函数调用add函数
     */
    public CodeTest() {
        add();
    }

    /**
     * 定义变量、调用函数运算,结果给到全局变量
     */
    public void add() {
        int a = 170;
        int b = 200;

        value = sum(a,b);
    }

    /**
     * 运算函数
     */
    public int sum(int a, int b) {
        int c = a + b;
        return c;
    }
}
  • 字节码 - 构造函数
|[000108] com.hbb.annotate.test.CodeTest.<init>:()V
|0000: invoke-direct {v1}, Ljava/lang/Object;.<init>:()V // method@0003
|0003: const/4 v0, #int 0 // #0
|0004: iput v0, v1, Lcom/hbb/annotate/test/CodeTest;.value:I // field@0000
|0006: invoke-virtual {v1}, Lcom/hbb/annotate/test/CodeTest;.add:()V // method@0001
|0009: return-void
    
逐行解释
- 第一行(0000):调用Object类的构造方法,使用invoke-direct指令。这个构造方法是无参的,用于初始化对象。
- 第二行(0003):定义一个常量0,存放到寄存器v0中
- 第三行(0004):将v0的内存首地址交给v1,v1就是我们定义的“value”全局变量
- 第四行(0006):调用add函数,将v1作为参数
- 第五行(0009):返回void
  • 字节码 - add函数
|[00012c] com.hbb.annotate.test.CodeTest.add:()V
|0000: const/16 v0, #int 170 // #aa
|0002: const/16 v1, #int 200 // #c8
|0004: invoke-virtual {v2, v0, v1}, Lcom/hbb/annotate/test/CodeTest;.sum:(II)I // method@0002
|0007: move-result v0
|0008: iput v0, v2, Lcom/hbb/annotate/test/CodeTest;.value:I // field@0000
|000a: return-void

 #### 逐行解释
- 第一行(0000):将常量值170存储在寄存器v0中。
- 第二行(0002):将常量值200存储在寄存器v1中。
- 第三行(0004):调用名字“sum”的方法,将v0,v1作为参数,将返回值存在寄存器v0中
- 第四行(0007):将寄存器v0的值移动到v0中。 (这行指令是多于的)
- 第五行(0008):将v0(函数返回值)值赋值给v2,也就是全局变量“value”
- 第六行(000a):返回void
  • 字节码 - sum函数

|[000154] com.hbb.annotate.test.CodeTest.sum:(II)I
|0000: add-int v0, v2, v3
|0002: return v0

#### 逐行解释
- 第一行(0000):将v2和v3进行相加运算(170 + 200),将结果存到寄存器v0中
- 第二行(0002):将寄存器v0的值作为方法返回值。

  • 小结
    • 注意字节码中method@0003和field@0000相关字符
    • 在编译期调用全局变量、函数时无法明确对应字节码地址,所以先打上符号,当class被加载时,类加载器会解析class信息(常量、静态变量、函数、配置信息),存放到方法区
    • 运行时由栈区“动态链接”拿到符号去方法区查字段or函数字节码地址,之后交给执行引擎执行。
    • 这也说明了为什么反射会比函数正常执行耗时,因为反射没有相关字符,它需要通过函数签名去方法区变量函数表查询函数地址

3.4、总结

  • jvm是一种规范,与java语言没有直接关联,只与class字节码有关,它负责解释字节码为对应环境的汇编机器码执行。
  • 高速缓存区存在的意义是因为cpu与内存处理速度不一致,加个缓存区,来处理数据。
  • 方法区、堆区运行在主存,也就是内存 (线程共享)
  • 栈区运行在高速缓存区 (线程私有,每个线程都有一份)
  • 线程与线程之间无法直接访问内部数据,可以加上final字段,复制一份到方法区
  • 反射比正常函数执行慢的原因,是因为需要遍历方法区函数表(artMethod)找到函数字节码地址,而正常调用,字节码中会有一个符号,直接可以通过符号定位到字节码地址。