JVM

文章发布时间:

最后更新时间:

文章总字数:
1.7k

预计阅读时间:
6 分钟

页面浏览: 加载中...

学习JVM的基础知识

来历

JVM怎么来的很简单,当时为了解决跨平台运行的要求,JVM便应运而生了。

用过C的朋友都知道在Windows下编译的C程序是不能够直接到Linux上运行的,也就是说C++是不能够跨平台运行的。

而Python/Ruby等为什么能够直接跨平台运行呢?因为他们用的是直译器,相当于每跑一行代码就会生成机器码然后执行,因此使用直译器可以解决跨平台的问题。

但是直译器也有缺点,那就是直译器无法像编译器一样对代码进行优化,提高代码的运行速度。

如何解决直译器和编译器之间的优缺点呢?答案就是将他们结合在一起。因此JVM就诞生了,就有了java里编译运行的流程:先将Test.java用javac编译成.class文件,然后通过JVM将字节码转换为机器码运行。

JVM架构

JVM大致由三部分构成:

  • 类加载器 ClassLoader
  • 运行时数据区 Runtime Data Areas
  • 执行引擎 Execution Engine

下面简单讲述一下这三个部分的作用是什么

类加载器

类加载器就是用来加载.class文件的,它将.class文件加载到内存中,主要经历加载 -> 连接 -> 实例化三个阶段。

运行时数据区

相当于划分出来的内存区域。JVM会定义java程序运行期间要用到的内存区域,这块区域存放了字节码信息和程序执行的数据,垃圾收集器也会对这一区域进行对象回收的操作。

例如运行时数据区会存储:

  • 虚拟机栈
  • PC寄存器
  • 本地方法栈
  • 堆
  • 方法区

JDK8还会存储元空间、各种常量池等

执行引擎

最主要的功能就是将字节码指令解释/编译为对应平台的机器指令。

执行引擎包含:

  • 解释器:读取字节码,然后执行指令。由于解释器是一行一行地解释和执行指令,所以它可以很快地解释字节码,但是执行起来比较慢
  • JIT(Just-In-Time,即时编译器):即时编译器会随着解释器运行的时间推移选择性地将一些热点代码编译成本地代码。执行本地代码会比一条条进行解释执行会快很多,因为本地代码是保存到缓存里的。
  • 垃圾回收器:用于回收内存中的垃圾对象。

JVM是如何执行java代码的?

Java代码的运行分为两个阶段,编译期和运行期

编译期

以Hello World为例:

1
2
3
4
5
6
7
8
package com.Err0r233;

public class test {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

将它编译后会生成对应的.class文件,通过idea的View(视图)->Show bytecodes(显示字节码)可以查看.class文件的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// class version 52.0 (52)
// access flags 0x21
public class com/Err0r233/test {

// compiled from: test.java

// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/Err0r233/test; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Hello World!"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 6 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}

字节码并非机器码,操作系统无法识别,因此需要在操作系统上安装不同版本的JVM来识别,通常情况下只需要安装JDK即可

运行期

当有了字节码,我们就需要启动JVM来运行字节码文件。

首先JVM会通过类加载器加载字节码文件,然后将字节码加载到JVM的运行时数据区,再通过执行引擎转化为机器码最终交给操作系统执行。

还记得运行时数据区里含有什么吗:

  • 虚拟机栈
  • PC寄存器
  • 本地方法栈
  • 堆
  • 方法区

其中PC寄存器、虚拟机栈以及本地方法栈都是线程私有的,堆和元空间是线程共享的,不同的线程共享这两部分数据。

当JVM当前线程执行的是java的普通方法时,PC寄存器存储了该方法的第一条指令,当方法开始执行后,PC寄存器存储的是下一个字节码指令的地址。如果JVM当前线程执行的是java的native方法时,PC寄存器的值为undefined。如果遇到if等分支跳转语句,PC寄存器会被置为目标字节码指令的地址。

除了PC寄存器以外,字节码指令的执行流转还需要虚拟机栈的参与,它的栈内会有许多栈帧,栈帧又大致由下面的这几个部分构成:

  • 局部变量
  • 操作数栈
  • 动态链接
  • 方法返回地址

例如下面这个Test类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
User user = new User();
Integer result = calculate(user.getAge());
System.out.println(result);
}

private static Integer calculate(Integer age) {
Integer data = age + 3;
return data;
}

}

首先JVM完成.class文件加载之后,会创建一个main线程,并调用main静态方法,这是java程序的入口点。

main线程在执行main方法的时候,JVM会向虚拟机栈压入一个main的栈帧。

栈帧的操作数栈存储了操作的数据,JVM在执行字节码指令的时候会从操作数栈中获取数据,执行计算操作后会将结果再压回操作数栈。

接下来当进行calculate方法调用的时候,会往虚拟机栈压入一个calculate的栈帧。然后执行age+3

当age + 3的时候,JVM会将操作数栈顶部的两个元素弹出,计算结果之后再压回操作数栈。

之后计算完毕,calculate方法执行完成,对应的calculate栈帧会从虚拟机栈中弹出,方法执行的结果(age+3的结果)会压入main栈帧中的操作数栈中。方法返回的地址被重置到main线程的PC寄存器,以便后续字节码执行引擎从PC寄存器中获取到下一条命令的地址。

执行引擎的解释器会从程序计数器(PC)中获取到下一个字节码指令的地址,获取到指令之后通过解释器转换为对应的机器码指令,由CPU执行。

,