深入理解JVM第2章笔记
2.1 概述
本章介绍Java虚拟机内存的各个区域,讲解这些区域的作用、服务对象及可能产生的问题
2.2 运行时数据区域
程序计数器
- 字节码行号(偏移量)指示器
- 线程私有
- 执行native方法时,计数器值为空(undefined)
- 唯一在JVM规范中没有规定任何OutOfMemeryError (OOM) 的区域
Java虚拟机栈
- 线程私有,生命周期与线程相同
- 每个方法执行时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法入口等信息
- 该区域若线程请求栈的深度大于虚拟机所允许的深度,则抛出 StackOverflowError
- 在该区域虚拟机栈动态扩展时无法申请到足够的内存时,则抛出 OutOfMemoryError(OOM)
本地方法栈
- 与虚拟机栈类似,用于执行本地(native)方法
- HotSpoot虚拟机中,本地方法栈与虚拟机栈合二为一
方法区
- 线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、及时编译器编译后的代码缓存等数据
- JDK8以前(不包含8),HotSpot虚拟机通过永久代来实现方法区。到了JDK8后,使用元空间的方式实现方法区(不能说元空间代替方法区,应该说方法区的实现从 ‘永久代’ 转换成 ‘元空间’ 的实现)
- JDK7时,HotSpot将字符串常量池、静态变量等移除永久代(存放到堆上)
- 方法区无法满足新的内存分配的时候也会抛出OutOfMemoryError(OOM)
运行时常量池
- 方法区的一部分
- Class文件中的常量池表在类加载时放入方法区中
直接内存
- JDK1.4中引用的NIO类可以使用native函数库直接分配堆外内存
- 各个内存区域总和大于物理内存限制时就会抛出OutOfMemoryError
- 元空间(MetaSpace)的内存区域属于直接内存
2.3 HotSpot虚拟机对象探秘
对象的创建
- 类未被加载,先执行类加载过程
- 接下来虚拟机为新对象分配内存,有两种方式:指针碰撞 & 空闲列表,采用哪种分配内存策略取决于内存是否规整。
- 将分配到的内存空间初始化为零值
- 将对象的哈希码、GC年龄等信息存储到对象头中
- 虚拟机中对象已创建完成,接下来执行
(构造方法)方法对对象进行初始化
指针碰撞
JVM堆中内存规整的时候为新对象分配内存会选用指针碰撞方式进行分配。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存时绝对规整的,所有被使用过的内存都会被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅时把那个指针向空闲空间方向挪动一段与实例对象大小相等的空间,这种分配方式就是指针碰撞。
空闲列表
如果Java堆内存中内存并不是规整的,已被使用的内存和空闲内存相互交错在一起,就不可以进行指针碰撞进行分配了,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候,从列表中找到一块大于对象实例大小的空间分配给实例对象,并更新列表上的记录,这种分配方式叫空闲列表。
对象创建在虚拟机中是非常频繁的行为,可能存在线程安全的问题,如果一个线程正在给A对象分配内存,指针还没来得及修改,同时另一个为B对象分配内存的线程仍然引用这个指针,B对象分配内存的时候就会出现问题,把A对象给覆盖了。
对象Object的内存布局
- 对象内存布局为三部分:对象头、实例数据、对齐填充
- 对象头信息:对象自身运行数据(Mark Word)、类型指针(Class Pointer)
- 如果对象是数组,则对象头还需要存储数组的长度
- HotSpot分配实例数据的默认顺序为 longs / doubles、ints、shorts/chars、bytes/booleans、opps(普通对象指针),即相同长度在一起分配
- 如果对象实例长度部署8bytes的倍数,则需要进行对齐填充
对象的访问定位
- 对象访问方式由虚拟机实现决定的,主要包括句柄访问和直接指针访问
- 句柄包含对象实例数据和类型数据的地址
- 句柄访问较稳定,直接指针访问速度更快
2.4 实战:OutOfMemoryError异常
Java堆溢出
// todo
虚拟机栈和本地方法栈溢出
// todo
方法区和运行时常量池溢出
// todo
虚拟机直接内存溢出
// todo