JVM 运行时数据区:程序计数器、Java 虚拟机栈和本地方法栈,方法区、堆以及直接内存
Java 虚拟机可以看作一台抽象的计算机,如同真实的计算机,它也有自己的指令集和运行时内存区域。
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存(运行时内存区域)划分为若干个不同的数据区域。
如下图所示:
一、程序计数器 Program Counter Register
1.定义:程序计数器是当前线程所执行字节码的行号指示器。
2.线程私有内存的原因:Java 中的多线程是线程间轮流切换并需要 CPU 给予时间片的方式实现的。在任何一个确定的时刻,都只有一个线程在执行指令。为了线程间轮流切换后能够快速恢复到正确执行的位置,每一个线程都有自己的程序计数器,各线程间程序计数器互不影响,独立存在。因此,程序计数器是 “线程私有” 的内存区域。
3.详解:在当前线程执行 Java 指令时,程序计数器中存储的是正在执行的字节码的地址;而当前线程执行 native 方法时,程序计数器存储值为空。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器来完成。
4.可能导致的错误:程序计数器是 Java 运行时内存区域中唯一一个不会抛出 OutOfMemoryError (OOM)情况的内存区域。
二、Java 虚拟机栈 JVM Stacks
1.定义:Java 虚拟机栈描述的是 Java 方法执行的内存模型。
2.线程私有内存的原因:在当前线程执行方法时,Java 虚拟机栈会创建一个对应该方法的栈帧(Stack Frame)。栈帧中有 局部变量表、操作数栈、动态链接和方法出口等信息。
每一个方法从调用到执行完成时,都对应着栈帧从入栈到出栈的过程。
3.详解:局部变量表存放了各种编译器可知的基本数据类型、对象引用类型和 returnAddress 类型。局部变量表大小在编译期就已确定了,当进入一个方法时,栈帧需要分配给局部变量表的内存空间早已固定,不会再做更改。
(关于 JVM Stacks 的详细情况可能放在 本系列 JVM 执行引擎 一文 中)
4.可能抛出的错误:
1.当请求的栈深度超越规定的栈深度时,便会抛出 StackOverflowError (sof) 异常
2.虚拟机栈动态扩展时,如果无法申请到足够的内存,抛出 OOM 异常
三、本地方法栈
1. 定义:本地方法栈描述的是运行时 native 方法的内存模型。
2.解释:本地方法栈与 Java 虚拟机栈几乎相同,只不过本地方法栈对应的是本地方法,Java 虚拟机栈对应的是 Java 方法。
Hotspot 虚拟机将 java 虚拟机栈与本地方法栈合二为一。
3.可能抛出的异常同 Java 虚拟机栈一样(SOF,OOM)
本地方法的解释:Java 本地方法
四、Java 堆
1. 定义:Java 堆是一块用来存放对象实例的运行时内存空间。
2.Java 堆唯一目的就是用来存放对象实例。几乎所有的对象实例(包括数组)都会在这里分配内存。属于“线程共享"的内存区域。
Java 堆是垃圾回收器管理的主要区域。Java 虚拟机可以处于物理上的不连续内存空间中,只要逻辑上是连续的内存空间即可。
java 堆是垃圾回收的主要区域
3.可能抛出的异常:
如果堆中没有内存完成实例分配,并且堆再也无法扩展时,那么会抛出 OOM 异常
五、方法区
1.定义:方法区用于存储已被虚拟机加载的类的结构信息、常量、静态变量、即时编译器编译后的静态代码等数据。
方法区是线程共享的内存区域。
2.解释:在 Hotspot 虚拟机上,方法区采用永久代的方式实现,因此方法区又被称为永久代(Permanent Generation),实际上这两者并不等价。因为这样实现的方法区不仅容易遇到内存溢出的问题,而且极少数方法(String.intern) 也会在不同的虚拟机上有不同的表现。
Hotspot 虚拟机现在也有放弃永久代而采用 Native Memory 来实现方法区的规划了(在 JDK1.7 中,将放在永久代的字符串常量池移出)。
对于垃圾回收而言,方法区主要集中在常量池的回收和类类型的卸载这两方面。但是方法区的垃圾回收效果可谓是差强人意,特别是类类型的卸载,不过这部分类型的回收又是必须的。
3. 可能抛出的异常:
方法区如果无法满足内存分配需求时,将会抛出 OOM 异常。
5.1 方法区重要组成部分: 运行时常量池 Runtime Constant Pool
1.定义:运行时常量池是 class 文件中每一个类或接口的常量池表的运行时表现形式,包括若干种不同的常量,从编译期可知的数值字面量到必须在运行期解析后才能获得的方法和字段引用。
每一个运行时常量池都在 Java 虚拟机的方法区中分配,在加载类和接口到虚拟机后,就将相应的常量放入运行时常量池。
2.可能抛出的异常:
当常量池无法再申请到内存时会抛出 OOM 异常
3.例子:
1 public class A { 2 public static void main(String[] args) { 3 String a = "abc"; 4 String b = "abc"; 5 System.out.println(a==b); 6 String c = new String("abc"); 7 System.out.println(a==c); 8 System.out.println(a==c.intern()); 9 } 10 }
六、直接内存 Direct Memory
在 JDK1.4 中新加入了 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓冲区(Buffer) 的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,坑定还是受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OOM 的异常。