深入理解Java虚拟机——内存分配与回收策略

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

本文参考自《深入理解Java虚拟机》(第三版)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少不入川。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值