以前学 JVM 的时候看过《深入理解 JVM》, 当时看的很模糊也记了些笔记, 更像是为了应付面试. 事实是确实把笔记都背上了, 春招找实习的时候, 内存管理, 类加载, 垃圾回收三连背一遍. 后来自己做项目的时候, 涉及到 JVM 的部分还是不怎么理解, 最近重读了上面的书并且看了一些技术大佬的专栏, 用博客记录下自己学习过程与思考.
本篇文章关注两个问题:
Java 字节码是什么? Java 源代码怎么变成 Java 字节码的?
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 位字节位基础的单位的二进制流, 下图就是显示了如何生成字节码文件.
使用 Sublime Text 查看 Math.class, 图片只截取了部分, 编辑器是使用 16 进制显示的. 为了方便查看, 我们使用 javap -c 指令对代码进行反汇编, 就可以得得到可读性更强的文件.
那么 Class 文件被加载后在 JVM 中是如何存储的呢? 我们以 HotSpot VM 为例, 这是目前使用最广泛的 Java 虚拟机. 虚拟机主要由类装载子系统, 运行时数据区和执行引擎三部分组成. JVM 内存模型将运行时数据区分为五个部分, 下面图中其中紫色部分是线程私有的, 黄色是线程公有的. 整个代码的执行流程在 JVM 内存中是这样的:
我们对着字节码文件来阐述. 虚拟机栈又叫做线程栈, 生命周期与线程相同. 栈主要由局部变量表, 操作数栈, 动态链接, 方法出口组成. 当 main 方法运行时 JVM 会在栈内存区域给主线程分配一块内存, main 方法和 compute 方法执行时, 会创建单独的栈帧用于存储方法的一些信息.
局部变量表: 存放的是方法在执行时各种基本类型和引用类型变量, 以及 returnAddress 类型 (指向了一条字节码指令的地址);
方法出口: 保存的是方法执行完后回到主线程的哪个位置. 对于 main 栈帧, 局部变量表里的 math 变量存放的是堆内存中 math 变量的地址.
操作数栈: 临时存放方法执行时的变量
动态链接: Class 文件中存放了大量的符号引用, 这些符号引用指向的是方法. 程序运行期间调用方法时, 根据运行时常量池的参数, 静态符号引用变成直接引用; 对象头里的指针会动态的找到方法区中存储的调用方法的信息.
程序计数器: 记录的是字节码指令正在执行或者即将执行的行号, 比如这行 "0: iconst_1" 执行完了, 程序计数器值就是 1, 表示即将执行下一行指令.
本地方法栈: 作用和虚拟机栈类似, 为 native 修饰的方法服务.
方法区: JDK1.8 及以后称为元空间, 存储被虚拟机加载的类信息, 常量, 静态变量等. 1.8 以后方法区使用的是本机的内存. 例如 这一行指令 "public static final java.lang.Integer CONSTANT;" 就是静态常量 CONSTANT 的信息.
堆: 堆是 JVM 内存模型中最大的一块, 虚拟机启动时就会创建, 存储的是大部分对象.
参考资料:《深入理解 Java 虚拟机》第二版 周志朋
《深入拆解 Java 虚拟机》郑雨迪
《JVM 虚拟机底层原理分析与性能调优》程序员诸葛
来源: https://www.cnblogs.com/fly-bryant/p/13223965.html