以jdk1.8为例
1. Java 虚拟机规范
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域。它们各有用途,有些随着虚拟机进程的启动一直存在(堆、方法区),有些则随着用户线程的启动和结束而建立和销毁(程序计数器、虚拟机栈、本地方法栈)。
《Java 虚拟机规范》中规定 Java 虚拟机管理的内存包括以下几个区域:
1.1 程序计数器
程序计数器(Program Counter Register),可以看做当前线程所执行的字节码的行号指示器(其实就是记录代码执行到了哪里)。特点如下:
- 线程私有;
- 占用内存空间较小;
- 若线程执行的是 Java 方法,记录的是虚拟机字节码指令地址;若执行的是本地(Native)方法,则为空(Undefined);
- 该区域是唯一一个在《Java 虚拟机规范》中规定无任何 OutOfMemoryError(内存溢出) 的区域。
主要作用:记录线程执行到了哪里。
1.2 Java 虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stacks):Java 方法执行的线程内存模型。
每个方法被执行时,虚拟机栈都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法从被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。其中局部变量表包括:
- Java 虚拟机基本数据类型(8 种)
- 对象引用(reference 类型,可能是一个指向对象起始地址的指针)
- returnAddress
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)表示,其中 long 和 double 占用两个槽,其他类型占用一个槽。局部变量表所需内存空间在编译期完成分配,当进入一个方法时,该方法需要在栈帧中分配多大的局部变量空间是完全确定的,运行期间不会改变其大小。
虚拟机栈的特点:
- 线程私有;
- 生命周期与线程相同;
- 两类异常
- 线程请求的栈深度大于虚拟机所允许的深度时抛出 StackOverflowError 异常;
- 栈扩展时无法申请到足够的内存时抛出 OutOfMemoryError (内存溢出)异常。
主要目的:Java 方法执行的线程内存模型。
1.3 本地方法栈
本地方法栈(Native Method Stacks)与 Java 虚拟机栈作用类似。二者区别:
- Java 虚拟机栈为 JVM 执行 Java 方法(字节码)服务;
- 本地方法栈为 JVM 使用到的本地(Native)方法服务。
异常与 Java 虚拟机栈相同。
主要目的:Native 方法执行的线程内存模型。
1.4 Java 堆
对多数应用来说,Java 堆(Java Heap)是 JVM 管理的内存中最大的一块。
唯一目的:存放对象实例(【几乎所有】的对象实例都在这里分配内存)。
《Java 虚拟机规范》描述:所有对象实例及数组都应在堆上分配。
而从实现角度看,由于即使编译技术(尤其是逃逸分析技术的日渐强大),"栈上分配"等手段使得对象并非完全在堆上分配。
特点:
- 线程共享
- 虚拟机启动时创建
PS: “新生代”、“老年代”、"Eden 区"等一系列对堆的区域划分,只是部分垃圾收集器的一些共性或设计风格,而非虚拟机的固有内存布局,更非《Java 虚拟机规范》的划分。
将 Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
1.5 方法区
方法区(Method Area):用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,该区域也是线程共享的。又称"非堆"。
1.6 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
Class 文件中除了有类的版本、字段、方法、接口等描述外信息,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
相比于 Class 文件常量池的一个重要特性是「动态性」,运行期间也可以将新的常量放入池中(例如 String 类的 intern() 方法)。
可能产生的异常:OutOfMemoryError。