结合《深入理解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故障诊断与性能优化》时看到一些有趣的地方,做了测试,顺便记录下来。
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个字节长度
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个字