Java 内存区域
- 运行时数据区(Runtime Data Area)
- 线程区域
- 程序计数器(Program Counter Register)
- 本地方法栈(Native Method Stack)
- Java 虚拟机栈(JVM Stack)
- 栈帧(Stack Frame)
- 局部变量表(Local Variable Array)
- 基本数据类型
- 对象引用(reference)
- 字节码指令地址(returnAddress)
- 操作数栈(Operand Stack)
- 常量池引用(Reference to Constant Pool)
- 动态链接
- 方法出口
- 共享区域
- 堆(Heap)
- 新生代(Young Generation)
- Eden
- From Survivor
- To Survivor
- 老年代(Old Generation)
- 永久代(Permanent Generation)
- 线程私有缓冲区(TLAB)
- 方法区(Method Area)
- 运行时常量池(Runtime Constant Pool)
- 类的版本、字段、方法,接口,静态静态变量等描述信息
- 直接内存(Direct Memory)
- 元空间(Meta Area)
- 其他区域
- JIT 代码缓存(Code Cache)
线程区域
该区域为每个线程独占内存,包括程序计数器、本地方法栈、Java 虚拟机栈。
程序计数器(Program Counter Register)
对于 Java 方法,记录正在执行的字节码指令的地址。
字节码解释器通过改变程序计数器来依次读取指令,实现代码的流程控制,如:顺序执行、选择、循环、异常处理等;
在多线程的情况下,线程轮流切换并分配处理器执行时间,PCR 用于记录当前线程执行的位置,便于切换时恢复;
对于 Native 方法,计数器为空;
JVM 规范中唯一没有定义 OOM 情况的区域。
本地方法栈(Native Method Stack)
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理(HotSpot 把它与 JVM 栈合二为一)。
该区域中使用的语言、使用方式和数据结构没有强制规定,因此 JVM 可以自由实现它。
Java 虚拟机栈(JVM Stack)
Java 虚拟机栈生命周期与线程相同。
每个 Java 方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、常量池引用等信息。
从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
设置每个线程的 Java 虚拟机栈内存大小:
1 | java -Xss512M HackTheJava |
其中当线程请求栈深度大于 JVM 允许的栈深度(比如递归),将抛出 SOF;JVM 栈可动态扩展,当栈扩展仍无法申请到足够内存则抛出 OOM。
当限制栈大小(不允许动态扩展),则无论时栈帧太大还是 JVM 栈容量太小,都是抛出 SOF。
栈帧(Stack Frame)
每个方法执行时, JVM 都会同步创建一个栈帧。其需要使用多少内存,在类结构确定时就可以确定。
操作数栈(Operand Stack)
存放计算的操作数和返回结果,元素可以是任意 Java 类型,32 位数据占 1 个栈容量,64 位数据占 2 个栈容量。
执行每一条指令之前,JVM 要求操作数已被压入操作数栈中。在执行指令时,JVM 会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
常量池引用(Reference to Constant Pool)
动态链接
指向运行时常量池中该栈帧所属方法的引用。
方法出口
即方法返回地址,方法在遇到 return
或触发异常后退出。
退出方法时可能执行的操作:
恢复上层方法的局部变量表和操作数栈;
把返回值压入调用者栈帧的操作数栈;
调整 PC 计数器指向方法调用后面的指令。
局部变量表(Local Variable Array)
其等价于一个数组,所存放的元素、最大容量在编译期可知。一般包含以下内容:
基本数据类型:以 Slot 为最小单位(32 位)分配内存,64 位长度的 long 和 double 类型会占用 2 个局部变量空间(Slot),其余的数据类型只占用一个。
对象引用(reference):不等同于对象本身,可能是一个指向对象初始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。可查找 Java 堆中的实例对象的起始地址、查找方法区中的 Class 对象。
字节码指令地址(returnAddress):指向一条字节码指令的地址。
至少存在一个指向当前对象实例的局部变量,预留出第一个 Slot 位存放对象实例的引用,方法参数值从 1 开始计算(this
关键字访问自动转为对一个普通方法参数的访问,由 JVM 调实例方法时自动传入此参数)。
Slot 的复用: 如果当前位置已经超过某个变量的作用域时,这个变量对应的 Slot 可以给其他变量服用。同时也说明只要其他变量没有使用这部分 Slot 区域,这个变量还会保存,因此这会对 GC 操作产生影响。
1 | public static void main(String[] args) { |
-verbose:gc
输出(没有被回收):
1 | [GC (System.gc()) 68813K->66304K(123904K), 0.0034797 secs] |
进行如下修改:
1 | public static void main(String[] args) { |
-verbose:gc
输出(被回收了):
1 | [GC (System.gc()) 68813K->66320K(123904K), 0.0017394 secs] |
被回收的关键在于局部变量表中的 Slot 是否还存在关于 placeholder 的引用:
出了 placeholder 所在的代码块后,还没有进行其他操作,所以 placeholder 所在的 Slot 还没有被其他变量复用,也就是说,局部变量表的 Slot 中依然存在着 placeholder 的引用;
第二次修改后,
int a
占用了原来 placeholder 所在的 Slot,所以可以被 GC 掉了。
共享区域
堆(Heap)
为 Java 程序中的对象分配内存,GC 的主要区域。
- 存放几乎所有对象实例和数组(了解“JIT 编译器”、“逃逸分析”,可实现栈上分配、标量替换优化技术)。
- 对于 Oracle Hotspot JVM 而言,可以明确所有对象实例都是在堆上创建(包括静态遍历和 Intern 字符串缓存),参考 文档。
- 对于 byte、char 以及 short 这三种类型的字段或者数组单元,它们在堆上占用的空间分别为 1byte、2bytes,以及 2bytes,与这些类型的值域相吻合。
- 现代的垃圾收集器基本都是采用分代收集算法,针对 新生代 及 老年代;
- 可划分出多个线程私有的分配缓冲区(TLAB):与存放内容无关,都是存储对象实例。
- 不需要连续的物理内存,可以动态增加其内存,失败时会抛出 OutOfMemoryError 异常。
主流的 JVM 支持堆扩展,通过 -Xms
(初始值)和 -Xmx
(最大值)指定一个程序的堆内存大小。
1 | java -Xms1M -Xmx2M HackTheJava |
指定新生代大小:-Xmn
,所以堆内存为:
1 | -Xms, -Xmx == -Xmn + 老年代 |
新生代(Young Generation)
大多数对象(80%)生存时间很短,主要使用复制算法。
Survivor 区中每经过一次 Minor GC 都没有被清理的对象,年龄会增加一岁,增加到一定程度就会被移动到老年代。
如果使用 Appel 划分,则分为 Eden 区和 Survivor 区。
老年代(Old Generation)
进入老年代的四种对象:
对象年龄太大(经历太多次 GC 后存活);
对象占用内存太大;
Young GC 后存活对象过多、Survivor 放不下;
动态年龄判断。
永久代(Permanent Generation)
容易造成内存溢出,JDK 1.8 已移除。
线程私有缓冲区(TLAB)
从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的线程分配缓冲区(TLAB,按内存分配分区)。
实际上这部分不属于共享区域,目的是更好地回收内存、提升对象分配的效率。
方法区(Method Area)
存放运行时常量池,以及类的版本、字段、方法,接口,静态静态变量等描述信息(即时编译器编译后的代码,.class 文件)。
不需要连续的内存,可以动态扩展,动态扩展失败会抛出 OutOfMemoryError 异常;
垃圾回收:主要目标是对常量池的回收和对类的卸载,但比较难实现,未完全回收可能导致内存泄漏。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。
但很难确定其的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。
运行时常量池(Runtime Constant Pool)
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域;
JVM 规范对于 Class 文件中的运行时常量池没有任何细节要求,可由 JVM 厂商实现;
动态性:除了在编译期生成的常量,还允许动态生成,例如
String.intern()
。