JVM面试整理--对象的创建和堆

文章目录


对象的创建过程是怎样的?

答:

对象的创建过程大致可分为如下四个步骤:

  • 类加载检查。JVM遇到 new 指令,会去检查能否在常量池中定位到类的符号引用,并且检查该符号引用代表的类是否已经被加载、解析和初始化过。如果没有,则进行类加载过程。
  • 为对象分配内存。内存分配方式可以选择"指针碰撞"或是"空闲列表"。具体选择哪一种,是由堆内存是否规整决定的。
  • 将内存空间初始化为零值(除对象头)。这一步保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用。
  • 为对象进行必要的信息设置。这些信息主要存放在对象头(Object Header)之中。对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。还有关于对象是否加锁的信息。
    对象头的内容如下:

助记方法:

1.类是对象的基础,只有加载到jvm的类,才可以创建其对象 -> 类加载检查。

2.对象是需要存储信息的 -> 就需要为其分配内存。

3.对象的信息称为实例变量,实例变量需要赋值才可使用 -> 初始化零值

4.对象头保存了对象的元数据->这些信息是在对象创建时设置的


对象在内存中的结构是怎样的(专业的叫法:对象的内存布局)

答:

再次引用前面的图:

对象在堆内存中存储,其内存布局可分为3部分:

对象头、实例数据和对齐填充。

  • 对象头(Object Header)。
    又可再分为两部分:第一部分,官方称为"MarkWord",存储的是对象自身的运行时数据。如:哈希码(HashCode)、GC分代年龄、锁状态标志、偏向线程ID等;第二部分是"类型指针"(Klass Pointer)。即对象指向它的类元数据 的指针,虚拟机可通过其确定该对象是哪个类的实例。如果是数组对象,其对象头还会有一块用于记录数组长度的数据。
  • 实例数据(Instance Data)。
    真正存储有效信息的部分,这里主要指对象的实例字段。包含从父类继承的字段信息,相同宽度的字段总是分配在一起。
  • 对齐填充(Padding)
    因为对象的大小必须是8字节的整数倍。对象头是8字节的整数倍,但是实例数据则不一定,当其大小不足8字节时,需要对其进行填充,以达到8字节。

下图给出了对象头的空间占用情况:


对象在内存分配时使用的哪种方式(有的地方也称为:分配算法)

答:对象在内存分配时,会使用到两种内存分配方式:

  • 指针碰撞
  • 空闲列表

具体使用哪一种内存分配方式,取决于堆内存是否规整。而堆内存是否规整,则决定于垃圾收集器是否带有整理功能(即是否采用了"标记-整理"垃圾收集算法)。

因此,带压缩功能的Serial New等收集器,采用 "指针碰撞" 分配算法。

而像CMS这种基于 Mark-Sweep 算法的收集器,则采用 "空闲列表" 分配算法。


知道什么是"指针碰撞"吗?

答:

一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。

Java虚拟机为新生对象分配内存时。如果Java堆中的内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放在另外一边,中间以一个指针作为分界点的指示器(如图所示)。那么分配内存所要做的仅仅是把指针向空闲内存方向挪动,挪动的距离正好等于对象的大小,这种内存分配方式就称为"指针碰撞"。

助记点:

1.堆内存需绝对规整(结合图加深理解)。关键字:已使用内存空闲的内存指针分界点的指示器

2.分配内存,即指针向空闲内存方向移动。关键词:向空闲内存的方向移动

3.内存分配方式,称为"指针碰撞"。关键词:内存分配方式


知道什么是"空闲列表"吗?

答:

堆内存不规整 (即已使用内存和空闲内存是交错在一起的)时,是不适合使用"指针碰撞"的方式为对象分配内存的。

而对象的内存是需要一整块连续区域的。为了能快速找到合适大小的内存,虚拟机就需要维护一个列表,用于记录++哪些内存可用,其大小是多少++。

当分配内存时,借助于该列表可以很快找到合适的内存。列表中已分配内存的记录需要进行更新(删除还是打标已使用? )。

这种内存分配方式,称为"空闲列表"。


内存分配算法是否存在并发问题?是如何解决并发问题的?

答:

内存分配时,不管是使用 "指针碰撞" 还是 "空闲列表" 分配算法,都有可能产生并发问题。

因为可能有多个线程发起了对象创建,从而出现多个线程对同一个内存位置的争抢情况,结果导致某些线程中对象创建失败。

为了解决并发问题,JVM给出了两种方案:

  • CAS + 失败重试
  • TLAB(本地线程分配缓冲)

请说一说"TLAB"

答:TLAB是本地线程分配缓冲(Thread Local Allocation Buffer)的缩写。有的地方也翻译为本地线程分配缓存。

它的工作原理:为每一个线程在堆中预先分配一小块内存,称其为TLAB。为线程分配堆内存时,优先从TLAB上分配,当TLAB用完后,再使用同步锁定的方式分配新的TLAB。

虚拟机通过参数 -XX:+/-UseTLAB 决定是否启用TLAB功能。


对象创建好后,如何对其进行定位访问

答:对象创建好后,可以通过如下方式对其进行定位访问:

  • 使用句柄
  • 直接指针

使用句柄访问, 将会从堆中划分出一块内存来作为句柄池,reference(栈中的本地变量表) 中存储的是对象的句柄地址,而句柄中包含了对象实例数据指针与对象类型数据指针(二元组)。

使用句柄访问:

使用句柄访问的好处:

reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
助记关键词:
句柄池句柄地址-存在本地变量表中句柄是一个二元组<实例数据指针, 类型数据指针>

使用直接指针访问,reference 中存储的直接就是对象地址 。而对象的头信息中包含对象类型数据的信息(称为对象类型指针) 。

使用直接指针访问:

直接指针访问的好处:

速度更快,节省了一次指针定位的时间开销。
助记关键词:
reference直接指向对象地址对象头中包含类型信息


jvm中堆的结构是怎样的?新生代、老年代

答:

Java堆可分为新生代和老年代,新生代和老年代的比例为1:2。这个比例可通过-XX:NewRatio参数进行调整。

新生代又可细分为一块较大的Eden区和两块较小的Survivor区。默认Eden和Survivor大小的比例是8:1。这个比例可以通过 -XX:SurvivorRatio 参数进行调整。

JVM每次只会使用Eden和其中一块Survivor区域来为对象服务,所以总是有一块Survivor区域是空闲的。

所以新生代的实际可用内存空间为9/10的新生代内存空间。

新生代存储的都是新创建的对象、比较小的对象;而老年代存的都是活得久的,或是比较大的对象。所以,老年代占JVM堆内存的比例较大。

下图是新生代和老年代的比例:


JVM中老年代和新生代的比例是多少?

答:新生代和老年代的默认比例是1:2。这个比例可通过-XX:NewRatio参数进行调整。

注:和堆的内存结构重复,单独拎出来是为了强调该知识点的重要性。


说说对象的分配规则。

答:

目前主流的虚拟机都是使用的分代收集算法。对应的内存的分配也是分代分配的。

规则如下:

  • 对象优先分配在Eden区。如果启用了TLAB,则优先在TLAB上分配。当Eden区没有足够空间时,会触发一次Minor GC(YGC)。
  • 大对象(指需要大量连续内存空间的对象,如长字符串和长数组)直接进入老年代。
  • 长期存活的对象进入老年代。对象头的MarkWord中有一个GC分代年龄,默认分代年龄大于15的对象进入老年代。对象首次进入Survivor区后,其对象年龄设为1,然后在Survivor中每熬过一次Minor GC,其对象年龄就加1,直至对象年龄达到阈值,对象直接进入老年代(也称为晋升为老年代)。可通过设置参数-XX:MaxTenuringThreshold修改对象进入老年代的年龄阈值。该规则针对的是Survivor中的对象
  • 动态对象年龄判断。其概念为:如果Survivor中相同年龄的对象的总大小大于Survivor空间的一半,年龄大于等于该年龄的对象,直接进入老年代。注意:这条规则也是针对Survivor中的对象的。 例如:Survivor空间=2M,当中有4个对象:

    对象1:大小-0.5M,年龄-3

    对象2:大小-0.6M,年龄-3

    对象3:大小-0.1M,年龄-2

    对象4:大小-0.2M,年龄-4

    根据动态对象年龄判断的规则:对象1+对象2的总大小 > 1M;

    所以,Survivor中年龄大于等于3的对象,都将进入老年代。

    故:对象1、对象2和对象4,都将进入老年代。

  • 空间分配担保。

助记关键词:
Eden区-优先分配大对象Survivor-长期存活的对象Survivor-基于同龄对象占比空间分配担保

参考:

《深入理解Java虚拟机(第2版)》p95、3.6节


什么是空间分配担保?

答:

每次在进行Minor GC之前,JVM会检查老年代最大可用连续空间 是否大于新生代所有对象的总空间

如果大于,说明Minor GC是安全的,进行Minor GC。

帮助理解 =>(因为Minor GC是对新生代空间的回收,如果其中的对象还需要使用,则需要将其移动到老年代,所以就需要老年代有足够的空间存放这些对象。这里考虑的是最极端的情况,即所有的新生代对象都将进入老年代)

如果老年代的最大可用连续空间,放不下所有的新生代对象,那么是否大于历次晋升到老年代的对象的平均大小。如果满足,则进行Minor GC,否则进行Full GC。

担保是由老年代作出的,并且主要发生在第二次判断中。老年代根据历史的经验值,对Minor GC的结果作出担保,保证GC后老年代的最大连续空间可以容纳GC后晋升到老年代的对象大小。

担保依然有失败的可能,因为Minor GC后存活的对象可能会很多,以至于大于老年代的可用连续空间。所以就有了第三个判断。

如果担保失败,则在Minor GC后,会再次触发一次Full GC(如图所示)。

下图演示了"空间分配担保"的流程:

JDK6 Update24之前的空间分配担保流程如下:

声明:大部分图片来源自互联网和书籍。


参考:

相关推荐
独行soc1 小时前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
sszmvb12343 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺3 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉3 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船3 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
ZL不懂前端3 小时前
Content Security Policy (CSP)
前端·javascript·面试
AAA 建材批发王哥(天道酬勤)4 小时前
JVM 由多个模块组成,每个模块负责特定的功能
jvm
测试界萧萧4 小时前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
百事老饼干4 小时前
Java[面试题]-真实面试
java·开发语言·面试