JVM 运行时内存数据区域
Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
一、程序计数器(Program Counter Register)
- 《 JVM 虚拟机规范(Java SE 8版)》所定义的程序计数器
Java 虚拟机可以支持多条线程同时执行,每一条 Java 虚拟机线程都有自己的 PC (program counter)寄存器。在任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法(current method)。
如果这个方法不是 native 的,那 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值就是 undefined。PC 寄存器的容量至少应当能保存一个 returnAddress 类型的数据或者一个与平台相关的本地指针的值。
- 《 深入理解 Java 虚拟机(第三版)》 讲述的程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在 “ Java 虚拟机的概念模型” 里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器的完成。
Java 虚拟机的概念模型:概念模型这个词会经常被提及,它代表了所有虚拟机的统一外观,但各款具体的 Java 虚拟机并不一定要完全照着概念模型的定义进行设计,可能会通过一些更高效率的等价方式进行实现它。
由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存。
如果这个线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(native)方法,这个计数器值则应为空(undefined)。此内存区域是唯一一个在 《Java 虚拟机规范》 中没有规定任何 OutOfMemoryError 情况的区域。
- 我的理解
程序计数器是 JVM 运行时内存区域的一小块内存空间。
它可以当作当前线程所执行的字节码的行号指示器。
它是程序控制流的指示器。
线程私有,每一个线程都有一个程序计数器。
二、虚拟机栈 (VM Stack)
- 《 JVM 虚拟机规范(Java SE 8版)》定义的 Java 虚拟机栈
每一条 Java 虚拟机线程都有自己私有的 Java 虚拟机栈(Java Virtual Matchine stack),这个栈与线程同时创建,用于存储栈帧(Frame)。Java 虚拟机栈的作用于传统语言(例如 C 语言)中的栈非常类似,用于存储局部变量与一些尚未算好的结果。另外,它在方法调用和返回中也扮演了很重要的角色,因为除了栈帧的出栈和入栈之外,Java 虚拟机栈不会再受其他因素的影响,所以 “栈帧可以在堆中分配”,Java 虚拟机栈所使用的内存不需要保证是连续的。
“栈帧可以在堆中分配”:请注意避免混淆 Stack、Heap 和 Java(VM)Stack、Java Heap的概念,Java 虚拟机的实现本质上是由其他语言所编写的应用程序,Java 语言程序分配在 Java Stack 中的数据,从虚拟机的程序角度上看则可能分配在 Heap中。
《 Java 虚拟机规范 》第一版中,Java 虚拟机栈也称为 “Java 栈”。
Java 虚拟机规范既允许 Java 虚拟机栈被实现成固定大小,也允许根据计算动态来扩展和收缩。如果采用固定大小的虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。
Java 虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩 Java 虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
Java 虚拟机栈可能发生如下异常情况:
1. 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
2. 如果 Java 虚拟机栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
- 《 深入理解 Java 虚拟机(第三版)》 讲述的 Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈(Java Virtual Matchine stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在《 Java 虚拟机规范 》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的栈深度,将抛出 StackOverflowError 异常;如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。
-
我的理解
Java 虚拟机栈是线程私有的,生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型,每个方法执行的时候,就会创建一个栈帧,方法的调用到执行完毕,对应到虚拟机栈就是一个栈帧出栈到入栈的过程。此内存区域会出现两种异常状况:
一、栈深度溢出:StackOverflowError 异常
二、栈扩展失败: OutOfMemoryError 异常
三、本地方法栈 (Native Method Stack)
- 《 JVM 虚拟机规范(Java SE 8版)》定义的本地方法栈
Java 虚拟机实现可能会使用到传统的栈(通常称为 C stack)来支持 native 方法(指使用 Java 以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(native method stack)。当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,也可以使用本地方法栈。如果 Java 虚拟机不支持 native 方法,或是本身不依赖传统栈,那么可以不提供本地方法栈,如果支持本地方法栈,那么这个栈一般会在线程创建的时候按线程分配。
Java 虚拟机规范允许本地方法栈实现成固定大小或者根据计算来动态扩展和收缩。如果采用固定大小的本地方法栈,那么每一个线程的本地方法栈容量可以再创建栈的时候独立选定。
Java 虚拟机实现应当提供给程序员或者最终用户调节本地方法栈初始容量的手段,对于长度可动态变化的本地方法栈来说,则应当提供调节其最大、最小容量的手段。
Java 虚拟机栈可能发生如下异常情况:
- 如果线程请求分配的栈容量超过 本地方法栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
- 如果本地方法栈可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
- 《 深入理解 Java 虚拟机(第三版)》 讲述的本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
《 Java 虚拟机规范 》对于本地方法栈中实现的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的 Java 虚拟机(譬如 Hot Spot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常
- 我的理解
作用与 Java 虚拟机栈相同,不同之处是本地方法栈用来执行本地方法的,如果不支持本地方法或不依赖传统栈,这个内存区域可以不实现。Hot Spot 虚拟机的实现方式是,合并到 Java 虚拟机栈里了。
四、堆 (Heap)
- 《 JVM 虚拟机规范(Java SE 8版)》定义的 Java 堆
在 Java 虚拟机中,堆(Heap)是供各个线程共享的运行时内存区域,也是供所有类的实例和数组对象分配内存的区域。
Java 堆在虚拟机启动的时候就被创建,它存储了自动内存管理系统(automatic storage management system, 也就是常说的 garbage collector(垃圾收集器))所管理的各种对象,这些受管理的对象无需也无法显式地销毁。本规范所描述的 Java 虚拟机并未假设采用何种具体技术区实现自动内存管理系统。虚拟机实现者可以根据系统的实际需要来选择自动内存管理技术。Java 堆的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java 堆所使用的内存不需要保证是连续的。
Java 虚拟机实现应当提供给程序员或者最终用户调节 Java 堆初始容量的手段,对于可以动态扩展和收缩 Java堆来说,则应当提供调节其最大、最小容量的手段。
Java 堆可能发生如下异常情况:
- 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那么 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
- 《 深入理解 Java 虚拟机(第三版)》 讲述的 Java 堆
对于 Java 应用程序来说,Java 堆(Java Heap)是虚拟机所管理内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里 “几乎” 所有的对象实例都在这里分配内存。在 《 Java 虚拟机规范 》中对 Java 堆的描述是:“所有的对象实例以及数组都应当在堆上分配” ,而这里笔者 “几乎” 是指从实现的角度来看,随着 Java 语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即时只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说 Java 对象实例都分配在堆上也渐渐变得不是那么绝对了。
根据《 Java 虚拟机规范 》的规定,Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。
Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 Java 虚拟机都是按照可扩展来实现的(通过参数 -Xmx 和 -Xms 设定)。如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。
-
我的理解
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域唯一目的就是存放对象实例。所有的对象实例以及数组都应当在堆上分配。(虚拟机规范定义),此区域是垃圾收集器管理的内存区域。
五、方法区(Method Area)
- 《 JVM 虚拟机规范(Java SE 8版)》定义的方法区
在 Java 虚拟机中,方法区(method area)是可供各个线程共享的运行时内存区域。方法区于传统语言中的编译代码存储器(storage area for compiled code)或者操作系统进程的正文段(text segment)的作用非常类似,它存储了每一个类的结构信息,例如,运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集与压缩。这个版本的 Java 虚拟机规范与不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。
Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。
方法区可能发生如下异常情况:
- 如果方法区的内存空间不能满足内存分配请求,那么 Java 虚拟机将抛出一个 OutOfMemoryError 异常。
- 《 深入理解 Java 虚拟机(第三版)》 讲述的方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《 Java 虚拟机规范 》 中把方法区描述为堆的一个逻辑部分,按时它却又一个别名叫做 “非堆” (Non-Heap),目的是与 Java 堆区分开。
根据《 Java 虚拟机规范 》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
-
我的理解
方法区是被所有线程共享的一块内存区域,用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。方法区在虚拟器启动时创建,《Java 虚拟机规范 (Java SE 8)》不限定实现方法区的内存位置,和编译代码的管理策略,也就是说可以这块区域可以不实现垃圾回收策略。如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
六、运行时常量池(Runtime Constant Pool)
- 《 JVM 虚拟机规范(Java SE 8版)》定义的运行时常量池
运行时常量池(runtime constant pool)是 class 文件中每一个类或接口的常量池表(constant_pool table)的运行时表示形式,它包括了若干种不同的常量,从编译器可知的数值字面量到必须在运行期解析后才能获得的方法或字段引用。运行时常量池类似于传统语言中的符号表(symbol table),不过它存储数据的范围比通常意义上的符号表更为广泛。
每一个运行时常量池都在 Java 虚拟机的方法区中分配,在加载类和接口到虚拟机后,就创建对应的运行时常量池。
在创建类和接口时,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那么 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
- 《 深入理解 Java 虚拟机(第三版)》 讲述的运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
-
我的理解
运行是常量池位于方法区内当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
保存的数据是 class 文件中编译后的常量池表的内容(各种字面量和符号引用)。