作为一个java程序员,我们每天都在创建各种对象,但是你有没有思考过,当你 new 一个对象的时候,jvm 做了哪些工作呢?今天就让我们一起来研究下,java对象的底层原理。
对象分配
一个对象的创建可以分为以下几步。
检查加载、分配内存、内存空间初始化、设置、对象初始化
下面我们一一来分析每一个步骤
1) 检查加载
jvm 首先会去加载我们的 class 文件,这个步骤可以从本地加载,也可以从网络中去加载
2) 分配内存
我们首先提出两个问题
- jvm是如何确可用的内存空间的
- 如果有两个线程同时 new 对象,如何避免分配的内存空间重叠(并发安全问题)
下面我们一一 解释这两个问题
jvm是如何确可用的内存空间的
指针碰撞:
如果堆内存区域规整,所有用过的内存放在一边,没用过的 放在一边,中间有一个指针来指示,那分配内存就是把那个指针向空闲的区域移动一段距离(对象的大小),如下图所示
空闲列表
如果内存区域不规整,已使用和未使用的区域相互交错,采用空闲列表的方式,虚拟机会维护一个列表,列表上记录了哪些区域使用了,哪些区域未使用.,分配的时候就从列表中找一块足够大的空间给对象,堆中的内存区域是否规整,取决于采用的垃圾收集器是否带有压缩功能。
发安全问题
CAS 机制
分配内存的时候采用cas机制,保证分配的原子性
分配缓冲(TLAB)
每个线程在java堆中,预先分配一块私有的内存地址,线程如果需要分配内存,就在自己的这个私有的这个区域分配
当空间不足的时候,再从eden 区 申请一块继续使用
3) 内存空间初始化
内存分配完毕后,将分配到内存空间都初始化0值。比如 int 就初始化成 0,boolean 就设置成 false 。
注意这里的初始化是jvm的初始化。
这一步保证了 java的对象实例字段,在java代码中可以不赋值,就可以直接使用。
4)设置
虚拟机对对象的头部进行一些必要的设置
如:
这个对象是哪个类的示例
如何才能找到类的元数据信息
对象的gc分代年龄等信息
这些都存放在对象的头部中
5)对象初始化
上面的步骤完成以后,对于jvm虚拟机来说,一个新的对象已经产生了,但是从java程序的角度来说对象的创建才刚刚开始。
这一步就是给对象赋值,并执行构造方法
对象的访问定位
经过上面的分析,我们已经掌握了如何去创建一个对象了。
那么接下去我们要分析的就是,如何去jvm里访问到我们创建的对象
句柄池
jvm中回去维护一个句柄池,句柄池中的对象,存放两个信息,一个是对象实例的地址,一个是对象所对应类的地址
然后,我们的引用直接指向句柄池中的的元素。
这样设计的好处是,对象在jvm中由于 gc,所以地址的变化是十分频繁的,用句柄池就可以将 我们对对象的引用,和对象的实际位置隔离开来,对象位置发生变化的时候,reference不用更改,减少了维护的开销
直接指针
直接指针去掉了句柄池,直接将reference 指向了我们的对象实例,然后对象的头部又存放着对象的类型。
这样的模式的优点,相对于使用句柄池就减少了一次地址的定位的开销,由于对象的访问很频繁,积少成多,可是很可观的,