Chapter3-垃圾收集器与内存分配策略

  • 3.1 概述
    • 垃圾收集(Garbage Collection)关注三件事:1. 哪些内存需要回收?2. 什么时候回收?3. 如何回收?
    • 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,不需要考虑回收
    • 而Java堆和方法区两个区域有着很显著的不确定性:只有处于运行期间,我们才能知道程序究竟会创建哪些对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。
  • 3.2 对象已死?
    • 3.2.1 引用计数算法
      • 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。它在大多数情况下它都是一个不错的算法,简单高效。
      • 问题:单纯的引用计数很难解决对象之间相互循环引用的问题,例如A和B互相引用。
    • 3.2.2 可达性分析算法
      • 基本思路:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

      • 固定GC Roots对象:
        • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
        • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
        • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
        • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
        • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
        • 所有被同步锁(synchronized关键字)持有的对象。
        • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
        • 还可以有其他对象“临时性”地加入,比如某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。
    • 3.2.3 再谈引用
      • 在JDK 1.2版之后,引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
      • 强引用是即“Objectobj=new Object()”这种引用关系。任何情况下,垃圾收集器永远不会回收掉被强引用的对象。
      • 软引用是用来描述一些还有用,但非必须的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,
      • 弱引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
      • 虚引用是最弱的一种引用关系。虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
    • 3.2.4 对象死亡
      • 在可达性分析算法中判定为不可达的对象,它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
      • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
    • 3.2.5 回收方法区
      • 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
      • 回收废弃常量与回收Java堆中的对象非常类似。而要判定一个类型是否属于“不再被使用的类”需要同时满足下面三个条件:
        • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
        • 加载该类的类加载器已经被回收
        • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 3.3 垃圾回收算法
    • 垃圾收集算法可以划分为“引用计数式垃圾收集”(ReferenceCounting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。
    • 3.3.1 分代收集理论
      • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
      • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
      • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
      • 对于少量的跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GCRoots进行扫描。
      • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,
        • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
        • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
        • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
        • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
    • 3.3.2 标记-清除算法
      • 分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。
      • 主要缺点:
        • 1. 是执行效率不稳定,标记和清除两个过程的执行效率都随对象数量增长而降低;
        • 2. 第二个是内存空间的碎片化问题,可能会导致需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
    • 3.3.3 标记-复制算法
      • 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

      • 现在大多都优先采用了这种收集算法去回收新生代,但并不需要按照1∶1的比例来划分新生代的内存空间,而是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。
      • 发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%。
      • 当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(老年代)进行分配担保(Handle Promotion)。
    • 3.3.3 标记-整理算法
      • 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。解决了内存碎⽚化问题。

      • 是否移动对象都存在弊端,移动则内存回收时会更复杂(不适用于老年代),不移动则内存分配时会更复杂(空间碎片问题)。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,但是从整个程序的吞吐量来看,移动对象会更划算。
  • 3.4 HotSpot的算法细节实现
    • 3.4.1 根节点枚举
      • 根节点枚举必须暂停用户线程,尽管查找引用链的过程已经可以做到与用户线程一起并发,但根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行。使用一组称为OopMap的数据结构
      • HotSpot中的解决⽅案是使⽤⼀组称为OopMap的数据结构,OopMap存储两种引⽤:(1)栈⾥和寄存器内的引⽤:在即时编译中,在特定的位置记录下栈⾥和寄存器⾥哪些位置是引⽤;(2)对象内的引⽤:类加载动作完成时,HotSpot 就会计算出对象内什么偏移量上是什么类型的数据;
    • 3.4.2 安全点
      • HotSpot不为每条指令都生成OopMap,只是在安全点(Safepoint)记录了这些信息。强制要求必须执行到达安全点后才能够暂停。
      • 抢先式中断(Preemptive Suspension):系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。
      • 主动式中断(Voluntary Suspension):是当垃圾收集需要中断线程的时候,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
    • 3.4.3 安全区域
      • 用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,安全区域能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。
      • 当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举,如果完成了,那线程就继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
    • 3.4.4 记忆集与卡表
      • 为解决对象跨代引用的问题,垃圾收集器在新生代中建立了记忆集(Remembered Set)数据结构,用以避免把整个老年代加进GC Roots扫描范围。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
      • 只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针:
        • 字长精度:每个记录精确到一个机器字长,该字包含跨代指针。
        • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
        • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
      • 第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,最常用。
      • 判断方法:只要卡页内有一个对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
    • 3.4.5 写屏障
      • HotSpot虚拟机通过写屏障(Write Barrier)技术维护卡表状态,一个在机器码层面的手段,把维护卡表的动作放到每一个赋值操作之中。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,在赋值前的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的叫作写后屏障(Post-Write Barrier)。直至G1收集器出现之前,其他收集器都只用到了写后屏障。
    • 3.4.6 并发的可达性分析
      • 三色标记(Tri-color Marking):
        • 白色:表示对象尚未被垃圾收集器访问过。若在分析结束的阶段,仍然是白色的对象,即代表不可达。
        • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。
        • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
      • 解决并发扫描时的对象消失问题:
        • 增量更新:黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
        • 原始快照:无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照来进行搜索。
  • 3.5 经典垃圾收集器
    • 七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用

    • 3.5.1 Serial收集器
      • Serial收集器是最基础、历史最悠久的收集器。它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”。
      • 简单而高效(与其他收集器的单线程相比),额外内存消耗(Memory Footprint)最小;如果处理器核心数较少,Serial收集器可以获得最高的单线程收集效率,适合客户端模式。

    • 3.5.2 ParNew收集器
      • ParNew收集器实质上是Serial收集器的多线程并行(不是并发,用户线程仍需要等待)版本。ParNew实际上成为CMS专门处理新生代的部分。单核不如Serial收集器。

    • 3.5.3 Parallel Scavenge收集器
      • Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
      • Parallel Scavenge收集器的关注点是达到一个可控制的吞吐量(Throughput),CMS等收集器的关注点是尽可能地缩短用户线程的停顿时间。
    • 3.5.4 Serial Old收集器
      • Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。主要意义也是供客户端模式下的HotSpot虚拟机使用。

    • 3.5.5 Parallel Old
      • Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

    • 3.5.6 CMS收集器
      • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
      • CMS收集器基于标记-清除算法,整个过程分为四个步骤:
        • 1)初始标记(CMS initial mark):仅标记GCRoots能直接关联到的对象。
        • 2)并发标记(CMS concurrent mark):遍历整个对象图,但不需要停顿用户线程。
        • 3)重新标记(CMS remark):修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
        • 4)并发清除(CMS concurrent sweep):清理标记阶段判断的已死亡对象,也不需要停顿。
      • CMS实现并发收集、低停顿,但仍有三个缺点:
        • 1. CMS收集器对处理器资源非常敏感
        • 2. 无法处理“浮动垃圾”(Floating Garbage),可能导致Full GC的产生。
        • 3. 基于“标记-清除”算法,会有大量空间碎片产生。
    • 3.5.7 Garbage First收集器
      • Garbage First(简称G1)收集器开创了面向局部收集和基于Region的内存布局形式。JDK 9后,G1取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。
      • G1收集器采用Mixed GC,其考虑哪块内存中存放的垃圾数量最多,回收收益最大。G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以扮演新生代的Eden空间、Survivor空间,或者老年代空间,对扮演不同角色的Region采用不同的策略去处理。
      • Region中还有一类特殊的Humongous区域,专门用来存储大对象。大多数都把Humongous Region作为老年代的一部分来进行看待。
      • 优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。它将Region作为单次回收的最小单元,避免在整个Java堆中进行全区域的垃圾收集。
      • G1收集器的运作过程大致可划分为以下四个步骤:
        • 初始标记(Initial Marking):仅标记GC Roots能直接关联到的对象,并且修改TAMS指针的值
        • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,可与用户程序并发执行。
        • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
        • 筛选回收(Live Data Counting and Evacuation):负责更新Region的数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
      • G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,是在延迟可控的情况下获得尽可能高的吞吐量。

  • 3.6 低延迟垃圾收集器
    • 衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”。
    • 3.6.1 Shenandoah收集器
      • 相对于G1有三个优点:
        • 1. 支持并发的整理算法。G1的回收阶段是可以多线程并行的,但却不能与用户线程并发,这点作为Shenandoah最核心的功能。
        • 2. Shenandoah(目前)是默认不使用分代收集的。
        • 3. Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(ConnectionMatrix)的全局数据结构来记录跨Region的引用关系。
    • 3.6.2 ZGC收集器
      • ZGC(Z Garbage Collector)是一款在JDK 11中新加入的低延迟垃圾收集器,ZGC和Shenandoah的目标都希望尽可能对吞吐量影响不太大,把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
      • ZGC的Region(Page或者ZPage)具有动态性——动态创建和销毁,以及动态的区域容量大小。ZGC的Region可以具有如图的大(动态变化)、中(32MB)、小(2MB)三类容量。
      • 采用的染色指针技术(Colored Pointer),一种直接将少量额外的信息存储在指针上的技术。染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
  • 3.7 选择适合的垃圾收集器
    • 3.7.1 Epsilon收集器
      • 这是一款以不能够进行垃圾收集为“卖点”的垃圾收集器,如果应用只要运行数分钟甚至数秒,只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那运行负载极小、没有任何回收行为的Epsilon便是很恰当的选择。
    • 3.7.2 收集器的权衡
      • 如果是数据分析、科学计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点。
      • 如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点。
      • 如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的。
  • 3.8 内存分配与回收策略
    • 1. 对象优先在Eden分配:当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
    • 2. 大对象直接进入老年代:需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。
    • 3. 长期存活的对象将进入老年代:对象在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。
    • 4. 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
    • 5. 空间分配担保:发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。
  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值