运行时数据区
-
程序计数器:
- 线程私有(每个线程都有一块独立的内存空间用来保存该线程的程序计数器)
- 指向当前线程所执行到的位置,字节码解释器就是通过它来执行下一条需要执行的指令,分支,循环,跳转等,都是依赖它实现的;
- 线程切换后,可以恢复到原来执行的位置继续执行,也是依赖于它;
- 当线程执行Native方法时,该计数器的值为空;
- 它是唯一一个没有OutOfMemoryError的内存区域
-
Java虚拟机栈
- 线程私有,生命周期与线程相同;
- 它描述的是Java方法执行的内存模型;
- 每个方法执行时,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接方法出口等信息,方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈出栈过程;
- 局部变量表存放了编译器可知的各种基本数据类型,对象引用和returnAddress类型;
- 局部变量的内存空间分配在便宜期间完成,运行期间不会改变大小;
- 该内存区域会抛出StackOverflowError(栈深度)和OutOfMemoryError。
-
本地方法栈
- 为虚拟机中使用到的Native方法服务;
- 虚拟机规范中没有强制规定实现,所以不同虚拟机可以有不同实现,但是与虚拟机栈的作用类似。
-
Java堆
- 所有线程共享,虚拟机启动时创建;
- 作用:存放对象实例;
- 是GC的主要区域
- 分为新生代(Eden区,From Survivor区,To Survivor区)和老年代;
-
方法区(元数据区)
- 各个线程共享,存储加载的类的信息,常量,静态变量,即时编译后的代码等数据;
- 这里的内存回收:常量池的回收和类型的卸载;
- 会抛出OutOfMemoryError异常
- 运行时常量池:用于存放编译器生成的各种字面量和符号引用,在编译时和运行时都可以加入内容;
-
直接内存
- 它不是运行时数据区的一部分,而是服务于NIO类的,直接通过Native操作分配堆外内存的区域;
- 它受制于本机物理内存的大小。
HotSpot虚拟机中的对象
对象的创建
- 当遇到new指令时,首先检查该符号引用代表的类是否已经经过加载,链接和初始化,若未进行,则先执行类的加载;
-
为新生对象分配内存(取决于内存是否规整)
- 指针碰撞:Java堆中内存绝对规整,其间通过一个指针作为分界点的指示器,分配内存时,向空闲那端移动一段与对象大小相等的距离;
- 空闲列表:Java堆中内存不规整,哪块内存是可用的记录在"空闲列表"中,分配时,从空闲列表中找到一块足够大的空间划分给对象实例并更新列表上的记录;
注意:因为分配内存是一个非常频繁的操作,所以为了保证线程安全:- 同步处理:CAS+失败重试来保证原子性
- 将内存分配动作划分到不同的空间中进行:堆中预先为每个线程预留一块空间,称为本地线程分配缓存(TLAB),只有TLAB用完后再分配新的TLAB时,才需要同步;
- 初始化内存,将内存空间全部初始化为零值(不包括对象头)
-
设置对象的必要信息(对象头)
- 对象的归属,如何找到类的元数据信息
- 对象的哈希值
- GC分代年龄
- 执行程序定义的初始化方法
对象的内存布局
-
对象头(Mark Word)
说明:会根据对象的状态复用自己的存储空间,可以根据标志位来判断,它主要由以组成- 对象的运行时数据
- hashCode
- GC分代年龄
- 锁的各种信息
- 类型指针(指向它所属的Class)
- 对于数组,还有一块用于记录数组长度的数据(因为对象可以从元数据中知道它占用的空间大小,而数组无法确定)
- 对象的运行时数据
- 实例数据:对象实例存储数据的有效信息,即程序中定义的字段信息。其中包含了其自己的数据以及从父类继承的数据,存储顺序受分配策略和字段定义顺序影响。
- 对齐填充:占位符,用来保证对象的起始地址始终是8字节的整数倍。
对象的访问定位
Java通过栈上的引用来操作堆上的具体对象,目前的访问方式有如下两种:
- 句柄
- 堆中划分出一块内存作为句柄池,引用指向句柄地址(对象实例数据和类型数据的具体地址信息)
- 优点:对象改变时,只需改变句柄中实例指针即可,栈中引用不需要更改
- 缺点:访问对象需要经过两次访问,速度慢
- 直接指针
- 引用直接存放对象地址,访问速度快(HotSpot使用)