以前学JVM的时候看过《深入理解JVM》,当时看的很模糊也记了些笔记,更像是为了应付面试。事实是确实把笔记都背上了,春招找实习的时候,内存管理、类加载、垃圾回收三连背一遍。后来自己做项目的时候,涉及到JVM的部分还是不怎么理解,最近重读了上面的书并且看了一些技术大佬的专栏,用博客记录下自己学习过程与思考。

本篇文章关注两个问题:

  1. Java字节码是什么?Java源代码怎么变成Java字节码的?

  2. Java字节码进入JVM后是怎么存储的?

  为了解释上面问题,假设现在我们有一个Main类,调用compute方法执行计算操作,代码如下:

public class Math {
    public static final Integer CONSTANT = 10;

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math1 = new Math();
        Math math2 = new Math();
        math1.compute();
        math2.compute();
    }
}

对于第一个问题: Class文件是一组以8位字节位基础的单位的二进制流,下图就是显示了如何生成字节码文件。

再探JVM内存模型-LMLPHP

使用Sublime Text查看Math.class,图片只截取了部分,编辑器是使用16进制显示的。为了方便查看,我们使用 javap -c 指令对代码进行反汇编,就可以得得到可读性更强的文件。

再探JVM内存模型-LMLPHP

再探JVM内存模型-LMLPHP

  那么Class文件被加载后在JVM中是如何存储的呢?我们以 HotSpot VM为例,这是目前使用最广泛的Java虚拟机。虚拟机主要由类装载子系统、运行时数据区和执行引擎三部分组成。JVM内存模型将运行时数据区分为五个部分,下面图中其中紫色部分是线程私有的,黄色是线程公有的。整个代码的执行流程在JVM内存中是这样的:

  再探JVM内存模型-LMLPHP

  我们对着字节码文件来阐述。虚拟机又叫做线程栈,生命周期与线程相同。栈主要由局部变量表、操作数栈、动态链接、方法出口组成。当main方法运行时JVM会在栈内存区域给主线程分配一块内存,main方法和compute方法执行时,会创建单独的栈帧用于存储方法的一些信息。

  • 局部变量表:存放的是方法在执行时各种基本类型和引用类型变量,以及returnAddress类型(指向了一条字节码指令的地址);
  • 方法出口:保存的是方法执行完后回到主线程的哪个位置。对于main栈帧,局部变量表里的math变量存放的是堆内存中math变量的地址。
  • 操作数栈:临时存放方法执行时的变量
  • 动态链接:Class 文件中存放了大量的符号引用,这些符号引用指向的是方法。程序运行期间调用方法时,根据运行时常量池的参数,静态符号引用变成直接引用;对象头里的指针会动态的找到方法区中存储的调用方法的信息。

再探JVM内存模型-LMLPHP

程序计数器:记录的是字节码指令正在执行或者即将执行的行号,比如这行 ”0: iconst_1“执行完了,程序计数器值就是1,表示即将执行下一行指令。

本地方法栈:作用和虚拟机栈类似,为native修饰的方法服务。

方法区:JDK1.8及以后称为元空间,存储被虚拟机加载的类信息、常量、静态变量等。1.8以后方法区使用的是本机的内存。例如 这一行指令 ”public static final java.lang.Integer CONSTANT;“就是静态常量 CONSTANT的信息。

堆:堆是JVM内存模型中最大的一块,虚拟机启动时就会创建,存储的是大部分对象。

参考资料:《深入理解Java虚拟机》第二版 周志朋

     《深入拆解Java虚拟机》郑雨迪

     《JVM虚拟机底层原理分析与性能调优》程序员诸葛 

07-02 21:42