JVM杂谈(一)运行时数据区域划分

543 阅读6分钟

JVM杂谈(一)运行时数据区域划分

最近在阅读《深入了解Java虚拟机》,记录一下自己的收获,并且梳理一下整体的思路

运行时数据区域

Java虚拟机在执行java程序过程中会把自己所管理的内存划分为若干个不同的数据区域,如下图

img

程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,看作是当前线程所执行的字节码的行号指示器,在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变程序计数器来选取下一条需要执行的字节码指令,由于Java虚拟机的多线程是通过线程轮换轮流切换,分配处理器时间来实现的,在任何一个确定时刻,一个处理器都只会执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每个线程都需要有独立的程序计数器,这也是为什么程序计数器是线程隔离的。

TIPS:如果线程执行的是Java方法,那么程序计数器记录的是正在执行的虚拟机字节码指令地址,如果执行的是本地方法(JAVA方法是由JAVA编写的,编译成字节码,存储在class文件中,本地方法是由其它语言编写的,编译成和处理器相关的机器代码),则计数器应该为空;

程序计数器是java虚拟机规范中唯一一个没有规定任何OOM情况的区域

Java虚拟机栈(Java Virtual Machine Stack)

Java虚拟机栈是线程私有的,其生命周期与线程相同,虚拟机栈描述的是java方法执行的线程内存模型(每个方法执行时候,java虚拟机都会同步创建一个栈帧Stack Frame来存储方法的各种信息,诸如局部变量表,方法出口等信息,,每一个方法从被调用到执行完毕就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程),其中局部变量表存放了编译期间知道的各种java基本数据类型,以及对象引用和returnAddress类型(指向了一条字节码指令的地址)。

这些数据在局部变量表中的存储空间以局部变量槽Slot来表示,其中64位长度的long与double数据会占据两个变量槽,其他都占用一个,当进入了一个方法时,这个方法在堆帧中所需的局部变量空间是确定的,运行期间不会改变其局部变量空间的大小,也就是变量槽的槽数

在java虚拟机规范中,对java虚拟机栈规定了两类异常状况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError,如果Java虚拟机容量可以动态扩展,当栈扩展时无法申请到足够内存会抛出OutOfMemoryError异常

TIPS:注意到,我们目前广泛使用的HotSpot虚拟机的栈容量是不可以动态扩展的,也就是该虚拟机上不会因为无法扩展而导致OOM——只要线程申请栈空间成功了就不会有OOM,但是如果申请时候就失败,仍然会出现OOM

本地方法栈(Native Method Stacks)

本地方法栈与虚拟机栈的作用非常相似,区别就是本地方法栈是为虚拟机使用到的本地方法服务,而虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务,同时,与虚拟机栈一样,本地方法站也会抛出StackOverflowError与OutOfMemoryError

Java堆(Java Heap)

Java堆是虚拟机所管理内存中最大的一块,被线程共享,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,java虚拟机规范中提到所有的对象实例及数组都应该在堆上分配,但是在现实开发中由于标量替换优化手段等一系列操作,java对象实例分配在堆上也不是那么绝对了

Java堆是垃圾收集器管理的内存区域,因此也被叫做GC堆

分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(TLAB)来提高对象分配的效率,不过无论怎么划分,存储的都只能是对象的实例,进行划分只是为了更好的回收内存或者更好的分配内存

根据Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间,但在逻辑上应该被视作连续的(参考磁盘空间存储文件,不要求文件连续存放),注意到,对于大对象(如数组对象),多数虚拟机可能出于实现简单、存储高效角度看,可能会要求连续的内存空间

Java堆可以被实现成固定大小,也可以是可扩展的,目前主流Java虚拟机都是按照可扩展来实现(通过参数-Xmx与-Xms设定)

如果在Java堆中没有内存完成实例分配,并且堆也无法扩展时就会抛出OOM

方法区(Method Area)

方法区与Java堆一样,是各个线程共享的内存空间,用于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据,Java虚拟机规范中把方法区描述为堆的一个逻辑部分,但是方法区又被称为“非堆”,用于与Java堆区分

注意到方法区与永久代本质上不等价,是因为HotSpot虚拟机选择把收集器的分代设计扩展到方法区,或者说用永久代来实现方法区,使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去为方法区专门编写内存管理的工作,在JDK8,完全废除了永久代的概念,改用在本地内存实现的元空间来替代

如果方法区无法满足新的内存分配需求,会抛出OOM

运行时常量池(Runtime Constant Pool)

  • 是方法区的一部分,当常量池无法再申请到内存时会抛出OOM