JVM 初窥(一) JVM内存区域划分
Java 虚拟机(下文简称JVM)在运行时,会将其管理的内存划分成几个部分。本文将介绍 JVM 是如何划分内存区域,每个区域服务于哪些对象,作用是什么。
运行时数据区域
根据《Java虚拟机规范》,将JVM内存区域划分为如下图所示的几块。
1.程序计数器(Program Counter Register)
程序计数器相对于其他JVM内存区域占用较少的内存空间,它相当于是当前线程执行字节码的行号指示器。JVM中的多线程会根据处理器分配的时间片轮流占用处理器执行程序代码,在线程切换后,为了回到执行的正确位置,每个线程都必须有一个独占的程序计数器。这里的独占指各个线程的程序计数器互不影响,数据独立存储,所以该块内存区域是线程隔离的内存。
虚拟机规范规定,如果线程执行的是一个Java方法,则程序计数器记录的是正在执行的虚拟机字节码指令的地址,JVM是把class文件转化成(通过解释器或JIT编译器)字节码去执行的;如果执行的是一个native方法,则这个计数器记录的值为undefined。
该区域是唯一一个不会发生OOM(内存溢出)的内存区域。
2.虚拟机栈(VM Stack)
虚拟机栈又经常被称为Java栈,是Java方法执行的内存模型,它也是线程隔离的,不同的线程都有自己的Java栈。在线程执行Java方法时,会在栈顶中创建一个栈帧(Stack Frame),栈帧中存放局部变量表,操作数栈,方法出口,运行池常量池的引用等数据。每一个 Java 方法从调用到执行结束的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
如果方法的调用链过深,超过了JVM所允许的栈最大深度,则会抛出 StackOverflowError 的异常。如果虚拟机栈动态扩展但无法申请到内存,则会抛出 OOM 异常。
3.本地方法栈(Native Method Stack)
本地方法栈和虚拟机栈类似,都是用于执行方法时在栈中记录方法栈帧信息的。只不过本地方法栈执行的是native方法,也因此叫做“本地”方法栈。
JVM 规范没有强制指定如何实现本地方法栈以及实用的方法,所以不同的 JVM 会有不同的实现。HotSpot 虚拟机是将虚拟机栈和本地方法合二为一来解决这个问题的。
本地方法栈也同虚拟机栈一样会抛出 StackOverflowError 和 OOM 异常。
4.Java 堆(Java Heap)
普遍来说,Java 堆是 JVM 内存区域中最大的一块区域,在 JVM 启动时创建,并且被所有线程共享。如同 JVM 规范的规定,所有的对象实例以及数组都在 Java 堆上分配。
Java 堆中,又可以细分为新生代和老年代,新生代又可细分为 Eden 空间,From Survivor 空间以及 To Survivor 空间。JVM 的垃圾回收(Garbage Collection)主要就是在这块内存区域中发生的,垃圾回收的分代收集就是针对于 Java 堆中不同的划分进行 GC 的。
Java 堆的大小可以在启动时通过参数 -Xmx 和 -Xms 指定。如果在Java 堆上分配对象时没有申请到足够的内存,则会抛出 OOM 异常。
5.方法区(Method Area)
方法区中存放了 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这块内存区域也是被各个线程共享的。方法区又被称为非堆(Non-Heap),用于与堆区分开。
当方法区无法满足内存分配需求时,将会抛出 OOM 异常。
常量池
- 静态常量池:指 class 文件中的常量池,包括字面量(文本字符串、final类型的常量),还包括类、方法的信息。
- 运行时常量池:JVM 在类加载后讲 class 文件中的常量池载入到堆内存中,方法区中保存了这些对象的引用。
永久代(PermGen) 和 元空间(Metaspace)
方法区其实说到底就是 JVM 规范中定义的一个概念,并没有规定具体要将方法区存放到什么位置。在 JDK6 和 JDK7 HotSpot 虚拟机的实现中,方法区是作为 Java 堆的一部分而存在的,被称为永久代,其大小通过 -XX:MaxPermSize 来指定。
但这样的实现是不合理的,一方面永久代运行时遇到的 GC 很少,另一方面永久代可能会因为内存不足抛出 OOM 的异常。所以在 JDK 8 的 HotSpot 虚拟机中,移除了永久代这部分空间,取而代之是元空间。元空间存储在本地内存中,这部分内存不属于 JVM 的内存区域,因此不会受限于 JVM 的内存限制。其大小可以通过 -XX:MaxMetaspaceSize 指定,若不指定这个参数,则会在运行时根据需求动态调整。
6.直接内存(Direct Memory)
直接内存不属于虚拟机运行时数据区的一部分,其内存的分配也不会受到 Java 堆大小的限制。但是,会受到计算机总内存大小以及处理器寻址空间的限制。如果虚拟机各个内存区域之和大于物理内存限制,也会导致 OOM 异常。
JDK 1.4 中加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,其可以使用 Native 方法直接分配堆外内存,然后通过引用访问这块内存进行操作,这样由于了避免在 Java 堆中来回复制数据,可以提高程序的性能。