一.JVM运行流程
JVM向操作系统申请内存,初始化运行时数据区,接下来装载使用的类,执行类里面相应方法的时候为当前虚拟机栈压入一个栈帧,方法执行完成后栈帧出栈,进行垃圾回收。
二.JVM中对象的创建过程
符号引用:常量池里面有一个对B对象的引用,但是我们目前不知道B对象的真实地址,所以我们用一个字面量去代表B对象。这就叫做符号引用。在new出A对象,检查加载的时候,JVM会动态解析符号引用装换为真实的地址(直接引用)。
JVM基础(四):Java类加载机制_符号引用转换为直接引用-CSDN博客
分配内存
分配内存的方式
为对象分配内存的方式有指针碰撞,空闲列表。
指针碰撞适用于内存空间较为规整的情况下,为A对象分配3格空间,指针往后移动三格;为下一个对象分配的时候,当前指针位置开始分配即可。
如果内存空间不规整的话,就需要用到空闲列表分配内存。JVM通过维护一张内存列表记录可用的内存块信息,当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。
这时候可能会有多个线程同时申请一块内存,怎么解决并发安全呢?CAS加失败重试和为线程分配缓冲。
解决并发安全
CAS
CAS操作包括三个操作数---内存位置、预期数值和新值。CAS的实现逻辑是将内存位置处的数值与预期数值向比较,若相等则将内存位置处的值替换为新值。若不相等,则不做任何处理。
CAS的操作是抱着乐观的态度进行的,总认为自己可以成功完成操作,当多个线程同时使用CAS
操作同一个变量的时候,只会有一个胜出并成功更新,其他均会失败。失败的线程不会被挂起,仅被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
举个例子,A和B进行CAS,A的预期值是0,新值是1,B的值是0,A拿到B的值0对比自己的预期值发现相同,就会把1赋值给B,B的值变成1;这时候C的预期值是0,新值是1,C拿到B的值0发现不是自己预期的值,CAS失败。
CAS是一种CPU的指令,CPU的指令要不就执行,要不就不执行,所以具有原子性。如果是多核CPU,会有一个lock指令,保证只有一个CPU执行。
给每一个线程分配一个线程缓冲
一般分配对象都在堆的新生代的Eden区上,如果开启本地线程缓冲,如果A线程和B线程都在Eden分配对象,会分别为这两个线程在Eden区上划分一块区域,让这两个线程分配对象。
JVM默认的方式就是线程缓冲,如果被禁用则使用CAS
内存空间初始化
为当前对象的成员变量初始化值,比说int类型的初始化为0。
设置
针对对象头的一些设置。
对象的内存布局
类型指针:当一个对象创建的时候,在堆里面存储的对象,会有一个类型指针指向方法区里面这个对象的类型。虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据:实例数据就是我们所赋予给对象的那些属性信息,这部分内容是紧挨着对象头存储的。
对齐填充:对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头正好是8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
对象初始化
构造方法执行。
对象的访问定位
我们一般执行一个方法的时候,会在当前线程的虚拟机栈里面压入一个栈帧,在这个方法里面执行Person person1 = new Person,会在堆里面创建一个对象,这个person的引用会存在虚拟机栈的局部变量表里面,Person类型会存在方法区里面。堆里面的对象的类型指针会指向方法区的Person类型,局部变量表里面的person引用会指向堆里面的对象。
局部变量表里面的person引用指向堆里面的对象,存在两种方式:句柄访问和直接指针。
句柄访问
Java堆中将会划分出一块内存来作为句柄池,reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
举个例子,去浴室洗澡,我们拿到搓澡师傅的手牌是13号,这个13号就相当于一个句柄,这个句柄指向的真正对象是王师傅。就相当于这个句柄就是一个代号,我们通过这个代号找到真正的搓澡师傅。
好处:句柄稳定,对象被移动只要修改句柄中的地址,不需要修改线程里面虚拟机栈的栈帧的局部变量表里面的引用。
坏处:需要开辟一块独立的空间给句柄池;同时访问对象效率较低,需要通过两次指针。
直接指针
reference
中直接存储的就是对象地址,对象中存储了到对象类型的类型指针。
使用直接指针的方式,访问速度快,节省了一次指针定位的开销。