Java 内存自动管理-虚拟机和内存区域概述

本文参考 JVM自动内存管理:内存区域基础概念

一、虚拟机及其定义

1、虚拟机概述

虚拟机: 模拟某种计算机体系结构,执行特定指令的软件;虚拟机一般分为 系统虚拟机、进程虚拟机。

系统虚拟机:Virtual BoxVMware 等,完整的模拟整个操作系统。

进程虚拟机:JVMAdobe Flash PlayerFC模拟器 等,进程虚拟机不会完整的模拟系统,而只是模拟某个特定指令级的虚拟机。

高级语言虚拟机:JVM.NET CLRP-Code 等,高级语言虚拟机属于进程虚拟机的一种,只不过更加细致化,将模拟某个指令级限定为某个高级语言。

Java 语言虚拟机:将高级语言限定为 Java 语言,但是并不一定所有的 Java 虚拟机都能称之为 JVM;

JVM:

  • 必须通过 Java TCK (Technology Compatibility Kit) 的兼容性测试才能称之为 JVM。
  • JVM 并非一定执行 “Java” 程序,JVM 面对的是 class 字节码;比如 Scala 语言,其生成也是 class ,便可运行在 JVM 上。
  • 业界三大商用虚拟机:Oracle HotSpot、Oracle JRockit VM、IBM J9 VM

Java 虚拟机概念遵循 “公有设计,私有实现”,即设计规范必须遵循,但是具体实现可以不同,只要在外表上一致即可,比如 JVM 规范要求内存可以自动释放,但每个虚拟机所采用的 GC 算法实现可能不尽相同;但最终要保证和达到的目的就是:同一份代码不做任何修改,在不同 JVM 上都能正确运行,

2、Java虚拟机运行时数据区

JVM 数据区定义:

在 JVM 规范中定义了若干种程序运行时需要的存储不同数据类型的数据区域;某些区域是全局共享的,随着虚拟机启动而创建,随着虚拟机关闭而销毁;某系区域是线程私有的,随着线程启动而创建,随着线程停止而销毁。所有的 Java 虚拟机都遵从该规范。

JVM 数据区划分:

Java 虚拟机数据区主要分为5大区块:程序计数器、Java 堆、Java 虚拟机栈、本地方法栈、方法区;图例如下:

数据区截图

从上图可以看出,方法区和堆是所有线程共享的,虚拟机栈、本地方法栈、程序计数器是每个线程私有的。

程序计数器:

程序计数器是一块非常小的内存空间,其作用是记录当前线程执行字节码的行号。如果当前线程执行的是一个 Java 方法,则该区域记录的是 class 地址,如果执行的是一个 本地的 Native 方法,则此区域为空。此区域是唯一一个在 JVM 规范中没有规定内存溢出等内存异常的区域。

二、Java 虚拟机栈和本地方法栈

1、Java 虚拟机栈的特征

  • 线程私有:Java虚拟机栈是线程私有的,其生命周期与线程相同;Java 虚拟机栈描述的是 Java 方法执行时的内存模型;每个方法在执行时都会创建一个栈帧,用来保存这个方法的操作数栈、局部变量表、方法出口、动态链接等信息;每个方法运行的过程就对应了一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 后进先出(LIFO)栈:Java 虚拟机栈是一个后进先出的栈,后面进入的栈帧会优先出栈;
  • 存储栈帧:每个 Java 方法的调用、执行和退出,都和存储的栈帧有着密切的联系。
  • 异常:JVM 规范对此定义了两种异常状况:OutOfMemoryError、StackOverflowError;如果线程请求的栈深度大于 JVM 允许的栈深度,就会抛出 StackOverflowError异常。

2、本地方法栈的特征

  • 线程私有:同上
  • 后进先出(LIFO)栈:同上
  • 作用是支撑 Native 的调用、执行和退出
  • 异常:同上

本地方法栈实现各不相同,如 Oracle HotSport 直接将 Java 虚拟机栈和 本地方法栈合二为一。

3、栈帧的概念和特征

  • 栈帧被设计为用于存储数据和部分过程结果的数据结构,同时也被用于存储动态链接、方法返回值和异常分派。
  • 每一个完整的栈帧都包含:局部变量表、操作数栈、动态链接信息、方法正常完成和异常完成信息;在程序运行时,栈帧需要多大的局部变量表、多深的操作数栈在编译期就已经确定了;Java 编译器会将其写入 class 字节码的 code 表中。
  • 在程序执行时,方法调用链可能会很长,很多方法都处于执行状态,对于执行引擎而言,在活动线程中只有位于 Java 虚拟机栈栈顶的栈帧才是有效的;该栈帧被称为当前栈帧,与之对应的关联方法被称之为当前方法,虚拟机执行中所有执行的字节码指令都针对当前栈帧和当前方法进行操作;

关于局部变量表和操作数栈大体内存图如下:

Java 虚拟机栈内存图

4、局部变量表

定义:局部变量表是一组局部变量值得存储空间,用于存储方法参数、方法内部定义的变量等;

在 JVM 编译时,就已经在 class code表中确定了该方法所需要局部变量表的最大容量;局部变量表以变量槽(Slot)为最小单位,JVM 规范并未明确说明一个 Solt 所占用的空间大小。只是非常有导向性的描述了一个 Solt 应该能存放一个布尔型、字节型、字符型、短整型、整型、浮点型、引用型(
reference)、返回地址(returnAddress)型的数据(两个 Solat 可以存放 long 或 double 型的数据);在这8中数据类型中,他们都可以使用32位甚至更小的内存空间来存储;这种规定允许 Solt 的大小随着不同 JVM 实现、不同操作系统、甚至是不同硬件如 CPU 等而变化,但是必须保证每个 JVM 上栈帧的 Solt 至少在外观上保持一致。

  • reference:即对象实例引用类型,JVM 规范并未完全说明该类型应该是何种数据结构甚至占用空间大小等,但此类型至少能完成2件事:1、通过该 reference 至少能直接或者间接的找到该引用引用的对象在 Java 堆中实例的具体内存地址;2、通过该 reference 能直接或者间接的找到这个对象所属的数据类型在 Java 方法区中的类型信息。
  • returnAddress:即返回地址型,该种类型目前已经不被使用,以前主要是为了字节码的3条指令:jsr、jsrw、ret服务的;他指向了一条字节码指令的地址,在以前的 JVM 中使用这几条命令和 returnAddress 实现异常处理。
  • long & double:同 JMM 规范类似,在 Solt 中同样允许对64位的 long、double类型分成2个32位的操作进行,不同的是由于局部变量表建立在 Java 虚拟机栈之上,属于线程私有,所以不会产生原子性的线程安全问题,同时 JVM 也不允许通过 class 直接访问 Solt 中的 long 和 double 类型。

JVM 通过索引值方式定位局部变量表中的数据,索引值从0开始,到局部变量表最大范围结束,对于32位的类型数据,索引n就代表访问第n个Solt中的数据,对于64位的类型数据,索引n代表第n和n+1个Solt中的数据。对于64位的数据,JVM不允许通过任何方式单独访问其中一个 Solt 数据。

局部变量表用于方法间参数传递,以及方法执行过程中存储基础数据类型和对象引用;如果正在执行的方法是实例方法(new 的对象里的),那么该方法的局部变量表第0个Solt将存储该方法所对应的实例引用;可以通过关键字 bis(不一定对) 来访问这个隐含参数,其他变量按顺序存储在局部变量表中;为节省空间,Solt可以被重用,如果方法体中定义的变量作用域,如果当前程序计数器范围查过了其作用域,那么该变量所占用的局部变量表中的 Solt 将被释放并重用;

5、操作数栈

定义:操作数栈也称操作栈,与局部变量表相同,其大小在编译期确定。

操作数栈由若干个 Entry 组成,JVM 规范定义,在操作数栈中,任意一个栈元素(Entry)都可以存储任意数据类型的数据,包括64位的 long 和 double;但普通32位的数据类型 Entry 深度为1,long 和 double Entry 深度为2;

在方法执行过程中,操作数栈栈帧用于存储计算参数和计算结果;在方法调用时,操作数栈也用来准备调用方法的参数以及接收返回值;在方法刚开始执行时,操作数栈是空的,在执行过程中,各种不同的字节码指令向操作数栈中写入和读取数据。比如一个int型加法操作,JVM必须保证操作数栈栈最接近栈顶的两个元素都是int型,相加的过程对应两个元素出栈相加并入栈的过程。出栈的数据类型必须与字节码的类型严格匹配,在编译时,编译器会严格检查该项。

在 Java 虚拟机栈中,往往两个栈帧是相互独立的,但在 JVM 实现中,往往对其进行优化,比如让两个逻辑上独立的栈帧出现一部分重叠,让下面栈帧的操作数栈和上面栈帧的局部变量表重叠,这样在方法互相调用参数传递时他们就共用一部分数据,从而避免了数据复制带来的性能损失。

可参考视频 Java 虚拟机栈和本地方法栈个人理解:局部变量表类似于方法执行时的永久存储区,而操作数栈类似于temp缓存区,所有运算借助操作数栈完成,最终结果返回到方法局部变量表。

6、异常情况

  • 如果线程请求的栈容量超出 Java 虚拟机栈容量,那么将抛出 StackOverflowError 异常。
  • 如果 Java 虚拟机栈可以动态扩展,但尝试扩展的动作无法申请到足够的内存去完成扩展,或者在建立新线程时无法获得足够的内存去创建对应的虚拟机栈,那么将抛出 OutOfMemoryError 异常。
  • JVM 参数:-Xss用于控制 Java 虚拟机栈的栈深度,注意:Oracle HotSpot 虚拟机 Java 虚拟机栈和本地方法栈是一个,所以此参数将控制两个栈的总体栈深度。

Java 内存自动管理-虚拟机和内存区域概述
https://mritd.com/2016/03/21/java-memory-overview-of-vm-memory-auto-management-and-memory-regions/
作者
Kovacs
发布于
2016年3月21日
许可协议