【JVM系列】- Java内存区域与内存溢出异常
文章目录
Java不同于C/C++,Java将内存管理的工作交给虚拟机自动完成,因此只有了解虚拟机的内存管理机制,才能在发生内存泄漏/内存溢出时解决问题
一、运行时数据区
虚拟机在执行Java程序时,会将所管理的内存划分为不同的区域
1. 程序计数器
相当于“解释器解释字节码时的行号指示器”,解释器通过改变计数器的值来选取下⼀条指令
-
线程私有,每个线程拥有独立的程序计数器
-
唯⼀在Java虚拟机规范中没有规定OutOfMemoryError情况的区域
2. Java虚拟机栈
-
线程私有,生命周期与线程相同
-
Java方法执行的线程内存模型:
- 每个方法执行时会同步创建⼀个栈帧,存放栈帧在Java虚拟机栈
- 栈帧存放局部变量表、操作数栈、动态连接,方法出口
- 方法的调用和执行完成对应栈帧出栈和入栈
- 局部变量表中存放了编译期可知的基本数据类型,对象引用,returnAddress类型,存放单位是局部变量槽;运行期间槽的数量不变,不同虚拟机变量槽的大小不⼀定⼀样
两类异常:
-
线程请求的超出虚拟机最大栈深度:StackOverflowError
-
栈空间申请失败:OOMError
3. 本地方法栈
功能与虚拟机栈类似,虚拟机栈执行Java方法,本地方法栈执行本地方法
两类异常:StackOverflowError、OOMError
4. Java堆
线程共享
存放实例对象,几乎所有对象实例都在堆上分配内存
-
虚拟机启动时创建
-
优化:划分出多个线程私有的分配缓冲区以提升内存管理的效率
-
堆可以被实现为固定大小or扩展,通过参数-Xmx和-Xms设定
-
OOMError:当实例对象无法分配内存且空间无法扩展时抛出OOMError
-
Java堆由垃圾收集器管理,大多基于分代收集理论设计
5. 方法区
线程共享
相当于是代码段+数据段
存放虚拟机加载后的类型信息、常量、静态变量,即时编译后的代码缓存等
-
以前HotSpot虚拟机用永久代实现方法区,后来用本地内存的元空间
-
当方法区无法满足新的内存分配需求时,抛出OOMError
6. 运行时常量池
!!方法区的⼀部分
存放的是类加载后Class文件常量池表中的字面量和符号引用,具有动态性
-
不止存放编译期间已经产生的常量,运行期间也可以将新产生的常量放入常量池
-
jvm运行时常量池的具体实现由厂商自行决定
-
受方法区内存的限制,当常量池无法再申请到内存时抛出OOMError
7. 直接内存
注意 !!不是运行时数据区的⼀部分
不受Java堆大小的限制,但受本机总内存的限制,如果设置参数时忽略了这⼀部分内存需求会导致抛出OOMError
二、HotSpot虚拟机对象
讨论内存细节需要把范围固定到具体的虚拟机和具体的内存区域才有意义,以Java堆为例
1. 对象的创建
-
遇到new指令,JVM首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
-
类加载检查通过后,虚拟机将为新生对象分配内存
-
对象所需内存的大小在类加载完成后便可完全确定
-
空间分配方式:指针碰撞(内存规整)、空闲列表(不规整)
-
Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理的能力决定
-
保障线程安全:同步处理、预留本地线程分配缓冲TLAB
-
-
内存分配完成之后,虚拟机必须将分配到的内存空间都初始化为零值
-
对对象的对象头进行必要的设置(对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息)
-
执行构造函数,即Class文件中的()方法;new指令之后会接着执行()方法,对对象进行初始化,这样一个真正可用的对象才算完全被构造出来
2. 对象的内存布局
对象在堆中的存储布局包含对象头、实例数据和对齐填充三个部分
-
对象头:包含两类信息
- Mark Word:用于存储对象自身的运行时数据,为了能够在极小的空间内存储尽量多的信息,被设计为动态定义的数据结构,能够根据对象的状态复用存储空间
- 类型指针:指向对象对应类型的元数据,用于确认该对象是哪个类的实例,使用句柄则不需要保留类型指针
- 如果对象是java数组,需要在对象头中记录数组长度
-
实例数据:对象真正存储的有效信息,字段存储顺序受参数设置和在源码中定义顺序的影响
-
对齐填充:没有特别含义,也不是必然存在,以8字节为单位,没有对齐则需要填充
3. 对象的访问定位
主流的访问方式主要有使用句柄和直接指针两种:
-
通过句柄访问对象:
-
Java堆分为句柄池和实例池两个部分,栈中的reference数据存储对象的句柄地址,句柄中包含两个指针
-
好处是栈中的reference存储稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,reference本身不需要被修改(对象移动在垃圾收集时非常普遍)
-
-
通过直接指针访问对象:
- 栈中的reference数据存储堆中对象地址,对象中包含到对象类型的指针和实例数据
- 速度更快,节省了⼀次指针定位的开销
三、OutOfMemoryError异常
1. 堆溢出(-Xmx-Xms)
-
异常排查:
- 首先Dump出当前的内存转储快照,通过内存映像分析工具对堆转储快照进行分析,确认是内存溢出还是内存泄漏
-
异常处理:
- 如果是内存泄漏,通过工具进⼀步找到GC Roots引用链找到泄漏的具体代码位置
- 如果是内存溢出,检查堆参数设置,对比机器内存,检查代码是否存在不合理(是否存在某些对象⽣命周期过长,持有时间过长等)
2. 栈溢出(-Xss)
-
StackOverflowError异常:
-
线程的栈深度超出,新的栈帧内存无法分配
-
出现时可以先定位到具体线程再相应解决
-
-
OutOfMemoryError异常:
- 扩展栈容量时无法申请到足够的内存(支持扩展)
- 或创建线程申请内存时就无法获得足够内存(不支持扩展)
- 出现时考虑减少线程数量/更换64微虚拟机/减少最大堆/减少栈容量(可以通过“减少内存”的方式换取更多线程)
3. 方法区和运行时常量池溢出
运行时常量池是方法区的一部分
方法区从JDK8开始完全使用元空间来代替永久代来实现方法区
JDK7及以后,原本存放在永久代的运行时常量池被移到Java堆中
-
JDK6中intern()方法会把首次出现的字符串实例复制到永久代的字符串常量池中存储,返回永久代中这个字符串实例的引用
-
JDK7中intern()方法由于字符串常量池移动到堆中,不需要拷贝字符串实例到永久代,而是在常量池中记录其首次出现的实例引用
4. 本机直接内存溢出
由本机直接内存溢出导致的OOMError明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出后产生的Dump⽂件很小并且使用了直接内存,就需要考虑⼀下本机直接内存溢出