JVM的结构

1,939 阅读5分钟

JVM的结构

结合《深入理解Java虚拟机:高级特性及最佳实践》、《实战Java虚拟机——JVM故障诊断与性能优化》、油管视频,下图是我对JVM结构的总结:

要理解JVM的结构,其实可以从java程序怎么运行的角度去理解:

java程序运行的是class文件,所以需要类装载子系统来把class文件加载到内存中运行,而class文件具体是加载存放到jvm中一块叫方法区的内存空间,方法区除了存放类信息外,还划分了一块叫运行时常量池的区域,用来存放字符串字面量、数字常量等信息。

class文件加载了,内存中有类信息了,java程序中就可以创建对象。那么对象又存哪里?当然是我们耳熟能详的堆内存了。另外,除了堆内存,还有一块直接内存,这块内存是堆外的内存,是直接向系统申请的内存空间,这块内存访问速度比堆内存块,比如Java的NIO库就使用了直接内存。

上面几样都是跟程序的数据存储相关的,而程序需要实现运行,以下的就是相关的:

Java栈: Java是多线程的,一条运行的线程就会有一个对应的Java栈。而一个线程跑的程序,会调用很多的函数,一个函数就对应Java栈中一个栈帧。栈帧存放着函数运行需要的东西,栈帧包含了:局部变量表操作数栈帧数据区等等。帧数据区是用来支持栈帧做常量池解析、方法返回、异常处理的。比如当Java字节码需要访问常量池时,帧数据区保存则常量池的指针,方便程序访问常量池;比如异常处理,帧数据区中会有一个异常处理表,方便在发生异常时找到处理异常的代码。

Exception table:
from    to  target  type
4       16  19      any
19      21  19      any

上面的异常处理表,表示字节码偏移量4~16字节可能抛出异常,如果抛出异常,跳转到偏移量19的地方继续执行。

程序计数器: 英文名是Program Counter Register,直译是“程序计数器寄存器”,所以国内的博客、书籍叫法有些统一。有的叫“程序计数器”,有的叫“PC寄存器”。我这里参考《深入理解Java虚拟机-JVM高级特性与最佳实践》的表述方法,叫程序计数器。程序计数器就是用来记录线程当前执行到哪一行字节码的。

本地方法栈: 结构跟Java栈差不多,区别就是它是用来执行本地方法的。

以上的方法区、Java堆、程序计数器、Java栈、本地方法栈构成了JVM的运行时数据区

有了运行时数据区来存放Java程序运行的所有数据,就可以用执行引擎来把程序执行跑起来了。比如Interpreter是解析器,用来读取执行字节码的指令;JIT Compiler是即时编译器,可以即时把字节码指令编译机器码,弥补了Interpreter的性能不足;GC就是垃圾收集器,用来收集在运行时产生的垃圾内存。

关于局部变量表的一个细节

我在看《实战Java虚拟机-JVM故障诊断与性能优化》时看到一些有趣的地方,做了测试,顺便记录下来。

细节1:不同编译器编译出来局部变量表的大小有可能不一样。

public class TestStackDeep {
    private static int count = 0;

    public static void recursion(long a, long b, long c) {
        long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
        count++;
        recursion(a, b, c);
    }
    public static void main(String args[]) {
        try {
			recursion(0L,0L,0L);
        } catch (Throwable e) {
            System.out.println("deep of calling = " + count);
            e.printStackTrace();
        }
    }
}

上面代码中的recursion方法,有3个long类型的形参,方法中还定义了10个局部变量。在Intellij IDEA中使用jclasslib的插件可以查看到这个方法的局部变量表的大小:

这里是在Intellij IDEA中使用jdk7的javac编译器编译出来的,局部变量表占26个字:

改成eclipse编译器后,局部变量表占6个字:

看来eclipse编译器是做了优化,在recursion方法中定义的10个局部变量都没有使用过,所以直接就去掉了,只剩下6个字

“字”是指计算机内存中占据的一个单独的内存单元编号的一组二进制串。一般32位计算机上一个字为4个字节长度

细节2:局部变量表的槽位

public class LocalVar {
    public void localvar1() {
        int a = 0;
        System.out.println(a);
        int b = 0;
    }

    public void localvar2() {
        {
            int a = 0;
            System.out.println(a);
        }
        int b = 0;
    }

    public static void main(String[] args) {

    }
}

还是使用jclasslib来查看局部变量表。其中,

localvar1的局部变量表(最大占3个字):

localvar1的局部变量表(最大占2个字):

注意看localvar1的Index列显示占的槽位是0,1,2三个槽位,一个槽位占一个字,所以localvar1的局部变量表最大占3个字;

而注意看localvar2的Index列显示占的槽位是1,0,1,其中槽位“1”复用了,这叫叫槽位复用,所以localvar2的局部变量表最大占2个字