JVM 中的内存区域划分
主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区
程序计数器(Program Counter Register)
- 用于记录当前线程执行的位置
- 没有规定任何 OutOfMemoryError 情况(内存溢出)
- 每条线程内部都有一个私有程序计数器
- 生命周期随着线程的创建而创建,随着线程的结束而死亡
一些我们熟悉的恢复线程、分支操作、循环操作、跳转、异常处理等也都需要依赖这个计数器来完成
虚拟机栈
- 用来描述 Java 方法执行的内存模型
- 每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧
- 虚拟机栈也是线程私有的,与线程的生命周期同步
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧
一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等
局部变量表
- 局部变量表是变量值的存储空间
- 方法内部创建的局部变量都保存在局部变量表中
系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)
操作数栈
- 操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)
- 栈中的元素可以是任意Java数据类型
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的。在方法执行的过程中,会有各种字节码指令被压入和弹出操作数栈
动态链接
- 主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)
Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态连接(Dynamic Linking)
返回地址
- 用来帮助当前方法返回到方法被调用的位置
一般来说,方法正常退出时,调用者的 PC 计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
本地方法栈
本地方法栈和虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。
堆
- 该区域唯一目的就是存放对象实例
- 是 JVM 管理的内存中最大的一块
- 是 Java 垃圾收集器(GC)管理的主要区域
- 被各个线程共享的内存区域
它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。
按照对象存储时间的不同,堆中的内存可以划分为新生代(Young)和老年代(Old),其中新生代又被划分为 Eden 和 Survivor 区。不同的区域存放具有不同生命周期的对象,这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,进而提高垃圾回收效率
方法区
- 方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据
- 被各个线程共享的内存区域
异常
StackOverflowError 栈溢出异常
递归调用是造成StackOverflowError的一个常见场景。
如果在方法中,递归调用了自身,并且没有设置递归结束条件,则会产生StackOverflowError。
原因就是每调用一次方法时,都会在虚拟机栈中创建出一个栈帧。
因为是递归调用,方法并不会退出,也不会将栈帧销毁,所以必然会导致StackOverflowError。
因此当需要使用递归时,需要格外谨慎。
OutOfMemoryError 内存溢出异常
1.理论上,虚拟机栈、堆、方法区都有发生OutOfMemoryError的可能。
2.实际项目中,大多发生于堆当中
例如在一个无限循环中,动态的向ArrayList中添加新的HeapError对象。
这会不断的占用堆中的内存,当堆内存不够时,必然会产生OutOfMemoryError
总结
JVM的运行时内存结构中一共有两个“栈”和一个“堆”
分别是:Java虚拟机栈和本地方法栈,GC堆 和 方法区,还有程序计数器
JVM 内存中只有堆和方法区是线程共享的数据区域,其它区域都是线程私有的
程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭
栈中的栈帧随方法的进入和退出进行出栈和入栈操作