【大厂突击】二、对象的创建、内存布局以及访问定位
在我们日常编码过程中,创建对象通常仅仅使用到一个 new 关键字就行,在上一篇文章中我们讲到了对象是存放在在堆内存中,但是对象的创建过程是怎样的呢?对象又是被如何定位的呢?对象在堆里又是怎么布局的呢?请看官往下看
首先在给对象分配内存之前需要经过类加载过程,类被加载、解析、初始化后才会根据对象大小进行内存分配,那 JVM 是怎么知道对象的大小呢?当然是在加载的过程中会对对象进行大小分析啦,具体如何分析我们稍后再讲
JVM 在知道这个对象具体的大小之后,下面需要做的就是从内存里面划分一块空间给这个对象“居住”。找到一块合适的空间这可不是一件容易的事情,大家可以想象一下平时我们和朋友组队一起去食堂里面吃饭,假设你和两个朋友一共三个人,现在去食堂一看要么有两个位置和别人拼桌的要么有五个位置一桌的,看了一圈没有合适的只能换个地方了。。。
那虚拟机是如何留住这个“吃饭的客人”的呢?有两种方法,“指针碰撞”和“空闲列表”。我们先来说说指针碰撞,假设这个饭店有 50 个位置,不管每次进门的是几个客人我都让客人有序入座。第一次来一个客人,让他坐第一个位置,第二次来两个客人,让他们坐第二和第三个位置,以此类推。
也就是说指针碰撞是在堆内存绝对规整的情况下,所有的被占用的内存在一边,另一边是空闲的,中间有个指示器来划分界限,每次有对象来的时候划出和对象大小相等的空间就行。
聊完指针碰撞,再来聊聊空闲列表,还是以吃饭为例子,我们知道去饭店吃放我是交钱来被服务的,我想坐哪里就坐哪里,哪有听服务员排排坐的道理。所以说在 java 堆内存不规整的情况下指针碰撞的方式是行不通的。
在空间交错的情况下虚拟机必须维护一张列表,上面记录着现有可用内存的信息,然后如果有对象来了就根据这张列表上的信息找到一块合适的内存分配,并且更新列表
不同的垃圾收集器是否有压缩整理算法决定了这个 Java 堆是否规整。Serial、ParNew等有压缩整理功能的收集器采用的分配算法为指针碰撞,而 CMS 这种基于 Mark-Sweep 算法的采用空闲列表
现在大家已经知道了对象在堆里面的创建过程,现在还有一个问题,在频繁的创建对象的过程中虚拟机是如何确保线程的安全的呢?还是以食堂吃饭为例子,到饭点的时候客人是一堆拥挤进来,这个时候如果空闲列表上记录错了或者指针碰撞记录的编辑错乱怎么办呢?
一般来说解决这个问题有两种方案,一种是给分配内存的一系列动作做同步,说白了就是加锁。实际上虚拟机是用 CAS 加上失败重试保证原子性(CAS 后续会讲到)。还有一种方式就是先给每个线程分配一小块空间,然后再在这一小块空间里面进行分配对象内存的一系列动作,这部分一小块空间成为本地线程分配缓冲(TLAB)。那么如果是第二种方案的情况下,虚拟机只需要在 TLAB 这块内存重新分配的情况下才要加锁。
上面我们了解了对象的创建,以及对象的内存分配。那么对象本本身是包含哪些东东呢?在 HotSpot JVM 中对象的内存布局被划分成三个区域:对象头、实例数据、填充对齐。
我们可以从上面这张图看到,如果是非数组对象对象头包含两部分:mark word 和 类型指针。数组对象的对象头还多了一个长度。
Mark Word 存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
类型指针,是对象指向它的类元对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。
对齐填充:对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。
我们使用 JOL 来做一个验证,
在项目中引入 maven 依赖:
然后编写如下的代码:
我们可以看到控制打印出来的信息如下:
我们可以看到真个对象占用了 16 个字节,0 个字节额外填充,这里我们可以看到如果对象满足 8 的倍数就不会有额外填充。我们再来看看另一个例子:
我们把 XiaoDao 这个类中的属性改成 byte 类型,运行结果如下:
此时有 3 个字节被当作额外填充。大家可以前往以下链接查看如何使用 JOL:链接地址
上面我们讨论了对象的创建和对象的内存布局,我们创建一个对象,虚拟机给这个对象在堆空间分配了内存,那这个对象时怎样被虚拟机找到的呢?我们就需要聊聊对象时如何被访问和定位的。首先我们来看一张图:
从这张图中我们可以看出对象时被 Java 栈中的 reference 间接的引用到了。在 Java 堆中有一块内存叫句柄池,它就像中介一样链接这 reference 和 堆中的实际对象。
这里小刀需要啰嗦一句,计算机体系中有许多术语,有些术语不太形象具体,甚至有些直接音译过来,但是用久了之后就会变成“行业黑话”,“句柄”就是典型的“行业黑话”,所以如果大家刚接触这些“黑话”的话,不用太过分的纠结导致主次不分。
句柄池也是一块小内存,这块内存里面存放着对象实例数据与类型数据各自的具体地址信息,reference 存放的是句柄的地址。
上面这种是通过指针间接的访问到对象,还有一种方式是通过指针直接访问如下图所示:
这两种方式都有自己的优势,使用句柄池的好处就是对象被移动的时候 reference 不需要知道所以 reference 本身不需要修改,使用直接指针的话就是比较快、高效,不用中介后租客和房东直接谈价格签合同。因为在 JVM 中可能存在大量的对象,有大量对象被访问定位,极少成多后这些开销也是比较大的执行成本。
好了,这次小刀就先和大家先聊到这,下次如果面试官问你:“你能说说对象的创建、内存布局以及对象如何被访问到的么?”。直接甩他脸上吧!
原创不易,感谢大家在看点赞,我们下期再见!!!