深入理解Java虚拟机学习笔记(一)

内存区域与内存溢出异常

运行时数据区域

  1. 程序计数器:线程私有,负责指令跳转,执行Java方法时记录虚拟机字节码指令地址,执行Native方法时为空。
  2. 虚拟机栈:线程私有,存储栈帧。
    • 栈帧:方法运行时的基础数据结构,方法执行时创建,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
    • 局部变量表:由若干个局部变量空间(Slot)组成,存放编译器可知的基本数据类型,对象的引用和返回地址类型,其中64位整数long和浮点数double的数据会占用2个空间(Slot),其余数据类型只占用一个。局部变量表的内存空间是在编译时完成分配的,在方法运行期间不会发生改变。
  3. 本地方法栈:规范中无强制规定,在Hotspot VM中与虚拟机栈合二为一。
  4. 堆:线程共享,虚拟机启动时创建,存放对象实例。也叫GC堆。
  5. 方法区:线程共享,存放虚拟机加载的类,常量,静态变量,JIT编译后的代码等数据。
  6. 运行时常量池:方法区的一部分,存放编译期生成的各种字面量和符号引用。
  7. 直接内存:不属于运行时数据区域,通过NIO类通过Native函数库直接分配堆外内存。

运行时内存溢出异常

  1. 程序计数器:无
  2. 虚拟机栈:
    • StackOverflowError: 线程请求的栈深度大于虚拟机所允许的最大深度。
    • OutOfMemoryError:如果虚拟机栈可以动态扩展,而线程无法申请到足够的内存。
  3. 本地方法栈:与虚拟机栈相同。
  4. 堆:
    • OutOfMemoryError:如果堆中没有足够的内存来完成实例分配,并且堆也无法进行扩展。
  5. 方法区:
    • OutOfMemoryError:如果方法区中没有足够的内存来完成实例分配,并且方法区也无法进行扩展。
  6. 运行时常量池:与方法区相同。
  7. 直接内存:
    • OutOfMemoryError:动态扩展时,超出了物理内存或操作系统的限制。

虚拟机对象

对象的创建

  1. 类加载检查
  2. 分配内存
    • 指针碰撞
    • 空闲列表
  3. 初始化对象为零值
  4. 设置对象头
  5. 调用构造方法

对象的内存布局

  1. 对象头
    • 运行时数据(Mark Word):HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
    • 类型指针:指向对象的类元数据,虚拟机通过这个指针判断对象是哪个类的实例。
    • 数组长度:记录数组的长度。
  2. 实例数据
    • 存储顺序:受虚拟机分配策略参数(FieldAllocationStyle)和字段定义顺序影响,Hotspot VM的默认顺序为:longs/doubles, ints, shorts/chars, bytes/booleans, oops(Ordinary Object Pointers),即相同长度字段被分配在一起,除此之外,父类成员总是在子类成员之前。若CompactFields参数为true,则子类中长度较小的字段也可能被插入到父类字段的间隙中。
  3. 对齐填充:不一定存在,起占位填充的作用。Hotspot VM中的自动内存管理系统要求对象的起始地址必须是8的整数倍,也就是对象的大小必须是8的整数倍,而对象头部分正好是8字节的倍数,所以实例数据部分不对齐时需要通过对齐填充来补全。

对象的访问定位

  1. 句柄:需要在堆中划分一块区域来存储句柄池,虚拟机栈中的局部变量表中的对象引用存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的地址信息。优点:对象引用存储的地址是稳定的,在对象被移动(如GC时)只需要改变句柄中的实例数据的地址。
  2. 直接指针:需要在堆中存放对象的类型数据信息,而对象引用存储的是直接指向对象的地址。优点:速度快,节省了一次指针定位的开销。Hotspot VM采用本方法进行对象访问定位