【JVM】垃圾回收算法与垃圾回收器

垃圾回收概述

垃圾: 指在运行程序中没有任何指针指向的对象
GC行为只在堆空间 和 方法区【元空间】

垃圾回收相关算法

标记阶段

首先区分哪些是存活对象,哪些是死亡对象,称为垃圾标记阶段。
判断存活一般有两种方式:引用计数算法,和可达性分析算法

引用计数算法

对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况,对象A被引用了,则A的引用计数器加1,引用失效则减1,计数器为0,则说明A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟
缺点:增加了空间开销,每次赋值都需要更新计数器,增加时间开销,致命缺陷:无法处理循环引用。【例如循环链表的引用,如图】,所有Java没有选择这种算法请添加图片描述
你引用我,我引用你,最后我们都不被引用了,计数就发现不了
python使用引用计数算法,用弱引用和手动解除来解决循环引用

可达性分析算法 [根搜索算法、追踪性垃圾收集]

可达性分析算法能有效解决循环引用问题。
“GC Roots”根集合就是一组必须活跃的引用。
基本思路:以根对象集合为起始点,从上至下搜索被根对象集合所连接的对象是否可达。
在这里插入图片描述
分析工作必须在一个能保障一致性的快照中进行,【及分析过程中不能还在变动】,这点不满足的话分析结果的准确性就无法保证。所以GC进行时必须“Stop The World”

GC Roots 包括哪几类元素

  1. 虚拟机栈中引用的对象,比如各线程被调用的方法中用到的参数、局部变量等
  2. 本地方法栈内JNI(本地方法)引用的对象
  3. 方法区中类静态属性引用的对象,例如:Java类的引用类型的静态变量
  4. 方法区中常量引用的对象 例如:字符串常量池里的引用
  5. 所有被同步锁(synchronized)持有的对象
  6. Java虚拟机内部的引用 例如:类加载器、异常对象、基本数据类型对应的Class对象
  7. 反应虚拟机内部情况的JVMBean,本地代码缓存等
  8. 除了固定的GC Roots集合外,还有其他对象“临时性”的加入GC Roots集合,比如分代收集和局部回收,分代收集时候,新生代区周边的关联的对象就可能是GC Roots集合

总结: 堆周边的指针,指向了堆的对象,他就是一个Root

对象的finalization机制

Java提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
即垃圾回收此对象之前,总会调用这个对象的finalized()方法,finalized()方法允许在子类重写,通常用于在对象被回收时进行资源释放,比如关闭文件、套接字、数据库连接等

永远不要主动调用finalized()方法,应该交给垃圾回收器。理由有三:

  1. 在finalize() 时可能会导致对象复活。
  2. finalize() 方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,如果不发生GC,则finalize() 方法没有执行机会
  3. 一个糟糕的finalized() 会严重影响GC的性能

由于finalize()方法的存在,虚拟机中的对象一般处于三种可能状态:

  1. 可触及的:从根节点开始,可以到达这个对象
  2. 可复活的:对象的所有引用都被释放,但是对象可能在finalize() 方法中复活
  3. 不可触及的:对象的finalized() 被调用,并且没有复活,就会进入不可触及状态
    finalized()只会被调用一次,复活过的对象,无法再次被复活,死了的对象,就不用说了

具体过程:

  1. 如果对象A到GC Roots没有引用链,则进行第一次标记
  2. 进行筛选,判断对象有没有必要执行finalize()方法
    ①对象A没有重写finalize()方法,或者finalize()方法已经被调用过了,则视为没有必要执行,对象A成为不可触及的
    ②如果对象A重写了finalize()方法,且还未执行过,那么对象A会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行
    如果对象A在finalize()方法中与引用链上的任何一个对象建立了联系,【比如让别的对象=this自己】那么在第二次标记时,对象A会被移出即将回收集合

MAT(内存分析工具)与Jprofiler的GC Roots溯源

获取dump文件, 使用JVisualVM导出,
通过对象往上溯源它到底在被谁引用。
-XX:+HeapDumpOnOutOfMemoryError 程序出现OOM时生成Heap dump文件

清除阶段

标记-清除(Mark-Sweep)算法

执行过程:当堆中有效内存被耗尽时,STW(Stop the world),
标记所有被引用对象,一般是在对象的Header中记录为可达对象。清除没有标记为可达的对象。

缺点:效率不算高,GC时STW用户体验差,清理出来的内存空间不联系,产生内存碎片,需要维护一个空闲列表
何为清除:所谓清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里,下次有新对象需要加载时,判断垃圾的位置空间是否足够,如果够就存放。

复制算法

核心思想:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除这块内存块所有对象
优点:没有标记和清除过程,实现简单,运行高效。复制过去以后保证空间的连续性,不会出现碎片问题
缺点:需要两倍的内存空间,对于G1这种分拆为大量region的GC,复制而不是移动意味着GC需要维护region之间对象引用关系,内存占用和时间开销都大
如果系统的垃圾对象很少,就不理想,因为来回复制一大堆,都是存活对象在瞎跑,垃圾对象回收了一点点。

标记-压缩算法(Mark-Compact)

复制算法的高效性建立在存活对象少,垃圾对象多的前提下,这种情况在新生代常见。所以幸存者1区和2区是这样的算法。
而老年代大部分是存活对象,所以不能用复制算法
执行过程:第一阶段标记,和标记清除算法一样,第二阶段,将所有对象压缩到内存的一端,按顺序排放,之后清理边界所有的空间【也可以叫标记-清除-压缩算法】
优点:不用空闲列表了,没有内存碎片了,解决了标记清除算法的缺点,消除了复制算法中内存减半的高额代价

缺点:效率低于复制算法,更低于标记清楚算法,移动对象同时还需要调整其他对象对此对象的引用地址。移动过程中需要STW。

小结

mark-Sweepmark-CompactCopying
速度中等最慢最快
空间开销少(堆积碎片)少(不堆积碎片)两倍堆(不堆积碎片)
移动对象

其他清理思想

分代搜集算法

没有最优的算法,只有最适合的算法。
分代收集算法:不同生命周期的对象采用不同的收集方式,以便提高回收效率,一般是把Java堆分为新生代和老年代,这样可以根据各个年代的特点使用不同的回收算法,以便提高回收效率
年轻代:区域小,对象生命周期短。存活率低,回收频繁。用复制算法最好,最快。
老年代:区域大,对象生命周期长,存活率高,回收频率低。不能用复制算法,一般使用标记-清除或者标记-清除与标记-整理混合实现

以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。对于碎片问题,CMS采用 基于Mark-Compact算法的Serial Old回收器作为补偿措施。当内存回收不佳时,采用Serial Old执行Full GC,以达到对老年代的整理

增量收集算法

如果一次性将所有垃圾进行处理,需要造成系统长时间的停顿。让垃圾收集线程和应用程序线程交替进行,
每次垃圾收集只收集一小片区域的内存空间,接着切换到应用线程。依次反复直到垃圾收集完成。总的来说,增量收集算法的基础仍然是标记-清除算法和复制算法。通过对线程间冲突的妥善处理,允许垃圾收集线程分阶段的方式完成标记清理或者复制工作

缺点:因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降

分区算法

一般堆空间越大,一次GC所需的时间也就越长,为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标停顿时间,每次合理的回收若干个小区间region。
每个小区间独立使用,独立回收。

垃圾回收相关概念

通过System.gc()显式的提醒Full GC【同时对老年代和新生代进行回收】但是这个函数是无法保证对垃圾收集器的调用的
System.runFinalization() 强制调用失去引用的对象的finalize()方法。【强制调用以后,垃圾回收还能再次调用的】

手动gc理解不可达对象的回收行为

class LocalVarGV{
    //回收不了
    public void localGC1(){
        byte[] buffer = new byte[10 * 1024 * 1024]; //10MB
        System.gc();
    }

    //可以回收
    public void localGC2(){
        byte[] buffer = new byte[10 * 1024 * 1024]; //10MB
        buffer = null;
        System.gc();
    }

    //回收不了, buffer 在局部变量索引为1的槽(slot)中, 还在占用着
    public void localGC3(){
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();
    }

    //可以回收, buffer 在局部变量索引为1的槽(slot)中,等出了作用域,槽被复用,buffer 没有引用了可以回收
    public void localGC4(){
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int value = 0;
        System.gc();
    }

    //可以回收,方法一运行结束,栈帧弹出,局部变量表也就没了,引用断开可以回收
    public void localGC5(){
        localGC1();
        System.gc();
    }
}

内存溢出(OOM)

没有空闲内存,且垃圾回收器也无法提供更多内存。

内存泄漏

对象不会再被程序用到了,但是GC又不能将他们回收的情况,叫做内存泄漏。

举例:

1.单例模式,单例的生命周期和应用程序是一样长的(Runtime类)。
所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
2.一些提供close的资源未关闭导致内存泄漏
比如:数据库连接、网络连接、IO连接必须手动close,否则是不能被回收。

垃圾回收的并行与并发

并发:并不是真正意义上的同时进行,只是CPU把一个时间段划分成几个时间片段,然后快速切换。在一段时间里是同时运行的。
并行:多核,同时进行,在一个时间点同时发生。

并行(Parallel):指多条垃圾回收线程并行工作,但是此时用户线程仍是STW等待状态。
串行(Serial): 单线程执行。在单核CPU下并行可能更慢
并发(Concurrent):指用户线程与垃圾回收线程同时执行(一个时间段内),不会停顿用户程序运行(会但是时间很短)。例如CMS、 G1

安全点(Safepoint)

程序到了安全点才能执行GC,一般采用主动式中断【设置一个中断标志,各线程运行到SafePoint时主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起】。
安全点太少可能导致GC等待的时间太长,太多可能导致运行时性能问题。
一般选择一些执行时间较长的指令作为安全点,如方法调用、循环跳转、异常跳转等。

安全区域(Safe Region)

安全区域是指在一段代码片段中,对象得引用关系不会发生变化,在这个 区域中的任何位置开始GC都是安全的。
如线程处于Sleep状态或Blocked状态

再谈引用

强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?

Java引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference) 和虚引用(Phantom Reference),
这四种引用强度依次减弱,除了强引用,剩下三个引用都有对应的类。3个类都是public
强引用:只要强引用关系存在,永远不会回收掉被引用的对象。死不回收
强引用对象都是可触及的,强引用是造成Java内存泄漏的注意原因之一。
软引用:将要发生内存溢出的时候会回收这些对象。内存不足即回收
在系统将要发生溢出异常前,会把软引用对象列进回收范围之中进行第二次回收。例如:高速缓存用到软引用
弱引用:只要垃圾回收,就会收集弱引用对象。发现即回收
优于垃圾回收线程通常优先级很低,因此不一定很快发现弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间
软引用、弱引用都非常适合来保存可有可无的缓存数据。
虚引用:无法通过虚引用来获取一个对象的实例。虚引用的唯一目的是能在对象被回收时收到一个系统通知。
系统在GC掉虚引用对象后,会把这个对象放到一个引用队列里

弱引用WeakReference的Demo

    public static void main(String[] args) {
        //构造弱引用
        WeakHashMap<Integer, Integer> weakHashMap = new WeakHashMap<>();
        weakHashMap.put(128,128);
        //127以下有常量池,所以不会回收。128这个对象只有弱引用指向它,所以垃圾回收后会将其销毁
        System.out.println(weakHashMap.get(128));//128 
        System.gc();
        System.out.println("After GC:");
        System.out.println(weakHashMap.get(128));//null
    }

终结器引用:它用于实现对象的finalize()方法。

垃圾回收器

GC分类与性能指标

分类

分类标准分类
按线程数分串行回收器、并行回收器
按工作模式分独占式垃圾回收器、并发式垃圾回收器
按碎片处理方式分压缩式、非压缩式垃圾回收器
按工作的内存区间分年轻代垃圾回收器、老年代垃圾回收器

性能指标

吞吐量:运行用户代码的时间占总运行的比例。(总运行时间:程序的运行时间+内存回收的时间)
垃圾收集开销,吞吐量的补数,垃圾收集时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java堆区所占的内存大小
快速: 一个对象从诞生到被回收所经历的时间
在这里插入图片描述
G1(JDK7可用、JDK9默认)也就是现在标准:在最大吞吐量优先的情况下,降低停顿时间。

不同垃圾回收器概述

七款经典的垃圾回收器
串行回收器:Serial GC 、Serial Old
并行回收器:ParNew 、 Parallel Scavenge 、 Parallel Old
并发回收器:CMS、G1

回收分区:
在这里插入图片描述
搭配关系:
在这里插入图片描述
在这里插入图片描述

Serial 回收器

串行回收,STW机制执行内存回收
Serial 收集器采用复制算法, Serial Old 收集器采用标记-压缩算法,作为CMS回收器的备用方案
在这里插入图片描述
优势: 简单高效(单核CPU), 不适合实时交互性强的应用
-XX:+UseSerialGC 可以指定新生代和老年代都使用串行收集器,新生代用Serial GC,老年代用Serical Old GC

ParNew回收器:并行回收

ParNew 只能处理新生代的回收, par指的是并行, New指新生代
JDK9之后,不让用ParNew + Serial Old 了 只能 ParNew + CMS, CMS以 Serial Old作为补充。
在这里插入图片描述
单CPU情况Serial 比 ParNew 快。
-XX:+UseParNewGC 指定年轻代使用并行回收器,不影响老年代
-XX: ParllelGCThreads 限制线程数量, 默认开启和CUP核心相同的线程数

Parallel回收器:吞吐量优先

Parallel 和 ParNew不同点在于,Parallel目标是达到一个可控制的吞吐量,也被称为吞吐量优先的垃圾收集器。
高吞吐量适合在后台运算而不需要太多交互的任务,如批量处理,订单处理,科学计算。
Parallel Old 采用标记-压缩算法,同样也是基于并行回收和STW机制。

参数配置

-XX:+UseParallelGC 指定年轻代使用Parallel
-XX:+UseParallelOldGC 指定年轻代、老年代都使用并行回收器
-XX:ParallelGCThreads 设置年轻代并行手机线程数,一般和CPU数量相同。默认情况下,CPU数量小于8,它的值为CPU数量。 当CPU数量大于8,它的值为 3 + [5 * CPU] / 8
-XX:MaxGCPauseMillis 设置垃圾回收最大停顿时间(STW时间),单位毫秒,会尽可能把停顿时间控制在MaxGCPauseMillis 内,收集器会调整Java堆的大小或者其他参数(慎用)
-XX:GCTimeRatio 垃圾收集时间占总时间的比例。(垃圾回收开销),这个参数与设置最大停顿时间有一定矛盾性。暂停时间越长,Radio参数就容易超过设定比例。
-XX:+UseAdaptiveSizePolicy设置Parallel 收集器具有自适应调节策略。
这种模式下,年轻代的大小、Eden 和 Survivor的比例、晋升老年代的对象年龄参数等会被自动调整。以达堆大小、停顿时间和吞吐量间的平衡
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标吞吐量和停顿时间。

CMS:低延迟

JDK1.5推出,真正意义上的并发收集器,第一次实现了让垃圾回收线程与用户线程同时工作 ,延迟低,适合浏览器 B/S系统
CMS 算法采用标记-清除算法。但是CMS只能和ParNew 和 Serial 回收器配合。配合Serial Old解决碎片问题。在这里插入图片描述
初始标记:仅仅标记出GC Root能直接关联到的对象,速度很快,STW
并发标记:从GC Root关联对象开始遍历整个对象图,耗时长但是不需要停顿用户线程。
重新标记:修正并发标记期间,因用户程序运行而导致标记产生变动的那一部分对象的标记记录。 STW
并发清除:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。标记清除,不需要移动对象,可以并发执行。
CMS是当堆内存使用率达到一定阈值就得开始回收,(怕并发回收得时候程序内存不够用)。
优点:并发收集、低延迟
缺点:

  1. 会产出内存碎片,如果来个大对象就不得不提前触发Full GC
  2. CMS收集器对CPU资源非常敏感,占用线程,导致应用程序变慢,总吞吐量降低
  3. CMS无法收集浮动垃圾,并发标记阶段如果产生新的垃圾对象,CMS无法对这些垃圾进行标记,导致这些新产生得垃圾对象无法及时回收
参数配置

-XX:+UseConcMarkSweepGC 手动指定使用CMS,自动开启 ParNew回收器
-XX:CMSlnitiatingOccupanyFraction 设置堆内存使用率得阈值,一旦达到该阈值,便开始回收。如果内存增长比较缓慢,可以设置一个较大阈值,内存增长快就得降低阈值防止发生Full GC。
-XX:+UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理。
-XX:CMSFullGCBeforeCompaction 执行多少次Full GC后对内存空间进行压缩整理,0即每次FullGC都整理
-XX:ParallelCMSThreads 设置CMS得线程数量,默认为(ParallelGCThreads + 3) / 4

小结

最小化的使用内存和并行开销 Serial GC
最大化应用吞吐量 Parallel GC
最小化GC的中断或停顿时间 CMS GC

JDK9 CMS被标记弃用
JDK14 删除了CMS

G1回收器: 区域化分代式

应用程序应对的业务越来 越庞大、复杂、用户越来越多,为了不断适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。
官方给G1设定的目标是 在延迟可控的情况下获得尽可能高的吞吐量
G1把堆内存分割为很多不相关的区域(Region物理上不连续的),使用不同的Region来表示Eden、幸存者0,幸存者1,老年代等。
G1跟踪各个Region的垃圾堆积价值,每次根据允许的收集时间,优先回收价值最大的Region。垃圾优先(Garbage First)
JDK8 需要使用-XX:+UseG1GC 来启用。

优势

1.并行与并发 : G1在回收期间,可以有多个GC线程同时工作。 G1拥有与应用程序交替执行的能力,部分工作可以与应用程序同时运行。
2.分代收集 :堆空间分为若干个区域,这些区域中包含了逻辑上的Eden、新生代、老年代,不要求连续。
在这里插入图片描述
3.空间整合:Region之间是复制算法,整体上实际可以看做标记-压缩算法。能避免碎片化,有利于程序长时间运行。当堆很大时,优势更明显。
4.可预测的停顿时间模型:能够让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。
由于分区的原因,可以只选取部分区域回收,每次根据允许的时间,优先回收价值最大的Region。保证在有限的时间获取尽可能高的收集效率


相对于CMS,G1还不具备全方位、压倒性优势。G1垃圾收集产出的内存占用和额外执行负载比CMS高。
从经验看,小内存应用上CMS表现大概率会优于G1,G1在大内存应用上则发挥其优势,平衡点在6-8G之间

参数设置

-XX:+UseG1GC
-XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围在1-32MB之间。默认是堆内存的1/2000
-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标,默认值200ms
-XX:ParallelGCThread 设置STW时GC线程数的值,最多设置为8
-XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数的1/4左右
-XX:InitiatingHeapOccupancyPercent 设置 触发并发GC周期 的 Java堆占用率阈值, 默认45
在这里插入图片描述

G1使用步骤 ① 开启G1垃圾收集器 ②设置堆的最大内存 ③ 设置最大的停顿时间。

G1回收器的适用场景
  1. 面向服务端应用,针对具有大内存、多处理器的机器
  2. 低GC延迟,并具有大堆的应用程序,堆大小在6GB或更大时,可预测暂停时间可以低于0.5秒

替换CMS的情况:

  1. 超过50% 的Java堆被活动数据占用
  2. 对象分配频率或年代提升频率变化很大
  3. GC停顿时间过长(长于0.5至1秒)

七种垃圾回收器总结

在这里插入图片描述
在这里插入图片描述

面试问题:

垃圾收集算法有哪些? 如何判断一个对象是否可以回收?

垃圾收集器工作的基本流程

垃圾回收器各种常用参数

GC日志分析

YoungGC

在这里插入图片描述

Full GC

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甲 烷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值