深入理解JVM虚拟机-运行时数据区域

Java虚拟机定义了程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会被销毁。其他数据区域是每个线程。每线程数据区域在创建线程时创建,并在线程退出时销毁。
运行时数据区域

PC寄存器/程序计数器(Program Counter Register)

Java虚拟机可以同时支持许多执行线程。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。如果该方法不是native,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前执行的方法是natice的,则Java虚拟机的pc寄存器的值未定义。Java虚拟机的pc寄存器足够宽,可以在特定平台上保存returnAddress或本机指针。
此内存区域是唯一在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

通俗来讲

每个线程都有一个独立的程序计数器,各线程之间互不影响,我们称之为线程私有。
当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令。

Java虚拟机栈(Java Virtual Machine Stacks)

线程私有,生命周期与线程相同。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接方法出口等信息。
局部变量表存放了编译器可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象其实地址的引用指针,也可能是指向一个代表对象的句柄或者与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64为长度的long和double类型的数据会占用两个变量槽,其余数据类型 只占用一个。局部变量表所需的内存空间再编译期间完成分配,进入一个方法时,这个方法在栈帧中需要分配多大的局部变量表空间是完全确定的,运行时不会发生改变。注意大小指槽的数量,槽的大小由具体的虚拟机实现自行决定。
在《Java虚拟机规范》中,对这个区域定义了两类异常状况:
1. 如果线程中的计算需要比允许的更大的Java虚拟机堆栈,Java虚拟机会抛出StackOverflowError
2. 如果Java虚拟机堆栈(Classic虚拟机)可以动态扩展,并尝试扩展,但没有足够的内存来进行扩展,或者如果没有足够的内存来为新线程创建初始Java虚拟机堆栈,Java虚拟机会抛出OutOfMemoryError

本地方法栈(Natice Method Stacks)

基本与Java虚拟机栈相同,线程私有,每个线程分配一个本地方法栈,异常情况等。

堆(Heap)

虚拟机有一个堆,所有线程共享。分配所有类实例和数组内存的运行时数据区域。
堆是在虚拟机启动时创建的。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会显式处理。Java虚拟机没有特定类型的自动存储管理系统,可以根据实现者的系统要求选择存储管理技术。堆可以是固定大小的,也可以根据计算的要求进行扩展,如果不需要更大的堆,则可以收缩。堆的记忆不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对堆初始大小的控制,如果堆可以动态扩展或收缩,则可以控制最大和最小堆大小(通过参数-Xmx和-Xms设定)。
异常情况:
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
与之关联知识
新生代、老年代、永久代、Eden和Survivor等。
各种垃圾回收器。

方法区(Method Area)

方法区与堆一样属于线程共享。存储每个类结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和接口初始化以及实例初始化中使用的特殊方法。它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
方法区域是在虚拟机启动时创建的。虽然方法区域在逻辑上是堆的一部分,但可以选择不垃圾收集或压缩它。不强制要求方法区域的位置或用于管理编译代码的策略。方法区域可以是固定大小的,也可以根据计算要求进行扩展,如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。
Java虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,以及在不同大小的方法区域的情况下,对最大和最小方法区域大小的控制(-XX:MaxPermSize)。
异常情况:
如果方法区域的内存无法满足分配请求,Java虚拟机会抛出OutOfMemoryError。

运行时常量池(Run-Time Constant Pool)

_运行时常量池_是class文件中constant_pool表的每类或每个接口运行时表示。它包含几种常量,从编译时已知的数字文字到运行时必须解析的方法和字段引用。运行时常量池的函数类似于传统编程语言的符号表,尽管它包含的数据范围比典型的符号表更广。

每个运行时常量池都从Java虚拟机的方法区域分配。类或接口的运行时常量池是在Java虚拟机创建类或接口时构建的。
异常情况:
在创建类或接口时,如果构建运行时常量池需要比Java虚拟机方法区域可用的内存更多,则Java虚拟机会抛出OutOfMemoryError。

直接内存(Direct Memory)

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据(零拷贝???)。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

The Java Virtual Machine Specification, Java SE 8 Edition
《深入理解Java虚拟机(第3版)》-- 周志明 著


版权声明:本文为nieyanbin原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。