Java 虚拟机完整结构包括类加载器、运行时数据区、执行引擎及本地接口,其中运行时数据区对jvm性能优化、内存问题排查时非常重要。本文主要是讲解虚拟机内存中各区域的作用、服务对象及其可能产生的问题。
Java 虚拟机在执行Java程序时把其所管理的内存划分为几个不同的数据区域,这些区域有的随虚拟机进程的启动而创建,有些则依赖用户线程的启动、结束而建立、销毁,在 Java SE 1.7 版本中,虚拟机内存包括以下几个运行时数据区域:
从图中可以了解到,虚拟机内存包括方法区、堆、虚拟机栈、本地方法栈及程序计数器,其中方法区和堆是所有线程共享的数据区。
1.1 方法区
- 线程共享
- 存储
- 类信息
- 常量
- 静态变量
- 方法字节码
方法区用于存储虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据,当方法区无法满足满足内存分配需求时,将抛出 OutOfMemoryError 异常。
对基于 HotSpot 虚拟机上开发部署程序的开发者来说,通常把方法区称为“永久代”,永久代是HotSpot虚拟机特有的概念,是对方法区的实现(别的JVM没有永久代的概念),这样 HotSpot 虚拟机的垃圾收集器可以像管理 Java 堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。
在JDK8中,HotSpot VM已经是以前的HotSpot VM与JRockit VM的合并版,并将永久代换成元空间, 一方面是节省空间,避免了常见的永久内存错误 java.lang.OutOfMemoryError: PermGen问题;一方面是为了整合JRockit,因为JRockit没有永代区这样类似的空间。
原来存储在永久代中的字符串常量(池)、符号引用(这两个在jdk7普遍就已经将其放在堆上了)和类的静态变量现在存储在java堆中,其余的数据作为元数据存储在元空间中。
原句:Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.
注:元空间是一块与堆内存不相连的本地内存。
1.1.1 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用。
运行时常量池相对于 Class 文件常量池的另外一个重要特性是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入常量池中,如 String 类的 intern() 方法。
1.2 堆内存
- 线程共享
- 存储对象或数组
堆(Java Heap)是 Java 虚拟机内存中最大的一块,其被所有线程共享,在虚拟机启动时创建。此内存区的唯一目的就是存放对象实例。在 Java 虚拟机规范中的描述是:所有的对象实例及数组都在堆内存上分配。
当前主流的虚拟机都是按照可扩展大小的方式(相对于固定大小)来实现堆内存的(通过 -Xmx、-Xms 控制),若在堆中没有足够的内存完成新建实例的内存分配,并且也无法再扩展时,将会抛出 OutOfMemoryError 异常。
1.3 栈
Java 虚拟机栈是线程私有的,其生命周期和线程的相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用至执行结束的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在 Java 虚拟机规范中,对该区域规定了两种异常状况:
1)若线程请求的栈深度大于虚拟机所允许的深度,抛出 StackOverflowError 异常;
2)若虚拟机栈可以动态扩展,但扩展时仍无法申请到足够的内存,抛出 OutOfMemoryError 异常。
不同的栈内存对应不同的栈深度,测试如下:
/**
* @ClassName: StackOverflowExceptionTest
* @Description: 栈测试
*/
public class StackOverflowExceptionTest {
private static int index = 1;
private void stackTest() {
index++;
stackTest();
}
public static void main(String[] args) {
StackOverflowExceptionTest soe = new StackOverflowExceptionTest();
try {
soe.stackTest();
} catch (Throwable e) {
System.out.println("stack deep: " + index);
e.printStackTrace();
}
}
当前栈内存大小设置为128k,运行结果如下:
此时的 -Xss 为128k,设置为1024k 时 stack deep: 24525
当前程序的临时虚拟机参数设置可在main方法上点击右键 -> Run as -> Run Configurations 对运行参数进行配置如下:
1.4 本地方法栈
本地方法栈为虚拟机调用本地(Native)方法服务,虚拟机规范没有对本地方法栈中的方法实现语言、数据结构做约定,有的虚拟机(如 HotSpot VM)直接把本地方法栈和虚拟机栈合并统一管理。
与虚拟机栈一样,本地方法栈区也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
1.5 程序计数器
程序计数器是一块较小的内存空间,用于当前线程所执行的字节码的行号指示器。
作用
当前线程执行的字节码的行号指示器,通过改变此指示器来选取下一个需要执行的字节码指令特征
在线程创建时创建
每个线程拥有一个
指向下一条指令的地址
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(多核中对应一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程间的计数器互不影响,独立存储,这类内存区域也称为“线程私有”内存。
若线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
若执行的是 Native 方法,这个计数器值则为 Undefined。
该区域没有规定任何 OutOfMemoryError 异常。
参考资料:
深入理解Java虚拟机:JVM高级特性与最佳实践