[java] 垃圾回收(GC)

2、GC基本原理

对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。
通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC 确定一些对象为”不可达”时,GC 就有责任回收这些内存空间。可以。程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。

3、垃圾回收(GC)算法

3.1、引用计数法和可达性分析

即一个对象如果没有任何与之关联的引用, 即他们的引用计数都为 0, 则说明对象不太可能再被用到,那么这个对象就是可回收对象。

为了解决引用计数法的循环引用问题, Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象, 不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

GC Roots包括四种对象:

  • 栈中的本地变量

  • 本地方法栈中(native方法)的变量

  • 方法区中的静态变量和常量引用的对象

  • 正在运行的线程

之所以是上面四种,总结为:GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots

从程序的角度来说就是,找到一段程序运行的整个过程中始终会存活的对象,这些对象的特点是始终会存活,不会死亡。即一些静态变量 和常量所引用的对象等。

3.2、复制算法

我们把 伊甸园区:幸存from区:幸存to区空间大小设成 8 : 1 : 1 ,对象总是在伊甸园区出生, 幸存from区保存当前的幸存对象, 幸存to区为空。一次 gc 发生后:

  1. 伊甸园区活着的对象 + 幸存from区存储的对象被复制到幸存to区;
  2. 清空伊甸园区和幸存from区;
  3. 颠倒幸存from区 和幸存to区的逻辑关系:From 变 To , To 变 From 。

好处:没有内存的碎片
坏处:浪费了内存空间,多了一半空间永远是空。
最佳使用场景:新生区

3.3、标记清除压缩算法

标记:对存活对象进行标记
清除:清除未标记的对象
压缩:清除过后会产生内存碎片,压缩算法就是再次扫描将碎片内存整理到一端。

3.4、分代收集算法

新生区用复制算法
养老区用标记清除压缩算法

4、垃圾回收(GC)器

在这里插入图片描述

常用收集器分类如下:

  • 串行收集器(进行垃圾回收的时候,必须暂停用户的所有进程,即 stop the world。):Serial,Serial Old
  • 并行收集器(Parallel,指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态):ParNew,Parallel Scavenge,Parallel Old
  • 并发收集器(Concurrent,指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行)):CMS,G1

4.1、Serial收集器

Serial是最早的垃圾收集器,用于新生代的垃圾回收,采用复制算法,它是一款单线程的垃圾收集器,垃圾回收时会暂停所有工作线程——“Stop The World”!,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,它是虚拟机运行在client模式下默认的新生代垃圾收集器。

4.2、Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是单线程的,其采用**"标记-整理"算法**,主要用于虚拟机的client模式下。其还有一个重要作用是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用。

4.3、ParNew收集器

ParNew是Serial收集器的多线程版本,也是用于新生代垃圾回收,采用复制算法,除了使用多个线程进行垃圾回收之外,其余都与Serial收集器完全一样。它是虚拟机运行在server模式下的首选的新生代垃圾收集器。ParNew收集器在多CPU的情况下更能发挥出其优势,它默认开启的垃圾收集线程数与CPU数量相同,可以使用-XX:ParallelGCThreads=N(CPU> 8 N=5/8; CPU<8 N=CPU,并行垃圾收集器都可以使用此参数)来限制垃圾收集的线程数。
除了Serial收集器外,只有它能与CMS(Cocurrent Mark Sweep)搭配使用,目前CMS+ParNew的搭配组合用的的挺多的。当老年代选用CMS收集器后默认的新生代收集器就是ParNew,也可以使用-XX:+UseParNewGC选项强制指定它。

4.4、Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,采用复制算法,是一个并行的多线程收集器。它的目标是达到一个可控制的吞吐量(Throughput),因此被称为"吞吐量优先"的垃圾收集器,适用于后台运算而不需要太多交互的任务(虚拟机在server模式下默认使用吞吐量优先收集器)。吞吐量就是CPU用于运行用户代码的时间与CPU运行总时间的比值,吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间)。
Parallel Scavenge收集器还有一个开关参数-XX:+UseAdaptiveSizePolicy,当这个参数打开之后,就不需要手工指定新生代的大小(-xmn)、Eden与Survtvor区的比例(-XX:SurvrvorRatio)、晋升老年代对象年龄(-XX.PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞量,这种调节方式称为GC自适应的调节策略(GC Ergonormcs)。自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。

4.5、Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。(因为采用的是标记整理算法,所以会产生大量的碎片,CMS会调用serial old去进行碎片整理,非常耗时间,这也就是CMS和serial old之间有一条线链接的原因)其只能与Parallel Scavenge收集器搭配使用,两者是"吞吐量优先"的垃圾收集器组合。在注重吞吐量及CPU资源敏感的场合,可以优先考虑 Parallel Old 加Parallel Scavenge 的组合

4.6、CMS收集器

CMS(Concurrent Mark Sweep)收集器用于老年代的内存回收,采用 “标记-清除” 算法,它以获取最短回收停顿时间为目标,可以为交互比较高的程序提高用户体验。

  1. 初始标记(CMS initial mark):只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

  2. 并发标记(CMS concurrent mark):并发标记阶段就是进行GC Roots 跟踪的过程,由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。和用户线程一起工作,不需要暂停工作线程。

  3. 重新标记(CMS remark)——STW:而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

  4. 并发清除(CMS concurrent sweep):用户线程被重新激活,同时清理那些无效的对象。

  5. 并发重置(CMS concurrent reset):CMS清除内部状态,为下次回收做准备。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。其工作示意图如下:
在这里插入图片描述

4.7、G1收集器

G1(Garbage-First)收集器是JDK1.7中新出的一款面向服务端应用的垃圾收集器,目的是想替换掉CMS收集器,用参数 -XX:+UseG1GC开启。主要应用在多CPU和大内存服务器环境下,在java 9中替代CMS编程默认的垃圾收集器。与CMS收集器相比,G1具备如下特点:

  • 不会产生内存空间碎片:与CMS的“标记一清理”算法不同,GI从整体来看是基于“标记一整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1之前的收集器收集的范围都是整个新生代或者老年代,G1跟这些收集器有很大区别。G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还有新生代、老年代的概念,但是新生代、老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所有时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage一First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之问的对象引用,虚拟机都是使Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Rcmembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加人Remembered Set即可保证不对全堆扫描也
不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  1. 初始标记(Initial Marking)

  2. 并发标记(Concurrent Marking)

  3. 最终标记(Final Marking)

  4. 筛选回收(Live Data Counting and Evacuation)

G1的前几个步骤的运作过程和CMS有很相似。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

注意:G1收集器没有full GC,而是Mixed GC,Mixed GC会回收young 区和部分old区。

G1关于Mixed GC调优常用参数:

  • -XX:InitiatingHeapOccupancyPercent:设置堆占用率的百分比(0到100)达到这个数值的时候触发global concurrent marking(全局并发标记),默认为45%。值为0表示间断进行全局并发标记。
  • -XX:G1MixedGCLiveThresholdPercent:设置Old区的region被回收时候的对象占比,默认占用率为85%。只有Old区的region中存活的对象占用达到了这个百分比,才会在Mixed GC中被回收。
  • -XX:G1HeapWastePercent:在global concurrent marking(全局并发标记)结束之后,可以知道所有的区有多少空间要被回收,在每次young GC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC.
  • -XX:G1MixedGCCountTarget:一次global concurrent marking(全局并发标记)之后,最多执行Mexed GC的次数,默认是8。
  • -XX:G1OldCSetRegionThresholdPercent:设置Mixed GC收集周期中要收集的Old region数的上限。默认值是Java堆的10%

其他常用参数:

  • -XX:+UseG1GC 开启 G1
  • -XX:G1HeapRegionSize=n, 设置每个Region 的大小,该值将是2的幂,范围1-32M,最多2048个region
  • -XX:MaxGCPauseMillis=200 最大停顿时间
  • -XX:G1NewSizePercent、-XX:G1MaxNewSizePercent:新生代占用整个堆内存的最小百分比(默认5%)、最大百分比(默认60%)
  • -XX:G1ReservePercent=10 保留内存区域,防止 to space(Survivor中的to区)溢出
  • -XX:ParallelGCThreads=n SWT线程数
  • -XX:ConcGCThreads=n 并发线程数=1/4*并行

5、什么时候会触发FullGC

  1. 直接调用System.gc
  2. 老年代空间不足,当执行Full GC后空间仍然不足,则抛出堆内存溢出异常
  3. 元空间满,
  4. YGC时的悲观策略。在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发Full GC。例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。

6、对象分配规则

  1. 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  2. 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  3. 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  4. 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  5. 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值