虚拟机运行时数据区
- 程序计数器
- 虚拟机栈/本地方法栈
- 方法区
- 堆区
- 程序计数器
每个线程都有一个独立的计数器,并与其他线程隔离,通过改变计数器的值进行字节码指令的操作,实现分支、跳转、循环、异常处理、线程恢复……等功能。 - 虚拟机栈
线程私有,生命周期与线程相同,每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口信息等。每个方法的调用(入栈)直至执行完成(出栈)的过程,对应值一个栈帧在虚拟机栈中的入栈和出栈过程。
局部变量表储存了编译期可知的各种基本类型、引用(非对象本身)、returnAdress(指向一串字节码指令的地址),其空间在编译器分配,方法在帧中分配多大的局部变量空间是编译器就确定的,不会在运行时发生改变。
如果线程请求栈深度大于虚拟机允许深度,抛出StackOverflowError异常
如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够内存,抛出OutOfMemoryError异常 - 本地方法栈
类似虚拟机栈,区别为虚拟机栈为虚拟机执行java方法,本地方法栈执行非java代码。 - 堆
线程共享,虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例在此处分配内存。物理上可连续可不连续,保证逻辑连续即可,同时大小有固定有扩展,若扩展失败,则抛出OutOfMemoryError异常。 - 方法区
线程共享,用于存储虚拟机已经加载的常量,类信息,静态变量等。内存可连续可不连续,可固定可扩展,无法扩展时抛出OutOfMemoryError异常。 - 运行时常量池
方法区的一部分,用于存放编译期间产生的各种字面量和符号引用。 - 直接内存
并不是运行时数据区一部分,也不是虚拟机规范中定义的内存区域。NIO类能够使用native函数创建堆外内存,然后通过储存在堆里的DirectByteBuffer对象对这块内存进行引用操作,避免java堆和native堆来回复制数据,提高部分场景应用,但该内存仍然受限于物理内存,也可能产生OutOfMemoryError异常。
HotSpot对象
- 对象创建
首先进行类加载检查,通过后为对象在堆中分配内存区域。- 若堆中内存规整(指针分界,一边用过,一边还没用),直接将指针移动对象大小等距离,该方法称为 “指针碰撞”。
- 若堆中内存不规整,虚拟机需要额外维护一份列表,从列表中找出一块足够分配对象内存的区域,该方法成为 空闲列表。
- 指针碰撞时的线程安全
当多线程操作时,指针的移动可能发生危险,如一个线程A正在分配内存还未移动指针,线程B又使用该线程分配新的内存。有两种方案解决:- 对指针操作进行加锁,使用CAS保证原子性。
- 给线程划分相对独立的堆区域(本地线程分配缓冲,TLAB),各个线程创建对象分配内存时在自己的TLAB分配,只有当TLAB不够需要新的TLAB时才会进行同步操作。
- 零值初始化
内存分配后对对象进行初始化零值,可以保证对象字段不被赋初始值也可以直接使用。 - 设置对象头
- init初始化
对象的内存布局
- 对象头
- 实例数据
- 对齐填充
-
对象头
- MarkWord(运行时数据),存储hashcode、GC分代年龄,锁状态标志,线程持有的锁、偏向线程ID等
- 类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针确定该对象是哪个类的实例。并不是所有虚拟机都有类型指针。
-
实例数据
对象真正存储的有效信息,程序代码中定义的各种类型字段的内容。存储顺序收到虚拟机策略参数和在java代码中定义的顺序有关。HotSpot默认规范是相同宽度的字段总是分配在一起(longs/doubles,ints,shorts/chars……) -
对齐填充
非必须,占位符左右,对象大小必须是8的整数倍,对象头满足,而实例数据不一定,该字段用来填充补齐。
对象的使用
通过栈中的reference来操作堆中的对象,reference访问对象的方式主要有两种:
- 句柄
- 堆中划分一块作为句柄池,reference储存句柄地址,句柄中存储着实例数据与类型数据具体的地址信息。
- 好处是reference储存的数据稳定,对象移动时只需要改变句柄中的实例数据指针。
- 直接指针
- reference储存的是对象的地址信息。
- 好处是速度快,无需一次额外的地址指针操作。