文章目录
- 对象的创建过程是怎样的?
- 对象在内存中的结构是怎样的(专业的叫法:对象的内存布局)
- 对象在内存分配时使用的哪种方式(有的地方也称为:分配算法)
- 知道什么是“指针碰撞”吗?
- 知道什么是“空闲列表”吗?
- 内存分配算法是否存在并发问题?是如何解决并发问题的?
- 请说一说“TLAB”
- 对象创建好后,如何对其进行定位访问
- jvm中堆的结构是怎样的?新生代、老年代
- 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之前的空间分配担保流程如下:
声明:大部分图片来源自互联网和书籍。
参考:
-
[1] 《深入理解Java虚拟机(第2版)》
-
[2] https://blog.csdn.net/qyj19920704/article/details/123965383
-
[3] https://blog.csdn.net/guorui_java/article/details/137178686