美文网首页
垃圾收集器与内存分配

垃圾收集器与内存分配

作者: Infinity233 | 来源:发表于2018-08-14 17:39 被阅读0次

第一次使用思维导图做笔记,在看过一次的基础上花了一早上和一下午的时间完成。感觉问题很大…………
下面是XMind自动到处的MarkDown文本。

垃圾收集器与内存分配

1.概述

GC的历史比Java更久,1960年诞生的Lisp是滴一门真正使用内存动态分配和垃圾收集技术的语言。

当Lisp还在胚胎时期时,人们就在思考GC需要完成的3件事情。

    1. 那些内存需要回收?
    1. 什么时候回收?
    1. 如何回收?

为什么要了解已经进入“自动化”时代的GC和内存分配?

  • 排查各种内存溢出、内存泄漏问题
  • 当GC成为系统达到更高并发量的瓶颈时。

哪些内存区域需要回收

  • 不需要过多考虑回收的区域:程序计数器、虚拟机栈、本地方法栈3各区域随线程而成,随线程而灭。每一栈帧中分配的内存在类结构确定下来时已知。这几个区域的内存分配和回收都具备确定性。
  • Java堆和方法区:一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,运行期才能知道。这部分的分配和回收都是动态的。

2.对象已死吗?

概述

  • 堆内存中存放着几乎所有的对象实例,回收前需要判断对象是否死亡。

非主流:引用计数算法(Reference Counting)

  • 给对象添加一个程序计数器,记录有多少个地方引用它。任何时刻计数器为0的对象不可能再被引用。实现简单,效率也高
  • 很难解决对象之间互相循环引用的问题。主流Java虚拟机里面没有选用其来管理内存

主流:可达性分析算法(Reachability Analysis)

  • 通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索做过的路径称为Reference Chain,当一个对象到GC Roots没有任何引用链相连(图论:不可达),证明此对象不可用。
  • 可作为GC Roots的对象
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(Native方法)引用的对象

再谈引用

  • 概述
    • 我们希望描述这样一类对象,当内存空间还足够时保留在内存中;如果内存空间在进行GC后依然紧张,则抛弃这些对象。
  • 四种强度依次减弱的引用类型
    • 强引用(Strong Reference)类似Object obj = new Object(); 只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
    • 软引用(Soft Reference)描述一些有用但非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    • 弱引用(Weak Reference)比软引用更弱。被过引用关联的对象只能活到下次GC之前。当GC器工作时,无论当前内存是否足够,都会被干掉。
    • 虚引用(Phantom Reference),一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个实例。唯一目的是能在这个对象被回收时收到一个系统通知。

生存还是死亡

  • 即时没通过可达性分析算法,也并非“非死不可”,真正的死亡需要经历两次标记过程
  • 第一次标记:没有通过可达性分析,会被第一次标记并进行一次筛选,筛选条件为此对象时候有必要执行finalize()方法。
    • 没必要执行finalize方法的情况
      • 没有重写finalize方法
      • 已经被虚拟机调用过了。任何一个对象的finalize方法都只会被系统自动调用一次。
  • 如果被判定为有必要执行,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后有一个虚拟机自动建立、低优先级的Finalizer线程去执行(不承诺等待它运行结束,放置过于缓慢导致内存回收系统崩溃)它。
  • 第二次标记:如果对象在finalize中重新与引用链上的任何一个对象建立关联(把自己赋值给类变量或者成员变量),那么在第二次标记时它会被移出“即将回收”的集合;否则就真的被回收的。
  • 后记:finalize运行代价高昂,不确定性大,忘了这个方法吧

回收方法区(HotSpot中的永生带)

  • 概述
    • 虚拟机规范说过可以不用实现此区域的垃圾回收,而且在方法区进行GC的性价比较低。
  • 主要回收内容
    • 废弃常量
    • 常量池中的其他类、接口、方法、字段的符号应用
      • 类似废弃常量
    • 无用的类
      • 该类所有的实例都已经被回收
      • 加载该类的ClassLoader已经被回收
      • 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

3.垃圾收集算法

标记 - 清除算法

  • 概念
    • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
  • 存在的问题
    • 效率问题
      • 标记和清除两个过程的效率都不高
    • 空间问题
      • 产生大量不连续的内存碎片,碎片太多导致需要分配较大对象时,无法找到足够的连续空间,不得不提前触发另一次垃圾收集动作。

复制算法

  • 概念
    • 将可用内存划分为大小相等的两块,每次只用其中一块。一块用完,将还存活的对象复制到另一块上,然后把已使用过的内存一次干掉。实现简单(只需移动堆顶指针),运行高效。
  • 内存区域划分
    • 传统方法把内存缩小为一般,太高了。新生代对象98%都是朝夕生死,所以不需要按1:1划分。
    • 将内存分为一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和一块Survivor,回收时将活着的对象复制到另一块Sur空间上,干掉Eden和刚才使用的Sur。默认8:1:1
  • 分配担保(Handle Promotion)
    • 当Sur空间不够时,需要依赖其他内存(老年代)进行分配担保
    • 如果另一块Sur空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代
  • 弊端
    • 对象存活率较高时就要进行较多的复制操作,效率变低
    • 如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有的对象都100%存活的情况,所以一般不直接用在老年代

标记 - 整理算法

  • 标记
    • 与标记 - 清除算法一样
  • 整理
    • 让所有存活的对象都向一端移动,然后直接干掉边界外的内存

分代收集算法

  • 概述
    • 当前商业虚拟机的垃圾手机都在用分代收集,没啥新思想,根据对象存货周期的不同将内存分为几块。一般是Java堆分为新生代和老年代。根据各年代的特点采用最适当的收集算法。
  • 新生代
    • 每次GC都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代
    • 对象存活率高、没有额外空间对它进行分配担保,就必须用“标记 - 清理”或“标记 - 整理”

4.HptSpot的算法实现

枚举根节点

  • GC Roots的选择
    • 全局性引用与执行上下文
  • Stop the world
    • 可达性分析必须在一个能确保一致性的快照中进行。
    • 一致性:整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证
  • OOP Map
    • 当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机通过OopMap的数据结构来达到这个目的的。
    • 在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。从而GC在扫描时就可以直接得知这些信息了。

安全点

  • 如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间。实际上HotSpot并没有为每条指令都生成OopMap,前面已经提到,只是在特定的位置记录,这些位置称为安全点。即程序只有在到达安全点时才能暂停。
  • 选定
    • 不能太少以至于让GC等待时间过长
    • 不能过于频繁以至于过分增大运行时的符合
    • 以程序“是否具有让程序长时间执行的特征(方法调用,循环跳转,异常跳转)”为标准进行选定的。
  • 中断方式
    • 抢断式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果有线程中断的地方不在安全电上,就恢复线程,让他“跑到”安全点上。几乎所有虚拟机采用这种方式
    • 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点时重合的

安全区域

  • 安全点的局限
    • 无法解决程序”不执行“的时候。比如Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求(走到安全点中断挂起),也不太可能等待线程重新被分配CPU时间。
  • 概念
    • 在一片代码区域中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们可以把Safe Region看作是被扩展了的Safepoint
  • 简单策略
    • 在线程执行到SafeRegion中的代码时,首先标识自己已经进入了SafeRegion,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。离开SafeRegion时,它要检查系统是否已经完成了根节点枚举(或整个GC),如果完成了,那线程就继续执行,否则它就等待直到可以安全离开SafeRegion的信号为止。

5.垃圾收集器

Serial收集器

  • 单线程收集器,在垃圾收集时,必须暂停其他所有的工作线程。简单高效,单CPU环境中,最高的单线程收集效率。采用复制算法。

ParNew收集器

  • 上面的多线程版,行为包括Serial收集器可用的所有控制参数、收集算法、STW、对象分配规则, 回收策略等都与Serial完全一致。Server模式下首选新生代收集器。除Serial外,唯一能与CMS配合工作。
  • 新生代
    • 多线程复制算法
  • 老年代
    • 单线程标记整理算法
  • 性能
    • 单线程绝不会比Serial快,两个线程也不一定比Serial快。默认开启的收集线程数与CPU的数量相同。

Parallel Scavenge收集器

  • 关注点不同,目标是达到一个可控制的吞吐量。

Serial Old收集器

  • 概述
    • Serial的老年代版本,采用标记整理算法。
  • Client模式
    • 这个收集器的主要意义
  • Server模式
    • jdk1.5以及之前的版本中与Parallel Scavenge收集器搭配之用
    • CMS的后备方案

Parallel Old收集器

  • ParallelScavenge的老年代版本。使用多线程和标记整理算法。1.6才开始提供,在此之前PS比较尴尬。

CMS收集器

  • 概述
    • 以获取最短回收停顿时间为目标的收集器。基于标记 - 清理算法
  • 流程
    • 初始标记
      • 标记一下GC Roots能直接关联到的对象,很快。
    • 并发标记
      • 进行GC Roots Tracing的过程。
    • 重新标记
      • 修正并发标记期间用户程序继续运作而标记产生变动的那一部分的标记记录
    • 并发清除
  • 缺点
    • 对CPU资源敏感
      • 占用一部分线程导致应用程序变慢。默认启动的回收线程数(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%,随着CPU数量上升而下降。
    • 无法处理浮动垃圾
      • 并发清理阶段产生的垃圾无法处理,只能留到下一次GC时时处理。
      • 由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎被填满了再进行收集
    • 产生大量空间碎片
      • 在顶不住要进行FullGC时开启内存碎片的合并过程,内存整理的过程无法并发,内存碎片解决了,停顿时间边长。

G1收集器

  • 概述
  • 特点
    • 并行与并发
    • 分代收集
    • 空间整合
      • 总体上基于“标记 - 整理”,局部看基于复制算法。意味着不会产生内存碎片
    • 可预测的停顿
      • 除了追求地停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个程度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • 流程
    • 初始标记
      • 标记一下GCRoots能直接关联到的对象,并修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。需要停顿线程,很快
    • 并发标记
    • 最终标记
      • 修正并发表及期间因用户线程继续运作二导致标记产生变动的那一部分标记记录。虚拟机将这段事件对象变化记录在Rememvered set logs里面。把Rememvered set logs的数据合并到Rememvered set中。停顿线程,并行执行。
    • 筛选回收
      • 先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。

名词解释

  • 并行
    • 指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
  • 并发
    • 指用户线程与垃圾收集线程同时执行
  • 吞吐量
    • CPU用于运行用户代码的时间与CPU总消耗时间的比值

理解GC日志

垃圾收集器参数总结

6.内存分配与回收策略

对象优先在Eden分配

  • 大多数情况,在Eden区中分配,Eden中没有足够的空间进行分配时,将发起一次Minor GC。

大对象直接进入老年代

  • 要避免一群短命大对象。

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

  • 虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次MinorGC后存活,并且能被Survivor容纳的话,将被移动到Survivor,并且对象年龄为1。对象在Sur中每熬过一次MinorGC,age就+1,当达到一定阈值(默认15),就晋升老年代。

动态对象年龄判断

  • 为了更好地适应不同程序的内存状况,虚拟机并不是永远要求对象年龄必须达到阈值才晋升。
  • 如果Sur中相同年龄的所有对象大小综合大于Sur空间的一半,年龄大于等于该年龄的对象可以直接进入老年代。

空间分配担保

  • 在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
    • 如果不成立,则虚拟机会查看HandlePromotionFailure是否允许担保失败。
      • 允许担保失败:检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试一次有风险的MinorGC。
      • 不会允许担保失败或者上述条件不成立,改为一次FullGC
    • 如果成立,此次MinGC可以确保是安全的(应对极端情况新生代所有对象全部存活)

相关文章

网友评论

      本文标题:垃圾收集器与内存分配

      本文链接:https://www.haomeiwen.com/subject/dbgvbftx.html