运行时数据区域
根据是否线程共享,可以分为线程私有和线程公有两类
线程私有:程序计数器,虚拟机栈,本地方法栈
线程公有:堆,方法区,运行时常量池和直接内存
程序计数器
每个线程都有一个独立的程序计数器,可以看作是当前线程所执行的字节码的行号指示器。
如果执行的是java方法,用来记录当前正在执行的虚拟机字节码指令的地址
如果执行native方法,计数器值为空
该内存区域不会发生任何的OOM
java虚拟机栈
生命周期和线程一致,随着线程的启动而创建,随着线程的结束而销毁。
虚拟机栈描述的是java方法的内存模型
每个方法在执行的同时都会创建一个栈帧,每个方法从调用到执行完成的过程,都对应着一个栈帧的入栈和出栈的过程
当栈的深度大于限制的深度时出现stackOverFlow异常
当栈的创建无法申请到足够内存时出现OOM
本地方法栈
和java虚拟机栈作用类似,主要区别是虚拟机栈主要服务java方法,而本地方法栈服务native方法
hotspot在实现上将java虚拟机栈和本地方法栈合二为一
堆
虚拟机启动时候创建,用来存放对象实例,线程共享
垃圾收集器采用了分代收集算法,java堆也因此分为新生代和老年代,新生代细分为eden区,from surivor区,to surivor区
堆可以处于物理上不连续的内存空间中,只要逻辑连续即可
堆的大小实现上可以固定的也可以动态可扩展的(通过-xmx和-xms控制)
当堆无法扩展(堆大小达到最大值)且无法给新对象分配足够的内存时,会导致OOM
方法区
用于存储已被虚拟机加载的类信息,常量,静态变量以及即时编译器编译后的代码等数据
当方法区无法满足内存分配时,也会抛出OOM异常
运行时常量池
常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,在类加载时放入常量池。
具有动态性,运行期间也可以将新的常量放入池中,比如String类的intern()方法
当然无法申请内存也会导致OOM
直接内存
NIO API中使用native函数库直接分配堆外内存,通过堆内的DirectByteBuffer对象作为这块内存的引用进行操作
避免在java堆和native堆来回复制数据
受到物理内存的限制,当动态扩展受阻时会出现OOM
HotSpot虚拟机对象
对象的创建
- 类加载
遇到new指令,根据指令参数去常量池查询对应的符号引用,检查符号对应的类是否已被加载,解析和初始化,还没被加载时则需要执行类加载过程
- 分配内存
为新生对象分配内存,内存的大小在类加载后确定
虚拟机中内存分配方式有两种:
- “指针碰撞”
在内存规整的情况下,已使用的内存都放在一边,空闲的内存则放在另一边,中间用一个指针作为分界点的指示器,分配内存时把指针向空闲空间那边挪一段与对象大小相等的距离
- 空闲列表
虚拟机维护一个列表用来记录哪些内存时可用的,当分配时在列表中查找一块足够大的空间划给对象实例
在高并发的情况下,内存的分配需要做同步处理,解决方案有两种:
1.CAS配上失败重试机制
2.TLAB
本地线程分配缓存(Thread Local Allocation Buffer),每个线程在java堆中预先分配了一小块内存区域TLAB,哪个线程需要分配内存时就在对应线程预先分配的内存区域分配即可,只有TLAB用完时且需分配新的TLAB时才需要同步锁定
- 内存初始化
对类的实例字段进行初始化为零值
- 对象头设置
设置类型指针,GC分代年龄信息,hashcode,锁信息等
- 执行构造函数
执行代码的构造器方法
对象的内存布局
- 对象内存布局由对象头(header),实例数据(Instant Data)和对齐填充(Padding)组成
- 对象头主要有markword,类型指针以及数组类型特有的数据长度字段组成
- markword主要存放运行时数据,哈希码,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID等
- 类型指针,即对象指向它的类元数据的指针
- 数组对象无法根据类元数据确定对象的大小,因此需要数组长度字段记录对象的大小
- 对齐填充不是必要的,由于jvm自动内存管理规范要求对象的起始地址必须是8字节的整数倍,因此对象的大小也必须要是8字节的整数倍,不足8字节时通过对齐填充补全
对象访问的定位
主要两种方式:句柄和直接指针
句柄
堆内存开辟一块空间用作句柄池,栈上引用变量指向句柄池的句柄地址,而句柄包含了对象实例数据的指针和对象类型数据的指针
好处:引用中存储的是句柄的地址,对象被移动的时候只会改变句柄中的实例数据指针,引用本身无需改变
直接指针
引用存储的是对象地址
优势:访问速度快,节省一次指针定位的时间开销
对象的创建
Object obj=new Object();
当碰到一个new指令时,jvm首先检查对应的类是否已经加载到jvm中,如果没有加载,需要先对类进行加载。
类加载分为5个步骤:
1.加载
通过类的全限定名获取对应的类定义数据的字节流加载到jvm的方法区中,并在内存生成对应的Class对象。
2.验证
验证Class定义的合法性,确定符合虚拟机的规范,保证虚拟机运行时的安全。
3.准备
为类变量分配内存并设置初始值,类的常量(被final修饰的类变量)直接初始化为常量值。
4.解析
将常量池中的符号引用替换为直接引用,得到类,字段或者方法在内存中的指针或者偏移量。
5.初始化
执行静态代码块和为静态变量赋值,如果存在父类,则先初始化父类。
类加载完成,方法区储存当前类的类信息。
类加载后,接着执行对象的创建
1.在堆中为对象分配内存
2.将内存空间初始化为零值
3.设置对象头信息
设置对象所属的类(类型指针),对象的哈希码,GC分代年龄等
4.执行对象的初始化方法
分配内存方式
1.指针碰撞
对于规整的堆内存,所有用过的内存都放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就把指针向空闲的内存挪动一段与对象大小相等的距离。
2.空闲列表
堆的内存不规整的情况下(有内存碎片),虚拟机维护了一个列表,记录哪些内存时可用的,在分配的时候查找一块足够大的内存空间划分给对象实例。
出于并发申请分配的考虑,避免并发冲突,虚拟机采用了线程预分配的机制,即本地线程分配缓冲(TLAB)。
线程创建时都在队中预先分配一小块内存区域,只有在用完时并重写分配才需要进行通过锁定。(CAS+重置机制)