GC回收


GC回收

基于正在使用的对象进行遍历,对存活的对象进行标记,其未标记的对象可以认为是垃圾对象,然后基于特定的算法进行回收,这个过程称之为GC(Garbage Collection)。

通过以下几个方面进行实现

  • GC判断策略(例如引用计数、对象可达性分析)
  • GC收集算法(标记-清除、标记-清除-整理、标记-复制-清楚)
  • GC收集器(例如Serial,Parallel,CMS,G1)

GC种类

手动GC

显示地进行内存分配(allocate)和内存释放(free)。如果忘记释放,该内存就不能再次被使用,称之为内存泄漏(memory leak)

手动需要C语言进行,java不能够手动操作内存

自动GC

一般实在JVM系统内存不足时,有JVM系统启动GC对象,自动对内存进行垃圾回收

如果GC的速度小于对象分配内存的时候,也会造成内存溢出

引用计数法

引用计数法

  • 绿色圆圈是内存中的根对象,表示程序正在使用的对象

  • 蓝色圆圈是内存中的活动对象,其中的数字表示其引用计数

  • 灰色圆圈是内存中没有活动对象引用的对象,表示非活动对象(垃圾对象)

    循环引用

存在一个设计缺陷即循环引用,如上图的红色即为垃圾,有计数 无法被回收,引发内存泄漏

通过弱引用软引用或者其他算法来排除

弱引用weak reference:WeakReference的一个特点是它何时被回收是不可确定的, 因为这是由GC运行的不确定性所确定的. 所以, 一般用weak reference引用的对象是有价值被cache, 而且很容易被重新被构建, 且很消耗内存的对象.

​ 在weak reference指向的对象被回收后, weak reference本身其实也就没有用了. java提供了一个ReferenceQueue来保存这些所 指向的对象已经被回收的reference. 用法是在定义WeakReference的时候将一个ReferenceQueue的对象作为参数传入构造函数.

软引用SoftReference:soft reference和weak reference一样, 但被GC回收的时候需要多一个条件: 当系统内存不足时, soft reference指向的object才会被回收. 正因为有这个特性, soft reference比weak reference更加适合做cache objects的reference. 因为它可以尽可能的retain cached objects, 减少重建他们所需的时间和消耗

标记清除

标记清除法

标记清楚就是对可达对象进行标记,不可达对象即认为是垃圾,进行清楚

通常分为两个步骤

  • 标记所有可到达的对象(reachable objects)
  • 清除不可到达对象占用的内存空间

可达性行分析:

GC遍历内存中整体的对象关系图确定根对象(根对象的定义)

  • 栈中直接引用的对象

  • 常量池中引用的对象

    大致可以理解为正在使用的对象

根对象对所有对象进行依赖查找,所有可以被遍历访问到的对象,对其进行标记,即为存活对象,也称为可达性

解决了循坏依赖的问题,但是存在短时间的线程暂停,这种现象为STW停顿(Stop The World Pause,权限暂停 ),暂停时间和堆内存的大小、对象的总数没有直接关系,而是同存活对象的数量来决定。

算法

标记-清除

即为上述所讲模式,不会对碎片进行整理

标记-清除-整理

上述模式以外,还会进行内存整理,但是会增加GC暂停时间

标记-整理

标记-整理

基于标记-清除-整理算法,创建新的内存用于存储幸存对象,同时复制和标记并发进行,可以减少GC时间,牺牲空间换取时间

碎片整理

系统GC每次执行清除(sweeping)操作,JVN都必须保证“不可达对象”占用的内存被回收然后重用。虽然内存回收了,但会创建大量的内存碎片(类似于磁盘碎片),进而引发两个问题:

  • 对象创建时,执行写入操作越来越耗时,因为JVM系统需要花费更多的时间去寻找对应大小的内存空间
  • 对象创建需要在一块连续的内存空间中分配内存。如果碎片问题非常严重,直至没有空闲片段能够存放新建的对象,就会发生内存分配错误(alloaction error)

因此JVM启动GC收集垃圾时不仅仅需要标记和清除,还需要执行“内存碎片整理”。整个过程会让所有的可达对象进行依次移动,进而减少或者消除内存碎片

碎片整理

分代设想

垃圾收集需要停止程序的进行,如果收集的过程很长,就会大大影响系统的性能。为了解决这个问题,通过实验发现内存的对象大致分为两大类

  • 存活时间较长(这类对象比较少)
  • 存活时间较短(这类对象比较多)

因此将VM中将内存分为年轻代(后者)和老年代(前者),采用不同的算法用来提高GC的性能。

缺点:不同分代的对象依旧会存在相互引用,难以回收

对象年龄分布图

对象分配

基于默认的JVM内存架构,对象创建时内存的分配过程如下

  1. 编译器通过逃逸分析(JDK8默认开启),确认对象在堆或栈上分配

  2. 如果在堆上分配,则首先检测是否可以在TLAB(Thread Local Alloaction Buffer)上直接分配

  3. 如TLAB不能分配,则分到Eden加锁区分配(线程共享区)

  4. 如果Eden区无法分配对象,则执行Yong GC(Minor Collection)

  5. 如果Yong GC之后Eden仍不能分配存储对象,则直接分配到老年代

    TLAB:线程本地应用缓存,在伊甸园区的私有区,空间小没有锁,效率快。如果不能分配,线程会再次申请,还是失败则会分配到伊甸园区

YoungGC操作

Young GC

  • GC触发时所有可达对象会被复制到一个幸存区(例如S1),S1如果无法存储这些对象会被直接复制到老年代
  • GC再次触发时Eden和S1的可达对象会被复制到S2,同时清空S1和Eden
  • GC再次触发时Eden和S2的可达对象会被复制到S1,同时清空S2和Eden,以次类推
  • 每次GC幸存对象都会年龄+1,到达一定阈值直接分配到老年代(可用参数 -XX:+MaxTenuringThreshold 来指定上线,默认15)

s1和s2至少有一个是空的,用来存放下次GC未被收集的对象

GC模式

垃圾收集事件通常分为:

  • Minor GC(小型GC):年轻代GC,新对象分配频率越高,GC越多
  • Major GC(大型GC):老年代GC
  • Full GC(完全GC):整个堆的GC事件

GC收集器

下文出现的吞吐量= 运行用户代码的时间/( 运行用户代码的时间+垃圾收集时间)

算法搭配

  • 年轻代和老年代串行收集器:Serial GC
  • 年轻代和老年代并行收集器:Parallel Gc
  • 年轻代并行和老年代并发收集器:Parallel Gc New 和 CMS-Concurrent Mark and Sweep
  • G1收集器

Serial GC串行收集器

参数配置: -XX:+UseSerialGC

应用特点

  • 内部仅一个线程执行垃圾回收
  • GC时Stop the world 时间较长

场景引用

  • JVM的客户端模式,实时性要求不高
  • 适用于CPU个数或者核数较少且内存空间少的环境

算法运用

  • 新生代标记-复制(存活对象少)
  • 老年代标记-清除-整理(回收少,碎片多)

CMS收集器

Mostly Concurrent Mark and Sweep Garbage Collector

设计目标是追求更快的响应时间

参数

使用CMS配置 -XX:+UseConMarSweepGC, 默认开启 -XX:++UseParNewGC

  • -XX:UseCMSCompactAtFullCollection 执行full GC后,进行一次碎片整理,整理的过程是独占的,会引起停顿的时间变长
  • -XX:+CMSFullGCsBeforeCompaction 设置进行几次full GC之后,进行一次碎片整理
  • -XX:ParallelCMSThreads 设定CMS线程数量(一般情况可约等于CPU数量)

应用特点

  • 空闲列表(free-lists)管理内存的回收,不对老年代进行碎片整理

  • 标记-清除阶段大部分工作和用户线程一起并发执行

  • 优点是性能高,每次GC的时间都很短

  • 缺点是CPU资源较少时,GC平均占用着,CMS会比并行GC的吞吐量小一些

    老年代的碎片无法处理,特别是在堆内存设置非常大的情况下,GC几次full GC后碎片清理会进行不可预测甚至长时间的暂停

场景应用

  • 适用于多个或者多核处理器,响应时间优先
  • CPU受限下,同用户竞争CPU,吞吐量少

算法运用

  • 新生代并行的标记-复制
  • 老年代并发标记-清除

Parallel收集器

并行收集器,利用多个或者多核CPU优势实现多线程并行GC操作

配置参数

使用Parallel GC配置 -XX:+UseParallelGC, 默认开启 -XX:++UseParallelOldaGC

  • -XX:ParallelGCThread=20 设置并行收集器并行数,最好和处理器数目相等
  • -XX:MaxGCPauseMills=100 设置每次年轻代垃圾回收最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
  • -XX:+UseAdaptiveSizePolicy 设置并行收集器自动选择年轻代大小和幸存区的比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用该收集器时一直打开
  • -XX:GCTimeRatio=99 设置吞吐量大小,默认值为99,就是将垃圾回收的时间设置成总时间的1%,它的值是0-100的整数值。假设值为n,则垃圾收集时间不得超过1/(1+n)

应用特点

  • 利用CPU的多核特性执行多线程下的并行化GC操作
  • GC期间,所有CPU内核都在并行清理垃圾,暂停时间短
  • 实现可控吞吐量和停顿时间(可配置)

场景运用

  • GC操作仍需暂停应用程序,GC不能打断,不适合低延迟要求场景,适用于需要高吞吐量而对暂停时间不敏感的场合
  • 后台计算、后台处理的弱交互场景而非web交互场景

算法应用

  • 年轻代标记-复制,对应Parallel Scavenge收集器
  • 老年代标记-清除-整理,对应Parallel old收集器

G1收集器

在JDK8中属于准产品

一种工作于服务端模式的垃圾回收器,面向多核、大内存的服务器,在实现高吞吐量时,也满足了GC的停顿时间可控。未来G1计划全面取代CMS。

  • 可以像CMS收集器一样能够同时和应用线程一起并发执行
  • 减少内存停顿时间
  • 满足可预测的停顿需求
  • 属于压缩型收集器,可以实现有效的空间压缩。消除大部分内存碎片问题

实现方式

在G1中堆不再区分年轻代和老年代,二十划分为多个(通常是2048个)存放对象的小堆区(small heap regions)。每个小堆区可以是eden、survivor、old,请两者合起来为年轻代。

  • 每次GC只处理一部分小堆区,陈为此次的回收集(collection set),收集所有年轻代和部分老年代的小堆区
  • 类似于CMS,在并发阶段会事先估计每个小堆区存活对象的数量,垃圾最多的被优先收集。
  • 基于停顿时间目标来选择需要回收、压缩的小堆区数量
  • G1基于标记、清理对应的region时,会将对象从一个或者多个region中复制到另一个region,在这个过程中伴随释放和压缩内存
  • 基于多核并行执行,满足其他收集器所不具备的(CMS不能压碎碎片、ParallelOld只能较长时间的整堆压缩)

G1不是一个实时的收集器,它只是近最大可能来满足设定停顿时间。基于以往的收集数据和指定的停顿时间来确定收集几个regions

region:1-2m,最多200个,最大支持内存64G

配置参数

-XX:+UseG1GC 启用G1收集器

  • -XX:MaxGCPauseMillis=200

    设置最大停顿时间指标,是一个软指标,JVM会尽力区完成这个指标,默认值为200毫秒

  • -XX:InitiatingHeapOccupancyPercent=45

    启动GC时整个堆内存的占比,默认45即45%

特点

  • 内存运用比较弹性
  • 支持并行和并发

运用场景

  • FullGC发生相对比较频繁或总时长比较长

  • 对象分配或对象进入老年代比例波动大

  • 较长内存停顿

    如果其他收集器运用良好,不建议更换G1

算法分析

  • 年轻代标记复制
  • 老年代标记复制整理

关键步骤

  1. 初始标记:属于YoungGC,具有STW,对持有老年代引用的survivor(root)进行标记
  2. 根区扫描:并发执行,对root进行扫描
  3. 并发标记:找出整个堆中存活对象,空区域标记为”x“,会被young GC中断
  4. 再次标记:完全堆堆中存活对象标记,采用比CMS更快的SATB算法
  5. 清理:并发执行,统计小队去存活对象,并对小堆区进行排序,清理垃圾,释放内存
  6. 复制/清理:小堆区中未被清理的对象进行复制,然后清理

后记

值得一提的是HotSpot工程师主要精力一直放在不断改进G1上,新版本JDK会带来新的功能和优化


文章作者: hyy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hyy !
  目录