理解JVM之内存模型JMM

1,083 阅读5分钟

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

JMM内存模型图

java内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制和规范。目的是保证并发编程场景中的原子性、可见性和有序性。

根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域。

五大内存区域

图中黄色块为线程的共享区域,紫色块为线程的私有区域。

堆是java虚拟机管理内存最大的一块内存区域,因为堆中存放的对象是线程共享的,故多线程环境中通常需要同步机制。

所有对象实例数组都要在堆上分配内存,它是线程共享的,同时它也是GC所管理的主要区域,因此常被称为GC堆。

关于在Java堆中经常出现的“新生代、老年代、永久代”等概念,只是一部分垃圾收集器的公有特性或者说是设计风格而划分的区域,Java虚拟机中并未有这样的内存布局。

Java堆既可以被实现成固定大小,也可以动态扩展,通过参数-Xmx和-Xms设定,当对象无法在堆中分配实例,并且堆无法再扩展时,Java虚拟机将会抛出OutOfMemoryErrno异常

方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

关于永久代,在Jdk8以前有着这样的设计,但由于永久代有-XX:MaxPermSize的上限,会使Java应用更容易遇到内存溢出。而在jdk8以后,就废弃了永久代的概念,改为用本地内存实现的元空间代替,该部分的内存有本机内存大小相关。

在方法区区域的回收目标主要是针对常量池的回收和对类型的卸载。如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

关于运行时常量池,是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。该部分的内容将在类加载后放到方法去的运行时常量池中,在运行期间,可以利用String的intern()方法将常量加入常量池。

虚拟机栈

虚拟机栈为线程私有区域,为方法执行的内存区。每个方法在执行时会在虚拟机栈中创建一个栈帧。栈帧用于存储 数据和部分过程结果的数据结构,同时也被用来处理动态链接、返回返回值和异常分派。一个完整的栈帧包含 局部变量表操作数栈动态链接信息方法正常完成异常完成信息。每个方法被调用的过程对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈帧: 是用来存储数据和部分过程结果的数据结构。

栈帧大小确定时间: 编译期确定,不受运行期数据影响。

局部变量表是一组变量值的存储空间,它用于存储方法,参数,以及方法内部定义的局部变量。

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和retyrnAddress类型(指向了一条字节码指令的地址)

Java虚拟机栈可能出现两种类型的异常:

  • 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  • 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

本地方法栈

本地方法栈与虚拟栈类似,区别在于为本地方法服务,即使用native方法调用底层c或c++代码。

程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

由于一个线程有着多个指令,为了线程切换可以恢复到正确执行位置,每个线程都有着私有的程序计数器,不同线程之间的程序计数器互不影响、独立存储。

需要注意的是,这块内存区域在java虚拟机规范中是唯一不会发生OutOfMemoryError的区域。当执行本地方法时,程序计数器为空(undefined)

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但该部分内存也被频繁使用,也有可能导致OutOfMemoryError异常出现。

NIO(New Input/Output)引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用直接操作。

一个直接内存导致的溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的遗产情况。如果发生溢出后产生的Dump文件很小,而程序中间接使用了DirectMemory(NIO),就有可能是这方面的原因了。

参考资料

java内存区域