zl程序教程

您现在的位置是:首页 >  Java

当前栏目

【JVM】一文搞懂常见GC算法

2023-04-18 16:13:19 时间

1、概述

GC目的:程序运行过程中可能会产生许多垃圾对象,持续占用内存会造成内存泄漏,最终可能导致内存溢出,迫使系统中断运行。

首先需要搞懂内存泄漏内存溢出的概念!!!

  • 内存泄漏(memory leak)是指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。通常少次数的内存无法及时回收并不会到程序造成什么影响,但是如果在内存本身就比较少获取多次导致内存无法正常回收时,就会导致内存不够用,最终导致内存溢出。
  • 内存溢出 (out of memory)指程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中。也就是说给你个int类型的存储数据大小的空间,但是却存储一个long类型的数据,这样就会导致内存溢出。

通俗的讲,比如2G内存分配给应用500M,但是由于一些对象没有进行回收,持续占用,导致应用可用内存小于500M(内存少点儿,就好像泄漏一部分)。如果这种情况一直发生,就会最终出现申请内存的大小超过可用内存,此时就会报OOM(内存不够用了,需求过量内存需要溢出点才能满足)。

什么是垃圾(garbage)?没有引用指向的一个或多个对象称为垃圾。

GC特点:频繁收集Young区;较少收集Old区;基本不动Perm区;
在这里插入图片描述
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代,因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)。

  • 普通GC(minor GC):只针对新生代区域的GC。
  • 全局GC(full GC):针对年老代的GC。

GC应用:

  • 年轻代中使用的是Minor GC,采用的是复制算法(Copying)
  • 老年代一般由标记清除或者标记清除与标记整理的混合实现

2、如何确定垃圾对象?

一般有两种方法:

  • 引用计数法(reference count):有一个引用指向一个对象,计数就加1 ,直到这个数为0,就会被当作垃圾进行回收。
    弊端:无法解决循环引用(对象A维护了一个成员属性,类型是对象B,对象B中维护了一个成员属性,类型是对象A,然后分别给这两个成员属性赋值,在将对象A赋值为null,将对象B赋值为null,这样对象A和对象B就都是null,但是a和b存在引用关系,这样a和b永远不会被回收)的问题。
  • 可达性分析:所谓的可达性就是通过一系列称为“GC Roots”的对象为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots到这个对象不可达)时,则说明此对象是不可用的。如下图所示,ABC可达,DE不可达。
    在这里插入图片描述
    GC Roots对象有哪些?(出自知乎 Java中的GCRoots到底有哪些?
    • 两个栈: Java栈 和 Native 栈中所有引用的对象;
    • 两个方法区:方法区中的常量和静态变量;
    • 所有线程对象;
    • 所有跨代引用对象;
    • 和已知 GCRoots 对象同属一个CardTable 的其他对象。(后两个不太不理解,望大佬指点)

3、GC算法

GC常用的算法有三个:

  1. Mark-Sweep(标记清除)
  2. Copying(复制)
  3. Mark-Compact(标记整理)

1)标记清除(Mark-Sweep)

标记清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

在这里插入图片描述
劣势:

  • 效率问题,标记和清除过程的效率都不高;
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2)复制算法(Copying)

复制算法就是将内存空间按容量分成两块。当这一块内存用完的时候,就将还存活着的对象复制到另外一块上面,然后把已经使用过的这一块一次清理掉。这样使得每次都是对半块内存进行内存回收。内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。
在这里插入图片描述

劣势:复制算法弥补了标记清除算法中,内存布局混乱的缺点,不过与此同时,它的缺点也是相当明显的。它浪费了一半的内容,这太要命了。如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重复一遍,复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视,所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

3)标记整理(Mark-Compact)

标记整理算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

在这里插入图片描述

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。

特点:

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
  • 消除了复制算法当中,内存减半的高额代价。

劣势:标记整理算法唯一的缺点就是效率不高,不仅要标记所有的存活对象,还要整理所有存活对象引用的地址,从效率上来说,标记整理算法低于复制算法。

4、GC算法总结

  • 内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。

  • 内存整齐度:复制算法=标记整理算法>标记清楚算法

  • 内存利用率:标记整理算法=标记清除算法>复制算法

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依旧不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程。

难道就没有一种最优算法吗?答案是无,没有最好的算法,只有最合适的算法,即分代收集算法(Generational Collection )。

  • 年轻代(Young Gen):年轻代特点是区域相对老年代较小,对象存活率低。这种情况复制算法的回收整理,速度是最快的,复制算法的效率只和当前存活对象大小有关,因而很适用于年轻代的回收,而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

  • 老年代(Tenure Gen):老年代的特点是区域较大,对象存活率高。这种情况,存在大量存活率高的对象,复制算法明显变得不合适,一般是由标记清除或者是标记清除与标记整理的混合实现。

    • Mark阶段的开销与存活对象的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程,通过并发、并行的形式提标记效率。
    • Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对象的移动,使其相对其它有对象移动步骤的回收算法,仍然是效率最好的,但是需要解决内存碎片问题。
    • Compact阶段的开销与存活对象的数据成正比,如上一条所描述,对于大量对象的移动是很大开销的,作为老年代的第一选择并不合适。


    基于上面的考虑,老年代一般由标记清除或者标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

  • 永久代(Permanent Space)元空间:存储class类,常量,方法描述:回收废弃常量和无用类。

分代收集法:也就是Jvm的垃圾回收所使用的算法,这种算法,既提高了内存空间的使用,而且根据各代的特点,针对处理,减少了cpu的使用率,提高了性能。

5、常见的垃圾收集器

收集器收集对象和算法收集器类型说明适用场景
Serial新生代,复制算法单线程简单高效,适合内存不大的情况
ParNew新生代,复制算法并行的多线程收集器ParNew垃圾收集器是Serial收集器的多线程版本搭配CMS垃圾回收器的首选
Parallel Scavenge新生代,复制算法并行的多线程收集器类似ParNew,更加关注吞吐量,达到一个可控制的吞吐量本身是Server级别多CPU机器上的默认GC方式,主要适合后台运算不太需要太多交互的任务
收集器收集对象和算法收集器类型说明适用场景
Serial Old老年代,标记整理算法单线程Client模式下虚拟机使用
Parallel Old老年代,标记整理算法并行的多线程收集器Parallel Scavenge收集器的老年代版本,为了配置Parallel Scavenge的面向吞吐量的特性而开发的对应组合在注重吞吐量以及CPU资源敏感的场合采用
CMS老年代,标记清除算法并行的多线程收集器尽可能的缩短垃圾收集时用户线程停止时间;缺点在于,1.内存碎片,2.需要更多的CPU资源,3.浮动垃圾问题,需要更大的堆空间重视服务的响应速度,系统停顿时间和用户体验的互联网或者B/S架构系统。互联网后端目前CMS是主流的垃圾回收器
G1跨新生代和老年代;标记整理+化整为零并行与并发收集器JDK1.7才正式引入,采用分区回收的思维,基本不牺牲吞吐量的前提下完成低停顿的内存回收;可预测的停顿是其最大的优势