一、对象的创建
对象的实例是分配在堆内存当中,主要说创建。
创建有两种方法,一个是指针碰撞,一个是空闲列表。
指针碰撞:有一段连续的区间,边界是两个指针(假设左右指针),分配一个对象,分配的地方就移动一定的字节数。如果发现左指针移动一定量的字节数后已经跟边界的右指针接触越过了,就会分配失败。(左右是相对的)
一边分配,一边垃圾回收,肯定会产生空间碎片,空间碎片的大小可能有足够大的空间在里面在去分配其他的对象。那么空闲列表就是来记录这些碎片的。
空闲列表是记录空闲,可用的空间。初始状态下,整个堆不就是一大块空闲空间嘛。为什么空闲列表这么好有的垃圾回收器,或者jvm有一部分还用指针碰撞呢,原因很简单,越好用的东西,管理起来就越麻烦,浪费的时间的越多。
上述两种方法都可以来管理堆内存,来进行对象实例的创建。
二、内存分布
这一部分介绍对象的结构。有啥用,了解对象的结构后,有一些关键字的运作流程,就显得那么简单。例如锁,垃圾回收的年龄判断。
- Header(对象头):存储对象的原数据(无法直接操作)
- 自身运行时的数据(Mark Word)(根据对象的状态来复用自己的空间,效果下图显示的淋漓尽致,里面的信息不一定全部存进去,可以只存进一部分,用来显示当前状态)
- 偏向时间戳
- 偏向线程ID
- 线程持有的锁
- 锁状态标志
- GC分代年龄(涉及垃圾回收)
- 对象哈希码(native方法)。
- 自身运行时的数据(Mark Word)(根据对象的状态来复用自己的空间,效果下图显示的淋漓尽致,里面的信息不一定全部存进去,可以只存进一部分,用来显示当前状态)
2.指针类型:找到对象实例的元数据(所谓的元数据是指描述类与其他代码关系的代码介绍链接:传送门)
(注意:如果是数组类的 ,还有数组的长度。还有,从图中发现,这些标签并不一定同时存在,根据类的状态来改变)
2.instanceData(实例数据)
- 相同的字节数的会放到一块,父类数据放在子类数据之前。
- 注意局部变量放在局部变量表中,而且是在运行期间才会进行读写操作。
3.Padding (对齐填充):填充内存的作用
- 虚拟机有位数要求的时候,如果对象数据没有占够位数/倍数,用padding填空
用一个map来表示从属关系:
{对象头:
{自身运行时的数据(Mark Word):{
hash值(主要跟native本地方法有关,飘过):{},
GC分代(垃圾回收时使用):{},
锁状态标志:{},
线程持有的锁:{},
偏向线程id:{},
偏向时间戳:{}},
指针类型(保存找到对象实例的引用):{},
数组长度(如果是数组类型,保存长度):{}},
运行时数据:{},
padding:{},
}
这些内容都是存在堆中的。
三、访问定位
访问定位主要通过两种方式:直接引用,句柄池。
直接引用:java栈中找到本方法的实例的时候,reference(存在于栈->栈帧->局部变量表->第0个位置(数组从零开始))存放堆中实例的引用。直接引用就是对象的堆中的实际地址,描述一个类需要类实例,类的一些描述,这些类描述存在方法区中。所以,使用直接引用的话,类的实例需要存储一个指向类描述的指针。
句柄池:类似于链接的中介,存放类实例的地址,类描述的地址。reference只需要指向本类的句柄池的位置,就可以得到两个引用。但是句柄池需要在堆中开辟出一块单独的空间来进行存储这些信息。
各有什么优缺点?
直接指针当然是速度快,不用在堆中进行中转。
类的位置是会改变的,垃圾回收的时候,为了保持拥有连续的空间。经常会进行位置的移动,那么就需要进行指针的改变。
使用句柄池的话,只需要改变句柄池的指针即可,不需要到栈中改变。为什么同样是改变指针,为什么句柄池会快。因为一次对象的移动涉及到很多个类,况且栈结构遍历很麻烦,很耗时间。
鉴于这两个优缺点,可以在堆中使用句柄池,在方法区中使用直接引用。有一些虚拟机是把老年代放在方法去中的(可以看一下垃圾回收过程:传送门)。
这样也可以理解为什么句柄池放在堆中了。