垃圾回收器概述

垃圾回收器概述

GC 从其底层实现方式(即 GC 算法)来看,大体可以分为两大类:基于可达性分析的 GC和基于引用计数法的 GC

  • 可达性分析法
    • 基本思路就是通过根集合作为起始点,从起始点开始搜索,经过的路径称为一个引用链,当一个对象没有被任务引用链访问到时候,则证明此对象不活跃,可以被回收,优点是GC垃圾回收效率高,实现比较简单(引用计数法是算法简单,实现比较难),缺点是在GC期间,整个应用需要被挂起(STW)。
  • 引用计数法
    • 在堆内存中分配对象时候,为对象分配一个额外的空间用来存计数器,如果有一个新的引用指向这个对象,则计数器+1,如果指向该对象的引用被置空或者指向其他对象,计数器-1。优点是天然带有增量特性,GC可与应用交替运行,不需要暂停应用,当计数器变为0的时候,对象可以马上回收,但是可达性分析类GC中,对象变成垃圾程序没法立刻感知,需要等待下一次GC执行 。缺点 存在循环引用的问题 两个对象相互引用,计数器都为1,既使这些对象都成了垃圾,GC也无法对其进行回收,然后就是 计数器的增减处理非常繁重,你就想多线程共享变量肯定需要对计数器进行原子增减,这玩意肯定需要引入锁啥问题,这会带来一系列新的复制性和问题。

基础概念

根节点

在Java对象中,可以作为GC Root的对象包括以下几种(是否作为根的判断依据是:程序是否可以直接引用该对象(比如调用栈中的变量指针),不同的垃圾回收器,选择GC Roots的范围是不一样的)

  • 虚拟机栈中(栈帧中的本地变量表)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native 方法)引用的对象

并行回收VS串行回收

根据垃圾回收的运行方式不同,GC可以分为以下三类

  • 串行执行:垃圾回收器执行的时候应用程序挂起,后台只能存在一个线程执行来及对象的识别和回收
  • 并行执行:垃圾回收器执行的时候应用程序挂起,但是在暂停期间会有多个线程进行识别和回收,减少STW的时间
  • 并发执行:垃圾回收执行的时候应用程序不用挂起(不是整个过程都是挂起,可能某一个步骤还是需要挂起的,扯一下G1)

三色标记法

可达性分析GC属于搜索型算法,三色标记法首要原则就是把堆中对象根据它们的颜色分到不同的集合里面, GC开始阶段,刚开始所有对象都是白色,在通过可达性分析的时候,首先会从根节点开始遍历 三种颜色所包含的意思如下。

  • 白色:还未被垃圾回收器标记的对象。
  • 灰色:自身已经被标记,但其拥有的成员变量还未被标记。
  • 黑色:自身已经被标记,且对象本身所有的成员变量也已经被标记

在 GC 开始阶段,刚开始所有的对象都是白色的,遍历对象图的过程其实就是白色->灰色->黑色过程,

  1. 首先会从根节点开始遍历,将GCRoot 将对象A,D加入灰色集合。
  2. 将A,D从灰色集合取出来,然后将A的所有引用加入到灰色集合同时将A加入黑色集合,D同理
  3. 重复从灰色集合取出的过程,一直到灰色集合为空

img

												图片来源https://www.jianshu.com/p/12544c0ad5c1
复制代码

通过可达性分析时,首先会从根节点开始遍历,但是呢过程中用户线程可能会改变引用关系,所以就会出现一些特殊情况,比如将扫描后黑色的对象增加引用关系到白色对象或者扫描时灰色对象断开到白色对象的引用,这样很有可能将后续可达的对象标记为垃圾对象。出现多标和漏标的情况

img
    ​											图片来源https://www.jianshu.com/p/12544c0ad5c1
复制代码

两种方式去解决这个问题,两种方式如下,其实这两种方式也是垃圾收集器标记过程:初始标记(GC Root根对象枚举),并发标记,最终标记(增量更新和原始快照),最终标记的过程。

  • 增量更新方式:当黑色对象插入新的引用关系到白色对象时就记录下来,并发扫描结束后,根据这些记录以被记录的黑色对象为根重新扫描一次。
  • 原始快照方式:将灰色对象断开到白色对象的引用关系记录下来,扫描结束后根据记录以记录的灰色对象为根重新扫描一次

垃圾回收算法

标记清除算法

标记-清除算法在概念上是最简单最基础的垃圾处理算法,标记-清除算法是由标记和清除阶段构成。标记阶段收拾将所有活动对象添加标记,方式有头标记和位图标记两种方式,清除阶段是清除掉没有标记的对象,回收时候把对象作为分块,连接到空闲链表中。这种优点就是简单快速,缺点有两个,一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记压塑算法

标记-压塑算法是在标记清除算法基础上,使用压塑取代清除这个回收过程(简单理解就是把对象活移动到一块),。

  • 标记阶段:通过根节点标记所有可达对象
  • **清除阶段:**将上一轮存活对象压塑内存到内存的另外一端,之后清理

标记压塑是优点和缺点

  • 缺点:需要重新安排可达对象的空间位置以及对移动对象后引用重定向。
  • 优点:不会带来碎片化问题,新来的对象分配通过指针碰撞就可实现。

标记复制算法

标记复制算法和标记压塑类似,两个算法的区别就是复制算法会把堆内存分成两个不同的区域,复制算法会将内存划分为容量大小相等的两块,每次的话只会使用其中的一块,当一块内存用完只会,就会将存活的对象复制到另外一块,然后将已经使用过的内存空间一次清理掉。

优点VS缺点

  • 优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可
  • 缺点:可用内存缩小到了原先的一半,空间利用率低

分代算法

分代算法是对上述三种算法的改进,因为上面三种算法或多涉及有标记和重新分配对象的过程,这些过程可能需要完全停止应用程序进行堆搜索,堆空间越大,进行垃圾回收所需要时间越大,所以分代算法想办法解决上述问题。分代算法基于这样一个假说:绝大多数对象都是朝生夕灭的,针对不同代使用不同GC算法,刚生成的对象称为新生代对象,对新对象执行的 GC 称为新生代 GC(minor GC),到达一定年龄的对象则称为老年代对象,面向老年代对象的 GC 称为老年代 GC(major GC),新生代对象转为为老年代对象的情况称为晋升。引入分代之后有以下两种问题。

不同分代在堆内存之中如何划分:

​ 将内存空间划分为一个较大的Eeden和两块较小的Survivor空间,每次使用的是Eden和其中一块Survivor,新生代GC采用标记-复制算法,主要利用该算法的高吞吐特性,老年代GC使用标记-清除算法。

​ PS:这里一个问题 如果Survivor不够用的怎么办,这个有个知识点,当空间不够用的时候,需要依赖其他的内存空间进行分配担保,一般来说就是老年的来进行担保,放不下去就扔给老年代。

img

​ 图片来源blog.csdn.net/youanyyou/a…

如何标记代际之间引用关系

分代算法需要考虑跨代/区之间对象的引用,因为新生代对象不仅只会被根对象和新生代里对象引用,还有可能被老年代对象引用,GC算法需要再不回收老年代对象的同时安全回收新生代里面的对象,新生代回收时候,不适合也不可能去扫描整个老年代,这样全扫描就失去了对堆空间进行分代的意义。

解决上面问题就是引入写屏障:如果一个老年代的引用指向一个新生代对象就会触发写屏障,这个时候会用一个记录集记录从老年代对象到新生对象的引用,新生代GC将会把记录集视为GC Root的一部分。

增量算法

增量GC是为了解决标记清除时候存在长停顿的问题,基本思想就是如果一次性将垃圾进行处理,可能造成系统长时间停顿,那么就可以尝试让垃圾收集线程和用户线程交替执行,每次只收集一小片区域的内存空间,接着切换回到用户线程,直到全部收集完成。这种方式的缺点就是不停需要进行用户线程和垃圾收集线程切换,增加消耗,造成系统吞吐量下降。

垃圾收集器

串行收集器

串行收集器有Serial和Serial Old两种,两者的区别就是Serial工作在新生代,使用复制算法,Series Old工作在老年代,使用的标记整理算法。串行收集器的特点是单线程运行,只会使用一个处理器和一条收集线程完成垃圾收集工作,在垃圾收集的时候,会暂停其他所有工作线程,直到它收集结束(STW)。

**优点:**单线程执行特性,应用于单个CPU硬件平台的性能可以超过其他的并行或并发处理器。

**缺点:**垃圾收集的时候,会暂停其他所有工作线程。

img

​ 图片来源blog.csdn.net/youanyyou/a…

并行收集器

简单来说并行处理器其实就是GC线程从单线程变为多线程的,代表的垃圾收集器主要有ParNew,Parallel Scavenge,Parallel Old这三种

  • ParNew收集器 其实就是 Serial收集器的多线程版本,基于“复制”算法,其他方面完全一样。
  • Parallel Scavenge 收集器 和 ParNew收集器类似,基于“复制”算法,能够通过参数-XX:+UseAdaptiveSizePolicy打开垃圾收集自适应调节策略的开关。
  • Parallel Old 就是 Parallel Scavenge 收集器的老年代版本,基于“标记-整理”算法实现。

CMS

CMS(Concurrent Mark Sweep) 收集器是以获取最短回收停顿时间为目标的收集,在垃圾收集可以让用户线程和GC线程并发执行,用户方面避讳感觉到卡顿。主要有下面四个过程

  • 初始标记:这个过程是标记 GC Root 开始的下级对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
  • 并发标记:这个会上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
  • 重新标记:再标记一次,防止其他进程产生垃圾(ps 为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾)
  • **并发清除:**清除系统中未标记的对象

G1

CMS存在一些问题,比如: 老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1重新将堆对象进行划分,G1 将连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。Region中还有一类特殊的Humongous区域,专门用来存储大对象G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中。G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大(垃圾)的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。这也是 "Garbage First" 得名的由来。G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次GC。

img 图片来源blog.csdn.net/youanyyou/a…

垃圾回收的步骤

  • 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  • 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
  • 最终标记:Stop The World,使用多条标记线程并发执行。
  • 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。

巨人肩膀

闲谈

感觉有帮助的同学还请点赞关注,这将对我是很大的鼓励~,公众号有自己开始总结的一系列文章,需要的小伙伴还请关注下个人公众号程序员fly呀,干货多多,湿货也不少(∩_∩)。

猜你喜欢

转载自juejin.im/post/7041465742402781197