1.前言
在读这篇博客之前,你需要了解分代收集理论中,收集器应该将Java堆划分出不同的区域**,**然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
例如appel式回收,HotSpot虚拟机中的新生代收集器都采用了appel式回收来设计新生代内存布局。
Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新 生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会 被“浪费”的。当然,98%的对象可被回收仅仅是“普通场景”下测得的数据,任何人都没有办法百分百 保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安 全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。
所谓分配担保就是:如果另外一块 Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直 接进入老年代,这对虚拟机来说就是安全的。
在将Java堆内存划分为不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域(Minor GC、Major GC、Full GC)。进而演化出与对象存亡特征相匹配的垃圾收集算法。
2.正文
1.对象优先在Eden区分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进分配时,虚拟机将发起一次MinorGC
2.大对象直接进入老年代
大对象指的是需要大量连续内存空间的对象,比如一个很长的字符串或者是一个很大的数组。
大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时大对象就意味着高额的内存复制开销。
HotSpot虚拟机提供了 -XX:PretenureSizeThreshold
参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
3.长期存活的对象将进入老年代
在前面我们了解了对象的创建过程中,会设置对象头,其中对象头中就包含了对象的年龄。
对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。
对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold
设置。
4.动态对象年龄判断
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到 -XX:MaxTenuringThreshold 才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 -XX:MaxTenuringThreshold
中要求的年龄。
5.空间分配担保
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看 -XX:HandlePromotionFailure
参数的设置值是否允许担保失败(Handle Promotion Failure)。
-XX:HandlePromotionFailure=true
代表允许担保失败;-XX:HandlePromotionFailure=false
代表不允许担保失败
如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者 -XX:HandlePromotionFailure
设置不允许冒险,那这时就要改为进行一次Full GC。
所谓冒险,在允许担保失败的情况下,将Survivor无法存放的存活对象,由老年代进行分配担保,这个步骤是有一定风险的。因为新生代中会有多少对象将发生晋升(新生代—>老年代)我们无法知道。
- 如果老年代中有足够的空间来容纳新生代中所有对象(最坏的情况,所有新生代对象都晋升),那么这种情况相对来说安全的多。
- 如果是老年代没有足够的空间来容纳新生代中所有对象,且我们无法知道有多少新生代对象发生晋升。简单来说就是我们无法确定老年代是否有足够的空间存放新生代中晋升的对象,我们只能以历史平均晋升值作为参考,那么这种情况就可能会发生担保失败。
取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实 地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下都还是会将-XX:HandlePromotionFailure
开关打开,避免Full GC过于频繁。
会将**-XX:HandlePromotionFailure
开关打开,避免Full GC过于频繁。
注意:该参数JDK1.7以后就废弃了, 只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC,否则Fu11GC文章来源:https://www.toymoban.com/news/detail-439688.html
本文参考自《深入理解Java虚拟机》(第三版)文章来源地址https://www.toymoban.com/news/detail-439688.html
到了这里,关于深入理解Java虚拟机——内存分配与回收策略的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!