企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 1.背景与大纲   在我们了解了java虚拟机的运行时数据区后,我们大概知道了虚拟机内存的概况,但是我们还是不清楚具体怎么存放的访问的;   接下来,我们将深入探讨HotSport虚拟机在java堆中对象的分配、布局、访问的全过程。 # 2.对象创建   ![](https://img.kancloud.cn/76/3f/763fe00c0772fa696f91e55e066eed60_747x372.png)   1.类加载:当遇到new指令时,先判断这个类是否被加载、解析、初始化过,如果没有,先执行相应类的加载过程(后面会详细分析这个过程)。   2.分配内存:   如果Java堆内存是规整连续的,采用“指针碰撞”的分配方式,   ![](https://img.kancloud.cn/d1/bf/d1bf1c1849def36a2ea299f9be3fa70c_448x253.png)   如果是不连续规整的,采用“空闲列表”分配方式。如下图:灰色表示已使用,数字表示可用   ![](https://img.kancloud.cn/09/52/09527213a3a4e16285f922cbd772acf6_676x319.png)   内存是否规整取决于垃圾收集器是否带有压缩整理功能。   Serial,ParNew等带有Compact过程的收集器,采用的分配算法是“指针碰撞”。   而CMS这种基于Mark-Sweep算法的收集器,通常采用“空闲列表”分配方式。   线程安全问题:即便是修改指针指向位置,A\\B两个线程有可能会指向同一个地址   ![](https://img.kancloud.cn/9f/04/9f04a52af161272b6586f2081a4d5fbd_667x318.png)   解决方案:   a.同步锁:   b.TLAB:本地线程分配缓冲,把内存的分配动作按照线程划分在不同的空间进行   等TLAB用完分配新的TLAB时,才需要同步锁   虚拟机是否使用TLAB,可以通过-XX:+/UseTLAB参数设定   ![](https://img.kancloud.cn/15/37/1537f67d355aa601a40668cf5f6fb395_589x282.png)   3.对象初始化为零对象: *  a.如果使用的是TLAB,这一步可以提前到TLAB分配的时候进行   b.作用:保证了实例字段在java代码中可以不赋初始值就可以直接使用*   4.对象头进行设置,   包括这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。   5.java程序初始化,最后执行init方法,把对象按照程序员的意愿进行初始化。这样一个真正可用的对象才算完全生产出来。 # 3.对象内存布局     ![](https://img.kancloud.cn/b4/b5/b4b5414397e0b536897e20a587b22bc2_633x530.png)   对象内存布局分为三块区域 ## 3.1.对象头(Header)   对象头主要包括:运行时数据、类型指针 ### 3.1.1.运行时数据   对象头,存储对象自身的运行时数据,如哈希码、对象的GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳,   1.这部分数据的长度在32位和64位虚拟机中,分别为32bit和64bit。   2.官方称其为:Mark Word   3.存储的运行时数据过多(超出32bit、64bit)   4.Mark Word被设计成为一个非固定数据结构,以便在极小的空间内存存储尽量多的信息   ![](https://img.kancloud.cn/0e/f7/0ef706e36bfe2b061153dfd0eb51366b_787x279.png)   5.根据对象的状态复用自己的存储空间 ###  3.1.2.类型指针   另一个部分是类型指针   1.定义:对象指向它的类元数据的指针   2.作用:虚拟机通过这个指针来确定这个对象是哪个类的实例   3.但是:并不是所有的虚拟机实现都必须在对象上保留类型指针   4.结论:查找对象的元数据信息并不一定要经过对象本身(下一节详细讲) ### 3.1.3.特例   另外,如果对象是java数组   1.对象头必须有一块用于记录数组长度的数据   2.原因:普通java对象的元数据信息可以确定java对象的大小   3.但是,从数组的元数据中却无法确定数组的大小 ## 3.2.实例数据(Instance Data)   1.存储内容:是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容   2.存储范围:无论是父类还是子类都要记录   3.存储顺序:存储顺序会受到虚拟机分配策略参数(fieldsAllocationStyle)和字段定义顺序的影响   4.默认分配策略顺序:按字节由大到小(即由宽到窄),   longs/doubles->ints->shorts/chars->bytes/booleans->oops(Ordinary Object Pointers)普通对象指针   5.在满足分配策略这个前提条件下,父类中定义的变量会出现在子类之前   6.如果CompactFields参数值为true,那么子类中较窄的变量也可能会插入到父类变量的空隙之中 ## 3.3.对齐补充(Padding)   1.作用:对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。   2.原因:由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。   3.解决:而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 # 4.对象的访问定位   ![](https://img.kancloud.cn/47/97/4797ad8cfe805a975e0d06503002c432_747x268.png)   对象的访问定位   1.Java程序需要通过栈上的reference数据来操作堆上的具体对象。   2.由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,   3.并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,   4.所以对象访问方式也是取决于虚拟机实现而定的。   5.目前主流的访问方式有使用句柄和直接指针两种。   6.总结:通过reference数据来定位具体对象,但reference只规定了对象的引用,并没有提供具体实现   ![](https://img.kancloud.cn/9b/bb/9bbb6da3faadc3b14bbda26098e80733_908x581.png) ## 4.1.句柄方式   1.Java堆中将会划分出一块内存来作为句柄池,   2.reference中存储的就是对象的句柄地址,   3.而句柄中包含了对象实例数据与类型数据各自的具体地址信息。   优点:   使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,   在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。 ## 4.2.直接指针方式   1.Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,   2.而reference中存储的直接就是对象地址。   优点:   使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,   由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。 ## 4.3.案例   虚拟机Sun HotSpot 使用的是直接指针的方式!