1. JVM运行时数据区域
1.1 程序计数器
是一块较小的内存,可以看作当前线程执行的字节码行号指数器;在Java虚拟机概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选择要执行的下一条字节码指令,它是程序控制流的指数器,分支、循环、跳转、异常处理、线程恢复都需要依赖它完成。
如果当前线程执行的是一个Java方法,则该计数器记录的是正在执行字节码指令的地址;如果执行是一个本地方法,则该计数器是空值;
在Java虚拟机规范中,该区域是唯一一个不存在OutOfMemoryError异常;
1.2 虚拟机栈
和程序计数器一样,也是线程私有的;它的生命周期与线程相同。
虚拟机栈描述的是Java方法的线程内存模型,每执行一个方法,虚拟机就会同步的创建一个栈针,用于存储局部变量表、操作数栈、动态链接、方法出口等信息;每个方法被调用直至执行完毕,对应的栈针在虚拟机栈中从入栈到出栈的过程;
局部变量表存放编译期可知的各种基本数据类型、对象引用类型(对象起始地址的引用指针或句柄或与此对象相关的位置)、returnAddress类型(指向下一条字节码指令的地址);
在Java虚拟机规范中,存在两类异常状况
- 如果线程请求栈的深度大于虚拟机中所允许的深度,则抛出StackOverFlowError
- 如果栈容量允许扩展,当栈进行扩展时无法申请到足够的内存,则抛出OutOfMemoryError
1.3 本地方法栈
和虚拟机栈的作用非常相似;
虚拟机栈为虚拟机执行Java方法服务
本地方法栈为虚拟机使用本地方法服务
与虚拟栈一样,如果栈深度溢出和无法扩展时,同样也会抛出StackOverFlowError、OutOfMemoryError
1.4 堆
虚拟机管理最大一块内存区域
线程共享的、在虚拟机启动时创建、用于存放对象实例
Java堆是垃圾收集器管理的内存区域
从内存分配的角度来说,所有线程共享的Java堆中可以划分出多个线程私有分配缓冲区,以提升对象分配时效率
如果在Java堆中没有内存完成实例分配,并且堆也无法在扩展时,虚拟机将会抛出OutOfMemoryError异常
1.5 方法区
线程共享的
用于存放虚拟机加载的类型信息、字符串常量、静态变量以及即使编译器编译的代码缓存数据等
两种实现方式
- 永久代
- 元空间
Java虚拟机规范规定,如果方法区域无法满足内存分配需求时,将抛出OutOfMemoryError异常
运行时常量池:
- Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用
- 当常量池无法申请再申请到内存时会抛出OutOfMemoryError异常
2. 对象创建
-
当Java虚拟机遇到一条字节码new指令时
-
首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用;
-
检查这个符号引用代表的类是否已被加载、解析和初始化;
-
如果没有,必须先执行相应的类加载过程;
-
类加载检查通过后,虚拟机为新对象分配内存;
Java堆是被所有线程共享的一块内存区域,主要用于存放对象实例,为对象分配内存就是把一块大小确定的内存从堆内存中划分出来,通常有指针碰撞和空闲列表两种实现方式。
内存分配方式
-
1.指针碰撞法
假设Java堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的GC收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。
-
2.空闲列表法
事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。
内存分配并发问题
- CAS: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
-
-
内存分配完成后,虚拟机必须将分配的内存初始化为零值。
-
对对象进行必要设置,属于那个类的实例;类的元数据信息;对象的哈希码;对象的GC分代年龄等;
3. 对象内存布局
3.1 对象头
-
对象自身运行时数据
对象的hashcode、GC分代年龄、锁状态标志、线程持有的锁、偏向锁线程ID、偏向时间戳
-
类型指针
即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定它是那个类的实例
3.2 实例数据
针对存储对象的有效信息
3.3 对其填充
起着占位符的作用;要求对象的起始地址必须是8字节的整数倍
4. 对象访问定位
4.1 句柄
需要在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄包括对象的实例数据和类型数据各自的具体地址信息
4.2 直接指针
如果直接指针访问的话,reference中存储的就是对象的地址,Java内存布局需要考虑如何存放访问类型的数据;如果只是访问对象本身的话,就不需要多一次间接访问的开销
4.3 两者对比
- 使用句柄最大的好处就是当对象移动的时候,无需改动reference中存储的指针地址,只需改动句柄中实例数据的指针
- 使用直接指针的好吃就是减少一次间接访问的开销
- 在HotSpot虚拟机中主要使用的是直接指针的方式进行对象访问