美文网首页
垃圾收集器与内存分配策略(二)

垃圾收集器与内存分配策略(二)

作者: jqdywolf | 来源:发表于2018-04-05 17:17 被阅读0次

垃圾收集器与内存分配策略(二)

垃圾收集算法

标记清除

这个很好理解,整个过程分为标记和清除两个阶段。标记上一章已经说过了,在标记结束后统一进行清除。


标记清除

缺点:
(1) 效率不高
(2) 内存中存在大量的内存碎片,如果后续有大的对象生成,可能无法找到足够的连续空间,从而提前触发垃圾回收。

复制算法

把内存分为平均的两部分,平时只使用其中一部分A,回收时,将还存活的对象全部复制到另一个部分B,然后A整个回收掉就可以了。


复制算法

注意的问题

  • 50%的内存是不用的,太浪费了
    商业上不会使用1比1的比例。HotSpot将内存分为一块较大的Eden和两块较小的Survivor空间。每次使用Eden和其中一块Survivor空间。回收时讲Eden和Survivor还存活的对象复制到另一块Survivor空间。HotSpot默认Eden和Survivor的比例是8:1.
  • 如果回收时发现Survivor不够放存活的对象怎么办
    需要老年代进行分配担保,即当Survivor不够放的话,剩下的对象直接放入老年代。
标记整理

和标记清除类似,在回收时,将还存活的对象向一端移动。


标记整理
分代收集算法

根据对象的存活周期,分为年轻代和老年代。不同年代的对象采用不同的垃圾收集算法。

  • 显然复制算法适合年轻代。因为年轻代对象的存活率很低,复制的对象的个数很少。
  • 标记清除和标记整理适合老年代。老年代对象的存活率很高,复制算法会复制的次数太高,效率不高,而使用标记清除和标记整理算法。

Hotspot的算法实现

枚举根节点

这一节主要是针对如何找到GC Root对象?

  • 现有的问题
    (1) 我们知道可作为GC Root对象的有全局性引用(static/常量)和执行上下文(栈中局部变量引用)中,但是这块地方有时可达几百M,依次遍历其中每个数据是否是引用,效率太低。
    (2) 且我们知道在进行GC Root枚举时,必须stop the world来保证引用关系不再改变。所以这也要求GC Root的枚举不能太耗时。
  • HotSpot的解决办法
    使用一组叫做OopMap的数据结构。
    (1) 记录每个类的每个数据位置(通过偏移量来确定)的类型(即是否是引用)。这一步在类加载后即可确定。
    (2) 在特定的位置上记录栈和寄存器中哪些位置是引用。
    这样我们在GC Root枚举时,可以快速找到所有引用。
安全点
  • 程序执行时并非在任何地方都能停顿下来GC。
    只有在安全点的位置上才能停顿下来,原因是因为只有安全点上才有全部的OopMap信息(就是上面说的“特定位置”),即只有安全点才能安全地进行GC Root枚举。
    安全点的选取由程序本身结构决定。不能太多也不能太少。一般在方法调用、循环跳转、异常跳转等。
  • 还有一个问题:如何在发生GC时,让所有的线程都跑到最近的安全点停下来?
    两种方式:抢占式和主动式
    抢占式:在GC发生时,中断所有线程,如果发现有的线程不在安全点,则重新唤醒知道跑到最近的安全点。--这种方式几乎没人使用
    主动式:线程主动发起。在GC发生时,在安全点位置上设置标记位,线程执行到安全点时轮询这个标志位,如果发现为true,则中断。

安全点很好地解决了如何进入GC的问题。

安全区
  • 安全点无法解决的问题
    当一个线程没有处于执行状态:就绪(等待分配CPU时间)。此时如果要发生GC,如何让这个线程进入安全点?总不能一直等,知道CPU分配给它时间吧。
  • 解决办法
    引入安全区的概念,即一段代码内无引用关系改变。线程进入安全区代码后,标记自己“安全区”标记,GC发生时就不用管标记“安全区”的线程了。当线程想要离开安全区时,判断GC是否结束,如果没结束要等GC结束后才能继续执行。

上面这段文章讲的是虚拟机如何发起内存回收,下面讲具体如何回收。

垃圾收集器

垃圾回收器
Serial

单线程收集器。即只有一个线程去垃圾回收工作。必须先进行stop the world,然后进行回收工作。
算法:由于是新生代,采用复制算法。
特点:单线程简单高效,多线程下效率不高。且必须stop the world。


Serial收集器
ParNew

和Serial唯一的区别就是使用多线程去垃圾回收工作。其他和Serial完全一样。
特点:有一个不和性能有关的优点:能和CMS配合的最好新生代收集器。


ParNew收集器
Parallel Scavenge

也是多线程去垃圾回收工作。算法也是复制算法。
它和别的收集器主要有两点不同:

  • 关注点不同
    其他收集器关注:如何让用户的线程停顿时间越短越好。
    它关注:如何让CPU的吞吐量最高。
    吞吐量=(CPU执行代码的时间)/(CPU执行代码的时间+CPU GC的时间)
    也被称为:吞吐量优先收集器
  • 自适应
    它无需用户设置Eden与Survivor的比例,进入老年代年龄参数,它可以动态调整,以达到最高吞吐量。
    也被称为:自适应调节策略。
Serial Old

是Serial的老年代版本。单线程+标记整理算法。

Parallel Old

是Parallel Old的老年代版本。多线程+标记整理算法。

CMS
  • 说明
    一个真正意义上的并行收集器:用户线程和GC线程可以同时运行。
    目标是尽可能缩短GC停顿时间的回收器。多用于B/S模型中,用户感知的停顿时间很小。
    算法:使用标记-清除算法。
  • 分为四个步骤:三标记一清除
    (1) 初始标记:标记GC Root直接关联的对象。
    (2) 并发标记:进行GC Root的trace过程,进行可达性分析。
    (3) 重新标记:修正(2)时,由于用户线程也在运行导致的引用关系变化。
    (4) 并发清除:就是清除的过程。

    (1)和(3)需要stop the world,但(1)和(3)的消耗时间非常短。
    (2)和(4)为并发,即可以和用户线程同时运行。消耗时间相较于1和3较长。但由于是并行,所以整个收集器看来就是和用户线程并行运行。
    CMS过程
  • CMS的三大缺点
    • 由于2和4需要CPU进行GC,所以用户线程的可用CPU变少了。
    • 并发清除阶段引起:提前Full GC和引发Concurrent Mode Failure问题。
      • 提前Full GC
        由于用户线程也在运行,所以清除时,必须留着内存给正在运行的用户线程使用。所以无法像其他收集器一样,老年代用满了才进行GC,CMS在老年代在65%就要开始GC了。
      • Concurrent Mode Failure
        由于用户线程也在运行且用户线程需要的内存未知,如果此时内存无法满足用户线程的需要,则就会引起CMF。进而引发虚拟机启动备选预案:使用Serial Old来进行GC,这就要花很长时间了。
    • 标记-清除算法自身的缺点:存在空间碎片,容易导致大对象无法找到连续的内存分配,而导致提前Full GC。
G1

jdk1.7中最具先进的收集器。

  • 有几个特点
    • 并行
      即整体看来,可以和用户线程并行执行
    • 分代收集
      前面说的收集器只针对某个年轻代或老年代,它G1可以管理整个堆。
    • 空间整合
      整体来看是标记-整理算法。从局部(Region)看是复制算法。总而言之,不会有空间碎片。
    • 可预测的停顿
      这是G1一个很大的特点:即用户可以指定在一个M长的时间片内,GC的停顿时间不超过N秒。
  • G1是基于Region来进行内存管理的。
    G1收集器的堆的分布已经和其他收集器不一样了。G1将堆分为大小相等的Region。仍然有年轻代和老年代的概念,但两者没有物理界限,都是一连串的Region集合。
  • 可预测的停顿实现方法
    G1会跟踪每个Region的回收价值(回收得到的空间以及GC时间),维护一个优先列表。每个GC时,在用户指定的时间内,找到回收价值最高的进行回收。
  • 如何解决跨Region引用来引起的全局堆引用扫描问题
    我们知道引用可能发生在跨Region之间,这个问题不仅仅是G1的问题,在其他收集器中也存在年轻代引用老年代的问题。而虚拟机的解决办法是Remembered Set,即每次赋值引用时,记录这个引用否引用其他Region(或老年代),如果是则在Remembered Set记录其他Region(或老年代)。保证不会圈堆扫描。
  • G1的具体步骤
    • 初始标记
    • 并发标记
    • 最终标记
    • 筛选回收
      前三个阶段几乎和CMS一样。最后的筛选回收:在运行的GC停顿时间内,选择回收价值最高的Region回收。这个阶段虽然可以和用户线程并行,但实际上由于所花费时间短,且停顿用户线程这个时间会更短,所以采用停顿用户线程的方式。


      G1回收过程

相关文章

网友评论

      本文标题:垃圾收集器与内存分配策略(二)

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