Java|携程面试官问我怎么划分 Java 虚拟机内存区域,相见恨晚!


Java|携程面试官问我怎么划分 Java 虚拟机内存区域,相见恨晚!
文章图片
Java|携程面试官问我怎么划分 Java 虚拟机内存区域,相见恨晚!
对于我们 Java 程序员来说 , 不需要像 C/C++ 程序员那样时时刻刻关心着内存泄露和内存溢出的问题 , 但实际的工作中 , 这两个问题出现的频率还是蛮高的 , 尤其是在多线程并发的情况下 。 如果不了解 Java 虚拟机是如何管理内存的 , 那么一旦遇到问题可能就会束手无策 。
了解 Java 虚拟机的内存区域划分有助于我们更好的去理解 Java 虚拟机 , 从而掌握内存问题排查的主动权 。
在谈 JVM 内存区域划分之前 , 我们先来看一下 Java 程序的具体执行过程 , 我画了一幅图 。


Java 程序的具体执行过程

Java 源代码文件经过编译器编译后生成字节码文件 , 然后交给 JVM 的类加载器 , 加载完毕后 , 交给执行引擎执行 。 在整个执行的过程中 , JVM 会用一块空间来存储程序执行期间需要用到的数据 , 这块空间一般被称为运行时数据区 , 也就是常说的 JVM 内存 。
所以 , 当我们在谈 JVM 内存区域划分的时候 , 其实谈的就是这块空间——运行时数据区 。
大家应该对官方出品的《Java 虚拟机规范》有所了解吧?了解这个规范可以让我们更深入地理解 JVM 。 该规范主要包含 6 个部分 , 分别是:
  • 第一章:引言
  • 第二章:Java 虚拟机结构
  • 第三章:Java 虚拟机编译
  • 第四章:Class 文件
  • 第五章:加载、链接和初始化
  • 第六章:Java 虚拟机指令集
  • 第七章:操作码
根据第二章 Java 虚拟机结构中的规定 , 运行时数据区可以分为以下几个部分 , 见下图 。


01、程序计数器
程序计数器(Program Counter Register)所占的内存空间不大 , 很小一块 , 可以看作是当前线程所执行的字节码指令的行号指示器 。 字节码解释器会在工作的时候改变这个计数器的值来选取下一条需要执行的字节码指令 , 像分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成 。
在 JVM 中 , 多线程是通过线程轮流切换来获得 CPU 执行时间的 , 因此 , 在任一具体时刻 , 一个 CPU 的内核只会执行一条线程中的指令 , 因此 , 为了线程切换后能恢复到正确的执行位置 , 每个线程都需要有一个独立的程序计数器 , 并且不能互相干扰 , 否则就会影响到程序的正常执行次序 。
也就是说 , 我们要求程序计数器是线程私有的 。
《Java 虚拟机规范》中规定 , 如果线程执行的是非本地(native)方法 , 则程序计数器中保存的是当前需要执行的指令地址;如果线程执行的是本地方法 , 则程序计数器中的值是 undefined 。
为什么本地方法在程序计数器中的值是 undefined 的?因为本地方法大多是通过 C/C++ 实现的 , 并未编译成需要执行的字节码指令 。
由于程序计数器中存储的数据所占的空间不会随程序的执行而发生大小上的改变 , 因此 , 程序计数器是不会发生内存溢出现象(OutOfMemory)的 。
02、Java 虚拟机栈
Java 虚拟机栈中是一个个栈帧 , 每个栈帧对应一个被调用的方法 。 当线程执行一个方法时 , 会创建一个对应的栈帧 , 并将栈帧压入栈中 。 当方法执行完毕后 , 将栈帧从栈中移除 。 栈遵循的是后进先出的原则 , 所以线程当前执行的方法对应的栈帧必定在 Java 虚拟机栈的顶部 。
栈帧包含以下 5 个部分 , 见下图 。

栈帧

1)局部变量表
顾名思义 , 就是用来存储方法中的局部变量的 , 包括方法的参数 。 对于基本数据类型的变量 , 直接存储变量的值;对于引用类型的变量 , 存储的是对象的引用 。 局部变量表的大小在编译期间就确定了 , 程序执行期间 , 它的大小是不会改变的 。
2)操作数栈
表达式的计算是在操作数栈中完成的 。 当一个方法刚开始执行的时候 , 这个方法的操作数栈是空的 , 在方法的执行过程中 , 会有各种字节码指令往操作数栈中写入和提取内容 , 也就是入栈/出栈操作 。 例如 , 在做算术运算的时候是通过操作数栈来进行的 , 又或者在调用其他方法的时候是通过操作数栈来进行参数传递的 。