六、一个对象创建的过程
一个对象的创建大概分为以下几步
当我们程序在碰到new这个关键字的时候,就会创建一个对象,那么大概是一下几步:
-
检查加载
检查我们这类是否被加载(app加载器),当然了还有很多的检查以后细说
-
分配内存
分配内存大概有一下两种方式
-
执行碰撞
我们都知道堆空间是化分的一块区域,那么如果我们之前维护的所有的对象(绝大多数的对象都是在堆空间上的,除非进过逃逸分析后,得出该对象无法逃出当前线程,那么就是栈内对象)都是正气的摆放在堆内存中,那么下次分配对象的时候,就可以很轻松的知道这个对象应该放在哪里,就是我们的指针向后移动来分配内存。
-
空闲列表
我们知道,垃圾回收有标记清楚算法和标记整理算法,清楚算法的话我们知道会想某一块的内存回收,但是不进行整理,那么我们就没有办法知道哪些内存是可以用的,所以我们需要维护一张空闲列表,连标记哪些内存是可用的
-
内存分配的问题以及解决方案
在之前的介绍中,我们知道jvm天生就是一个多线程的环境,所有我们分配内存的时候天然的就会存在线程安全问题。什么问题呢,线程a和b,a线程读取内存地址0001,可用,b线程同时也读取0001可用,然后a线程开始初始化自己对象,并且把引用返回,但是b线程在a线程稍后的时间中,同时初始化自己的对象,那么就会造成混乱。所以提出了解决方案
-
CAS加失败重试
Compare and Swap
[图片上传失败...(image-847ac6-1597672641022)]
我每次需要分配的时候都需要和上传读取的内存信息做比较,这样就能保证线程的安全,就像是数据库的乐观锁思想:
update table1 set a = 'new' where a = 'old'
虽然这个中锁比起悲观锁效率要搞上不少,但是还是耗费了性能和时间,所有才会有下面的另一种分配方式
-
本地线程分配缓冲(TLAB)
cas.png
-
-
当一个分配对象的线程来划分空间的时候,jvm会主动来分配一个缓存区域来tlab给当先线程单独划分区域使用,所有这个对于线程来说就是安全的,而且不会使用任何的失败重试的操作,分配对象就比价块。但是这个区域一般是有大小限制的(一般是Eden的1%),但对象超出这个大小的时候,就会采用上面的CAS机制来分配对象。
从官网中,我们也了解到这种机制是默认开启的
也可以自动设置TLAB的大小
-
内存空间初始化
我们都知道,对象的初始化时早于构造方法的,因为构造方法中有些需要用到对象的数据,那么这个成员变量就会显初始化,所以这一步就是初始化话这些属性变量,int的变量为0.....
-
设置
一个对象分为好几个部分
-
对象头
image-20200802175233481.png
对象头保存了很多的数据,包括我们知道的synchronized也是通过对象头上的锁标识来完成的。 -
实例数据
就是我们平时的数据
-
对齐填充
因为64为的比32的块,所有我们分配对象的时候是会以8字节的整数分配对象的,不够的部分就会进行填充
-
-
对象初始化
最后一步就是我们构造方法,我们就可以使用一个对象了。
七、对象的引用
我们的代码中,不可能直接去获取一个对象的内存地址来使用,都是拿到一个引用地址,然后jvm会帮助我们去调用方法等等。。。,那么我们如何获取对象的引用呢,现在主流的有两种方式
-
使用句柄
句柄拥有句柄池,句柄池中的数据执行会和jvm对象进行一一对应,然后我们就不需要获取对象的指针,我们只需要拿到一个永远不变的句柄引用就好,这种思想我认为是很有必要的
image-20200802191556996.png原因:
我们知道大部分的gc都会使用标记整理算法,这就意味着改变了对象的直接地址,那么我们在最gc的时候,就必须stop the work,来确保正在执行的程序对象引用的正常。
那么使用一个不会变动的句柄池,这样就保证了我们对象地址变动时候的安全性
缺点:
所谓的缺点也很明显,我们都知道内存是稀有且珍贵的,那么专门划分一个区域的句柄池,这样确实会造成不少的空间的浪费
同事,效率也缺乏,这个就像是我们本来只需要一个单表查询,然后现在我非得left join一下我才能找到对象
-
直接指针
基于句柄有不可逾越的性能问题,所有我们的主流的虚拟机都是去使用直接指针。