《深入理解java虚拟机》读书笔记(三)内存分配与回收策略

1.对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

  • 由 -Xms20M(堆内存的初始大小)、-Xmx20M(堆内存的最大大小)、-Xmn10M(堆内新生代的大小)这三个参数限制了Java堆大小为20MB,不可扩展;
  • -XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1;
  • 执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,回收的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少(其他三个对象是存活的,没有被回收);
  • Minor GC的发生原因是allocation4分配内存时Eden中的剩余空间已经不足以分配它,且已有的三个2MB的对象无法放入Survivor空间,只能通过分配担保机制提前转移到老年代去。
  • 收集结束后,4MB的allocation4对象顺利分配至Eden中,最终Eden占用4MB,tenured占用6MB
private static final int _1MB = 1024 * 1024

/**
 * VM 参数 : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 */
 
public class TestAllocation {
 
    public static void main(String[] args) {
        testAllocation();
    }
 
    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
    }
}

笔者在复现这段代码时,采用的环境是jdk17,垃圾收集器为G1,打印出来的垃圾回收报告与书中有明显区别,后续会参考一些其他资料进行补充。

2.大对象直接进入老年代

在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。

HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数指定大于该设置值的对象直接在老年代分配,避免大对象在Eden区及两个Survivor区之间来回复制。该参数只对Serial和ParNew两款新生代收集器有效,HotSpot的其他新生代收集器,如Parallel Scavenge并不支持这个参数。如果必须使用该参数进行调优,可考虑ParNew加CMS的收集器组合

3.长期存活的对象将进入老年代

Hotspot虚拟机中多数收集器都采用了分代收集来管理内存,给每个对象定义了一个对象年龄计数器,存储在对象头中。对象通常在Eden区诞生,如果经过第一次Minor GC后仍然存活,且能被Survivor容纳的话,该对象会被移动到Survivor空间中,且年龄被设为1岁,此后每次在Survivor区中熬过一次Minor GC,年龄会增加1岁,直到大于阈值-XX:MaxTenuringThreshold。

当Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立则可以确保这一次Minor GC安全;否则,则需要查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败,如果允许,则继续检查老年代的最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行有风险Minor GC,否则进行Full GC。

4.Garbage First(G1)收集器

G1收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式,是垃圾收集器技术发展历史上的里程碑式的成果。它可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是分代,二是哪块内存中存放的垃圾数量最多,回收收益最大,及Mixed GC模式。

  1. 把连续的Java堆划分为多个大小相等的独立区域(Region),每个区域可以动态扮演不同的年代区;
  2. Region中有一类特殊的Humongous区域,专门用来存储大对象,超过Region容量的对象会被存放在N个连续的Humongous Region之中。
  3. 跟踪各个Region中垃圾堆积回收价值的大小,在后台维护一个优先级列表,每次根据用户设定的允许的收集停顿时间(-XX:MaxGCPauseMills),优先处理回收价值收益最大的Region。
    G1内存分区示意图

G1收集器的运作过程大致可划分为以下四个步骤:

  1. 初始标记:标记GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但是耗时短且借用Minor GC的时候同步完成,实际不产生额外停顿;
  2. 并发标记:从GC Roots开始对堆中对象进行科达兴分析,递归扫描整个堆里的对象图,找出要回收的对象,该阶段耗时长,但可与用户程序并发执行。
  3. 最终标记:对用户线程做短暂暂停,用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录;
  4. 筛选回收:更新各个区的统计数据,对回收价值和成本进行排序,根据期望停顿时间来制定回收计划,自由选择任意多个区构成回收集,然后把决定回收的部分Region的存活对象复制到空Region中,再清理掉整个旧Region的全部空间。回收操作必须暂停用户线程,由多条收集器线程并行完成。

G1从整体上看是标记-整理算法实现的收集器,从局部看是基于标记-复制算法实现的。相比与CMS,优点包括分区布局、自定义停顿时间、动态收益计算、不会产生空间碎片;缺点是程序运行时的额外执行负载高于CMS。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值