- 运行时数据区域
- Java 内存模型
- 参考
运行时数据区域
程序计数器
程序计数器可以看做当前线程所执行字节码的行号指示器。
字节码解释器通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器完成。
每条线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。这一类的内存区域为“线程私有”的内存。
程序计数器内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
虚拟机栈
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
直接把 Java 内存区分为堆内存和栈内存是不准确的,Java 内存区域的划分远远比这复杂。只能说大多数程序员最关注的,与对象内存分配最密切的内存区域是这两块。所谓的“堆”指的是 Java 堆,而“栈”对应的是虚拟机栈。
局部变量表存放了编译期可知的各种基本数据类型(boolean, byte, char, short, int, float, long, double),对象引用(reference 类型,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。
局部变量表所需的内存空间在编译期间完成分配。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机允许动态扩展,扩展时无法申请到足够内存,就会抛出 OutOfMemoryError 异常。
本地方法栈
本地方法栈和虚拟机栈的作用是非常相似的,区别是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈为虚拟机使用到的 Native 方法服务。
本地方法栈也会抛出 StackOverflowError 异常和 OutOfMemoryError 异常。
Java 堆
对于大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块。
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。该内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被成为“GC 堆”。由于现在的垃圾收集器基本都采用分代收集算法,所以 Java 堆还可以细分为:新生代和老年代,再细致一点的有 Eden 空间,From Survivor 空间,To Survivor 空间。
Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实力分配,并且堆也无法再扩展,将会抛出 OutOfMemoryError 异常。
为什么栈比堆快?
分配和释放:栈(线程私有区)是在编译时系统自动分配空间,堆是动态分配(运行时分配空间),申请和交还空间开销较大。
访问方式:访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是访问真正的数据。栈有专门的寄存器,压栈和出栈的指令效率很高,且只需要访问一次。
调度:堆需要由 OS 动态调度,堆内存可能被 OS 调度在非物理内存中,或是申请内存不连续,造成碎片过多等问题。
方法区
方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。因为常量池中存放的是不会被轻易改变的内容,所以被称为永久代。
当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
字符串常量池
JDK1.7之前,字符串常量池在永久代中。
JDK1.7中,存储在永久代的部分数据就已经转移到 Java 堆中,但永久代仍存在于JDK 1.7中,并没有完全移除。例如,字符串常量池和类的静态变量都转移到了 Java 堆中。
JDK1.8中,取消了永久代, 其余的数据作为元数据存储在元空间中存放于元空间, 元空间是一块与堆不相连的本地内存。
运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
当常量池无法再申请内存时会抛出 OutOfMemoryError 异常。
运行时常量池与 Class 文件常量池
JVM对Class文件中每一部分的格式都有严格的要求,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行;但运行时常量池没有这些限制,除了保存Class文件中描述的符号引用,还会把翻译出来的直接引用也存储在运行时常量区。
相较于Class文件常量池,运行时常量池更具动态性,在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法。
Java 内存模型(Java Memory Model)
所有变量都存储在主内存中,每条线程有自己工作内存,工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存完成。
read-load 和 store-write必须按顺序执行(不一定连续执行),不允许单独出现。
参考
- 周志明. 深入理解 Java 虚拟机: Java 高级特性与最佳实践(第2版)[M]. 机械工业出版社. 2013.
- 【Java虚拟机】《深入理解Java虚拟机》| Java内存模型以及内存溢出异常
- JVM-String常量池与运行时常量池