第三章 垃圾收集器与内存分配策略

第三章 垃圾收集器与内存分配策略

概述

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

对象已死?

  • 引用计数法:在对象中添加一个引用计数器,被引用时计数器加1,引用失效时计数器减1,为0时就是未被使用,可以被垃圾收集器回收。
    • 优点:简单,高效
    • 缺点:存在循环引用的问题
  • 可达性分析算法:通过一系列被称为GC Roots 的根对象作为起始集点,判断这些集合中对象的引用所组成的引用链,不在这些引用链中的对象就是未被引用的,也就是不可达对象,这些对象可以被垃圾回收器标记为可回收状态,以便后续垃圾回收器回收。
    • 在java技术体系中,可作为GC Roots的对象包括以下几种:
      • 方法区中类静态属性引用的对象
      • 在方法区中常量引用的对象
      • 在本地方法栈中JNI(java native interface)引用的对象
      • JVM内部引用
      • 所有被同步锁(Synchronized关键字)持有的对象
      • 反映JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
      • 其他的一些由于选择不同的垃圾收集器而“临时”加入的对象

引用的分类

  • 强引用(Strongly Reference)
    • 最传统的“引用”定义,指在程序之中普遍存在的引用赋值,如:Object obj = new Object() 这种引用关系。
    • 无论在任何情况,只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。
  • 软引用 (Soft Reference)
    • 用来描述一些还有用,但费必须的对象。
    • 只被软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用 (Weak Reference)
    • 也是用来描述那些非必须对象,但是它的强度比软引用更弱一些
    • 当垃圾收集器工作的时候,无论内存是否足够,都会回收掉只被软引用关联的对象。
  • 虚引用 (Phantom reference)
    • 虚引用是最弱的一种引用关系。
    • 一个对象是否有虚引用对其生命周期完全不影响,也不能通过虚引用来取得一个对象实例
    • 为一个对象设置一个虚引用的唯一目的就是在对象被垃圾回收器回收的时候收到一个系统通知。

回收方法区

  • 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型
    • 回收废弃的常量
      • 回收废弃常量和回收堆中的的对象过程十分相似
    • 回收不再使用的类型,需要同时满足三个条件:
      • 该类的所有实例全部都被回收
      • 加载该类的类加载器全部被回收
      • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

JVM被允许对满足这三个条件的无用类进行回收,这里仅仅只是可以被允许回收,而不是贺对象一样,没有引用就必然回收。

垃圾收集算法

垃圾收集算法的实现细节,推荐阅读Richard Jones撰写的《垃圾回收算法手册》第2-4章的相关内容。

  • 分代收集理论

    • 分代收集名为理论,实质上是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
    • 弱分代假说(Weak Generational Hypothesis):觉得大多数的对象都是招生夕灭的。
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

  • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说只占极少数。

  • 对象分代设计:

    • 设计者一般至少将堆内存划分为新生代(Young Generaton)和老年代 (Old Generation)
    • 新生代大多数对象都是朝生夕灭,而少量没有朝生熄灭的对象将会逐步晋升到老年代(对象每熬过一次新生代垃圾收集器的回收,对象的分代年龄计数器则会加一,是否晋升到老年代则由分代年龄计数器的数值确定)
  • 垃圾收集算法

    • 标记-清除算法
      • 首先,标记出需要回收的对象。然后,统一回收所有被标记的对象。也可以反过来,首先标记存活的对象,然后,统一回收未被标记的对象。
      • 缺点
        • 执行效率不稳定
        • 会产生大量不连续的内存碎片
    • 标记-复制算法
      • 将可用内存区域划分为大小相等的两块。当一块内存用完了,将存活的对象复制到另一块上,然后清空用完的这块内存。
      • 优点
        • 当存活对象较少时,实现简单,运行高效
        • 不用考虑内存碎片问题
      • 缺点
        • 当存活对象较多时,会产生大量的复制开销
        • 内存有一半处于空闲状态,内存空间浪费太多了
    • 标记-整理算法
      • 针对老年代有大量存活的对象的存亡特征,标记-整理算法是将存活的对象贺被标记的对象同时向内存空间一端移动,然后直接清除掉边界以外的被标记需要清除的内存。
      • 是否移动存活对象是一项优缺点并存的风险决策:
        • 移动:则存在Stop The world 更新引用
        • 不移动:内存碎片导致内存分配和管理更复杂

HotSpot的算法细节实现

  • 根节点枚举
    • 枚举根节点时JVM必须要停顿
    • HotSpot解决方案里是使用一组被称为OopMap的数据结构来达到这个目的。类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中也会在特定的位置记录下栈里和寄存器里哪些位置在引用,这样收集器在扫描时就可以直接得知这些信息,并不需要从GC Roots开始查找
  • 安全点
    • 安全点的选取
      • 基本上是以“是否具有让程序长时间执行的特征”
      • 最明显的特征就是指令复用(例如:方法调用、循环跳转、异常跳转等)
    • 如何跑到安全点然后停顿下来
      • 抢先式中断(Preemptive Suspension)
        • 系统把所有用户线程全部中断,发现用户线程不在安全点上时,恢复此线程执行,让它一会儿再重新中断,直到它跑到安全点上。
      • 主动式中断 (Voluntary suspension)
        • 不直接对线程操作,简单的设置一个标志位,各个线程主动不停的轮训这个标识位,一旦发现中断标识位为真就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的。
  • 安全区域
    • 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。也可以把安全区域看作是被扩展拉伸了的安全点。
  • 记忆集与卡表
    • 记忆集(Remembered Set)是一种用于记录从非收集区域指向收集区域的数据结构
    • 记忆集的实现方式
      • 字长精度:每个记录精确到一个字长(64位/32位机器物理寻址的指针长度)该字包含跨代指针
      • 对象精度:每个记录精确到一个对象,该对象有字段包含跨代指针。
      • 卡精度:每个记录精确到一块内存区域,该区域内有对象包含跨代指针。
        • 卡精度是指用“卡表”的方式实现记忆集(目前常用的一种实现记忆集的方式),卡表与记忆集的关系,可以用Java语言中的HashMap与Map的关系类比理解。
  • 写屏障
    • 维护卡表,为卡表赋值时使用的一种技术
    • 写前屏障
    • 写后屏障
    • 卡表在高并发更新中存在伪共享(False Sharing)问题
    • 现代中央处理器的缓存系统是以缓存行为单位存储的
    • JDK7之后,HotSpot JVM 增加了-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。
  • 并发的可达性分析
    • 按照“对象是否被访问过”分为三色标记:
      • 白色:对象尚未被垃圾收集器访问过
      • 黑色:对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。
      • 灰色:对象已经被垃圾收集器访问过,但这个对象至少存在一个引用还没有被扫描过。
    • 对象消失的问题:
      • 赋值器插入了一条或多条从黑色对象到白色对象的新引用。
      • 赋值器删除了全部从灰色对象到该白色对象的直接或者间接引用。
    • 解决并发扫描对象消失的问题
      • 增量更新(Incremental Update)
        • 增量更新破坏的是第一个条件,步骤为:记录新增的插入引用,并发扫描结束后,以插入的黑色为根重新扫描。
      • 原始快照(Snapshot At The Beginning,STAB)
        • 原始快照破坏的是第二个条件,步骤为:记录被删除的引用,并发扫描结束后,以删除的灰色为根重新扫描。

经典垃圾收集器

  • Serial/Serial Old收集器
    • 单线程垃圾收集器
    • Serial新生代垃圾收集器,Serial Old老年代收集器
    • 基于标记-复制算法的垃圾收集器
    • 运行时会暂停所有用户线程(Stop The World)
    • 简单,高效,对内存资源受限的环境它是所有收集器里面额外内存开销最小的
    • 对于运行在客户端模式下的应用是很好的选择
  • ParNew 收集器
    • Serial 收集器的多线程版
    • 拥有Serial的很多相同的参数,算法,行为
    • 可以和CMS垃圾收集器协作
  • Parallel Scavenges收集器
    • 新生代垃圾收集器
    • 并行收集的垃圾收集器
    • 基于标记-复制算法实现的垃圾收集器
    • 以控制吞吐量为目标的垃圾收集器,“吞吐量优先的收集器”
  • Parallel Old收集器
    • Parallel Scavenges收集器的老年代版本
    • 老年代垃圾收集器
    • 基于标记-整理算法的垃圾收集器
    • 在吞吐量优先和资源受限的场合,都可以优先考虑Parallel Scavenges加Parallel Old收集器这个组合
  • CMS 收集器
    • 以获取最短停顿时间为目标的收集器
    • 多线程垃圾收集器
    • 老年代垃圾收集器
    • 基于标记-清除的垃圾收集器
    • CMS收集器运行过程步骤:
        1. 初始标记(CMS initial mark)
        • 从GC Roots 开始标记
        • 会有短暂的Stop The World
        • 运行时间较短
        1. 并发标记(CMS concurrnet mark)
        • 与用户线程并发执行
        • 不存在Stop The World
        • 运行时间较长
        1. 重新标记(CMS remark)
        • 重新修正由于用户线程运行导致的内存引用的变动
        • 比初始标记耗时长,但远比并发标记耗时短
        1. 并发清除(CMS concurrent sweep)
        • 并发清除之前步骤标记的可回收内存
        • 不需要移动对象,所以可以和用户线程共同运行,不存在Stop The World
        • 运行时间较长
  • GarBage First 收集器(里程碑)
    • 开创了基于局部收集的思路和基于Region的内存布局形式
    • “停顿时间模型”的收集器:在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间大概率不超过N毫秒这样的目标
    • 每一个Region区域都可以扮演新生代、老年代
    • 用特殊的Humongous区域,专门存放大对象。(超过一个Region的一半认为是大对象)
    • 每个Region大小可以使用参数-XX:G1HeapRegionSize设定,取值范围1M-32M,且应为2的N次幂
    • 对于超过了整个Region容量的大对象,将会被存放在N个连续的用特殊的Humongous区域区域。
    • G1之所以能建立预测停顿时间的模型,时因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行安全区域的垃圾收集。
    • 更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收获取的空间大小以及回收所需要的时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定的允许收集停顿时间(使用参数-XX:MaxGCPauseMillis指定。默认值是200毫秒)优先处理回收价值收益最大的那些Region,这也是“GarBage First” 名字的由来。
    • G1垃圾收集器的运作过程大致可以分为以下四个步骤:
        1. 初始标记
        1. 并发标记
        1. 最终标记
        1. 筛选回收
        • 负责更新Region的统计数据,对各个Region的回收价值贺成本进行排序,根据用户所期望的停顿时间来制定回收计划,可自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region存活的对象复制到空的Region中,再清理掉整个Region的全部空间。
    • 从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个Java堆全部清理干净。

JDK9之后官方不再推荐ParNew和CMS的组合,推出G1收集器,G1收集器不需要其他垃圾收集器的配合。

  1. 并行(Parallel):并行描述的时多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态的。
  2. 并发(Concurrent):并发描述的是多条垃圾处理器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然可以响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定的影响。

低延迟垃圾收集器

  • 内存占用

  • 吞吐量

  • 低延迟

  • Shenandoah收集器

    • RedHat公司独立发展,2014年贡献给OpenJDK,并推动它成为OpenJDK 12特性之一
    • OpenJDK存在,OracleJDK不存在
    • 有三点和G1不一样
        1. 支持并发整理算法,G1虽然是多线程的,但是不支持和用户线程并发
        1. 其次,Shenandoah(目前)是默认不使用分代收集的,换言之,不会有 专门的新生代Region或者老年代Region的存在,没有实现分代,并不是说分代对Shenandoah没有价值, 这更多是出于性价比的权衡,基于工作量上的考虑而将其放到优先级较低的位置上。
        1. Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗。
    • Shenandoah 的工作过程大致可以分为9个过程:
        1. 初始标记:与G1一样
        1. 并发标记:与G1一样
        1. 最终标记:与G1一样
        1. 并发清理:清理那些整个区域没有一个存活对象的Region。
        1. 并发回收:在不暂停用户线程的情况下,同时将存活对象复制到空的Region区域(解决方案:“Brook Pointers”转发指针)
        1. 初始引用更新:将堆中所有指向旧引用对象的指针修正到复制后的新地址,会产生短暂的暂停,实际上不进行更新操作,只是建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已经完成分配给他们的对象移动任务而已。
        1. 并发引用更新:开始和用户线程一起工作,并发更新旧对象的引用到新对象,不按照对象图区更新,而是沿着物理地址线性的去把旧值更新为新值。
        1. 最终引用更新:修正GC Roots的引用,最后一次停顿,时间视GC Roots的大小而定
        1. 并发清理:经过之前的并发回收,并发更新之后,整个Regions区域再无存活的对象,再调用一次并发清理过程。
  • ZGC收集器

    • ZGC的工作步骤:
        1. 并发标记:与G1,Shenandoah一样
        1. 并发预备重分配:根据特定查询条件统计得出本次收集过程需要清理哪些Region,将这些Region组成重新分配集。
        1. 并发重分配:核心阶段,把重新分配集中的存活对象复制到新的Region上,并为重新分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
        1. 并发重新映射:目的是修正并发重分配过程修改的对象引用,但是这个过程不是很迫切,巧妙的放到了下次并发标记中去。修正完旧对象到新对象的引用后,记录集的内存就可以得到释放。
    • 染色指针技术:
      • 染色指针技术可以使得一旦某个Region的存活对象被移走后,这个Region立刻就能够被释放和重用掉,而不必等引用修正晚才能清理。
      • 染色指针技术可以大幅减少在垃圾收集过程中内存屏障的使用数量
      • 染色指针技术可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

选择合适的垃圾收集器

  • Epsilon 收集器
    • 一款不进行垃圾回收的垃圾收集器
    • 系统从运行到结束退出,内存分配够用,可以不用进行垃圾回收操作。

垃圾收集器并不能形容它的全部职责,更贴切的应该称为:“自动内存管理子系统”,一个垃圾收集器的职责除了垃圾收集这个本职工作外,它还负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责。

实战:内存分配与回收策略(HotSpot 客户端模式 Serial + Serial Old)

  • 对象优先在EDen区分配
  • 大对象直接进入老年代
  • 长期存活的对象进入老年代
  • 动态对象年龄判断
  • 空间分配担保

本章小结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

唐·王惜之

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

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

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

打赏作者

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

抵扣说明:

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

余额充值