运行时数据区
运行时数据区用于保存 JVM 在运行过程中产生的数据,结构如图所示:
Heap
Java 堆是可供各线程共享的运行时内存区域,是 Java 虚拟机所管理的内存区域中最大的一块。此区域非常重要,几乎所有的对象实例和数组实例都要在 Java 堆上分配,但随着 JIT 编译器及逃逸分析技术的发展,也可能会被优化为栈上分配。
Heap 中除了作为对象分配使用,还包含字符串字面量 常量池(Internd Strings) 。 除此之外 Heap 中还包含一个 新生代(Yong Generation)、一个 老年代(Old Generation)。
新生代分三个区,一个Eden区,两个Survivor区,大部分对象在Eden区中生成。Survivor 区总有一个是空的。
老年代中保存一些生命周期较长的对象,当一个对象经过多次的 GC 后还没有被回收,那么它将被移动到老年代。
Methoad Area
方法区的数据由所有线程共享,因此为安全的使用方法区的数据,需要注意线程安全问题。
方法区主要保存类级别的数据,包括:
- ClassLoader Reference
- Runtime Constant Pool
- 数字常量
- 类属性引用
- 方法引用
- Field Data:每个类属性的名称、类型等
- Methoad Data:每个方法的名称、返回值类型、参数列表等
- Methoad Code:每个方法的字节码、本地变量表等
方法区的实现在不同的 JVM 版本有不同,在 JVM 1.8 之前,方法区的实现为 永久代(PermGen),但是由于永久代的大小限制, 经常会出现内存溢出。于是在 JVM 1.8 方法区的实现改为 元空间(Metaspace),元空间是在 Native 的一块内存空间。
Stack
对于每个 JVM 线程,当线程启动时,都会分配一个独立的运行时栈,用以保存方法调用。每个方法调用,都会在栈顶增加一个栈帧(Stack Frame)。
每个栈帧都保存三个引用:本地变量表(Local Variable Array)、 操作数栈(Operand Stack) 和 当前方法所属类的运行时常量池(Runtime Constant Pool)。由于本地变量表和操作数栈的大小都在编译时确定,所以栈帧的大小是固定的。
当被调用的方法返回或抛出异常,栈帧会被弹出。在抛出异常时 printStackTrace()
打印的每一行就是一个栈帧。同时得益于栈帧的特点,栈帧内的数据是线程安全的。
栈的大小可以动态扩展,但是如果一个线程需要的栈大小超过了允许的大小,就会抛出 StackOverflowError
。
PC Register
对于每个 JVM 线程,当线程启动时,都会有一个独立的 PC(Program Counter) 计数器,用来保存当前执行的代码地址(方法区中的内存地址)。如果当前方法是 Native 方法,PC 的值为 NULL。一旦执行完成,PC 计数器会被更新为下一个需要执行代码的地址。
Native Method Stack
本地方法栈和 Java 虚拟机栈的作用相似,Java 虚拟机栈执行的是字节码,而本地方法栈执行的是 native
方法。本地方法栈使用传统的栈(C Stack)来支持 native
方法。
Direct Memory
在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为 避免了在 Java 堆和 Native 堆中来回复制数据。