运行时数据区域
Java虚拟机在运行过程中会将其所管理的内存区域划分为多个不同区域,便于管理程序运行过程中的对象、方法等的创建执行和销毁。根据《Java虚拟机规范》规定,Java虚拟机的内存分为以下几种运行时区域:
其中方法区、堆区为线程共享的区域,其生命周期与虚拟机相同;虚拟机栈、本地方法栈与程序计数器为线程私有的区域,跟随线程创建、销毁。
程序计数器
程序计数器是一个较小的内存空间,代表线程当前正在执行的字节码的行号指示器,是虚拟机的概念模型,各虚拟机有自己的实现。字节码解释器工作时通过修改计数器的值找到下一条要执行的指令。
每个线程都有一个程序计数器,便于线程切换时能恢复正在执行的指令。
线程执行普通Java方法时,计数器记录的是正在执行的字节码指令地址;执行Native方法时,计数器值为空(Undefined)。
该区域不会发生OutOfMemoryError
Java虚拟机栈
虚拟机栈就是我们平常说的“堆”、“栈”中的“栈”,每个线程拥有一个虚拟机栈,用于存储方法的运行信息,程序每执行一个方法就会创建一个栈帧压入虚拟机栈中,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息。方法执行完就将栈帧弹出栈。
局部变量表用于存放编译器可知的基本数据类型、对象引用和returnAddress类型。
虚拟机栈可能出现StackOverflowError和OutOfMemoryError。
本地方法栈
类似于虚拟机栈,不过本地方法栈用于执行Native方法,Native方法用native关键字标识,没有方法体,使用C编写。
本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError异常。
Java堆
堆区主要用于存放对象实例,是Jvm管理的最大的一块内存区域,为线程共享,生命周期于虚拟机相同。堆区是垃圾收起器管理的主要区域,从对象回收角度,堆区被划分为新生代和老年代,新生代又分为Eden区、From Survivor区和To Survivor区。从内存分配角度,堆区可划分出线程私有的分配缓冲区(TLAB)。
通过 -Xms 和 -Xmx 参数可以设置堆区的初始内存和最大内存大小,会出现OutOfMemoryError异常。
方法区
线程共享的区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据(虽然叫方法区,但不是存储方法的区域)。
方法区无法满足内存分配需求时会出现OutOfMemoryError异常。
JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。
运行时常量池
运行时常量池(runtime constant pool)是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中。
一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相较于Class文件常量池的体征就是动态性,在运行时也可以将新的常量放入池中,如String类的intern()方法。
会出现OutOfMemoryError异常。
字面量:
- 文本字符串
- 8种基本类型的值
- 被声明为final的常量
符号引用:
- 类和方法的权限定名
- 字段的名称和描述符
- 方法的名称和描述符
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。