JVM 垃圾回收机制

用户头像
Alex🐒
关注
发布于: 2020 年 07 月 22 日

垃圾回收机制

垃圾回收(Garbage Collection,GC)针对堆(以及方法区)中的垃圾数据进行清理,防止内存泄漏,有效的使用可以使用的内存。

Java 的引用类型

如果判断对象是否为垃圾,与对象是否被引用有关,Java 中有 4 种引用类型:

  • 强引用(StrongReference):只要引用存在,垃圾回收器永远不会回收。Object obj = new Object();

  • 弱引用(WeakReference):生存到下一次垃圾回收,无论当前内存是否足够,第二次垃圾回收都会回收被弱引用关联的对象。

  • 软引用(SoftReference):在发生内存溢出之前进行回收,如果回收后没有足够的内存会OOM。

  • 虚引用(PhantomReference):不会对对象的生命周期有任何影响,无法通过引用取到对象实例,在对象被回收前收到一个系统通知。

判断对象可回收

引用计数法

为对象添加一个引用计数器,每当有一个引用,计数器加 1,当引用失效计数器减 1。任何时候当计数器为 0 时,对象不再被使用。



但是这种做法可能会遇见循环引用的问题(例如:A引用B,B引用A),计数器始终不为 0。

可达性分析法

为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法,通过一系列的 GC Roots 对象作为起点进行搜索,如果在 GC roots 和一个对象之间没有任何可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象



作为 GC Roots 中的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中的对象;

  • 方法区中静态属性引用的对象;

  • 方法区中常量引用的对象;

  • 本地方法栈(即一般说的Native方法)中的对象

  • 系统类加载器(Application ClassLoader)加载的对象

如何判断常量

运行时常量池中的废弃常量要回收,即常量池中的字符串没有任何 String 对象引用的话,是可以被回收的

如何判断类

类的回收与对象的回收方法不同,但是回收类至少要满足以下条件:

  • 类的所有实例被回收

  • 加载类的 ClassLoader 被回收

  • 类对应的 java.lang.Class 对象没有被引用

STW

STW 是 GC 中很重要的概念,全称 Stop the world,即程序全局暂停时间,GC 优化算法都是围绕减少 STW 的时间或频率。

为什么需要 STW

如果没有 STW,会出现浮动垃圾(即标记完是存活对象,线程随之结束,可能对象已经变成了垃圾),回收性能差、效率低。

垃圾回收算法

标记-清除

最基础的垃圾回收算法,分为两个阶段:1. 标记存活的对象;2. 清除未标记的对象

标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。有如下缺陷:

  1. 空间问题,产生大量内存碎片,可能发生大对象不能找到连续的可用空间,从而导致 FullGC

复制

为了解决“标记-清除”算法内存碎片化的缺陷,将内存划分为两部分,每次只使用其中一部分,当内存满后将存活的对象复制到另一块内存区域,并清除当前区域,完成回收。有如下缺陷:

  1. 虽然不会出现碎片,但需要两倍的存储空间(只有一半的空间被利用)

  2. 当存活对象增多,效率会大大降低

优化的复制算法

在复制算法的基础上,使用三个分区进行处理(即新生代的空间划分方式 Eden/S0/S1),默认空间比例 Eden:S0:S1为 8:1:1,有效内存(即可分配新生对象的内存)是总内存的 90%(Eden 和 S0 区都可以分配新生对象)。



IBM 的研究表明,新生代中的对象 98% 是朝生夕死的,所以 8:1:1 的比例是十分合理的。(每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存是会被浪费的)。

标记-整理

结合了以上两个算法,标记阶段和“标记-清除”算法相同,区别是:标记后不清理对象,而是将存活对象移向内存的一端,使内存紧凑排列,然后清除其他区域的对象。

相比“标记-清除”算法会牺牲一些效率,但是不会出现碎片,提高内存利用率。

增量

每次垃圾回收一部分区域,减少 STW 时间。线程切换和上下文转换会造成垃圾回收时间增加。

分代收集

根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老年代(Old Generation)和新生代(Young Generation)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时会有大量垃圾需要被回收,因此不同区域可选择不同的算法。

垃圾收集器

分代收集法是目前大部分 JVM 所采用的方法,可根据应用分别为新生代和老年代选择合适的垃圾收集器(如上图)。新生代垃圾收集器和老年代垃圾收集器的组合使用,G1是一种特殊的垃圾收集器,未采用传统的新生代/老年代划分。最新版本的 JVM 提供的 G1 的升级版本 ZGC。

-XX:+UseSerialGC
相当于 Serial + SerialOld,新生代和老年代都是单线程处理

-XX:+UseParallelGC
相当于 Parallel Scavenge + SerialOld,新生代是多线程处理,老年代是单线程处理

-XX:+UseParallelOldGC:
相当于 Parallel Scavenge + ParallelOld,新生代和老年代都是多线程并行处理

-XX:+UseConcMarkSweepGC
相当于 ParNew + CMS + Serial Old,新生代使用多线程的 ParNew 收集器,老年代中使用 CMS 并发收集器(相对STW最低),采用 CMS 有可能出现 Concurrent Mode Failure 的情况,如果出现了,降级为 SerialOld 收集器

并行 vs 并发

首先理解并行和并发的概念:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上。

Serial 收集器

启用命令 -XX:+UseSerialGC,相当于 Serial+Serial Old

使用“复制”算法的“单线程”新生代垃圾回收器,在 jvm client 模式下的默认新生代收集器。

单线程回收,回收期间必须 STW(应用线程暂停),单个 CPU 的运行环境比较快,效率较高。图示:

ParNew 收集器

启用命令 -UseParNewGC,相当于 ParNew+Serial Old

Serial 收集器的“多线程”实现,采用“复制”算法的“多线程”新生代垃圾回收器。

多线程“并行“回收,依然会STW,在多 CPU 的运行环境下使用。图示:



默认开启的收集线程数与 CPU 的数量相同,可以使用 -XX:ParallelGCThreads 限制线程数

Parallel Scavenge 收集器

启用命令 -XX:+UseParallelGC,相当于 Parallel+Serial Old;-XX:+UseParallelOldGC,相当于 Parallel+ Parallel Old

采用“复制”算法的“多线程”新生代垃圾回收器,在 jvm server 模式下的默认新生代收集器。

重点关注的是程序达到一个可控制的吞吐量:吞吐量=运行时间 / (运行时间 + 垃圾收集时间)

自适应调节策略是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别,可设置最大停顿时间(-XX:MaxGCPauseMills)和 GC 时间占比(-XX:GCTimeRatio)。图示:

Serial Old 收集器

Serial Old 是 Serial 垃圾收集器老年代版本,使用“标记-整理”算法的“单线程”老年代垃圾回收器,在 jvm client 模式下的默认老生代收集器。在 Server 模式下,主要有两个用途: 1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用;2. 作为老年代中使用 CMS 收集器的后备垃圾收集方案。

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 的老年代版本,使用“标记-整理”算法的“多线程”老年代收集器。在 JDK1.6 才开始提供。 在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。

CMS(Concurrent Mark Swep)收集器

启用命令 -XX:+UseConcMarkSweepGC,相当于 ParNew + CMS + Serial Old

基于“标记-清除”算法的“多线程”老年代“并发”收集器,重点关注的是程序获得最短 STW 时间,适合交互占比多的应用程序。



整个收集过程分主要为以下步骤:

  1. 初始标记(CMS initial mark,需要 STW,耗时短),只扫描直接和 GC Roots 关联的对象

  2. 并发标记(CMS concurrent mark,耗时长,不需要 STW),基于(1)的结果进行 GC Roots Tracing,所有可到达的对象都在本阶段中标记。

  3. 并发预处理(CMS concurrent preclean,耗时长),为了减少(3)的处理时间,标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。

  4. 重新标记(CMS remark,需要 STW,耗时一般大于(1),远远小于(2)),重新扫描堆中的对象,进行可达性分析,标记活着的对象,处理可能新产生的垃圾。

  5. 并发清除(CMS concurrent sweep,耗时长,不需要 STW),激活用户线程,同时清理那些无效的对象。

  6. 并发重置(CMS concurrent reset),重置 CMS 收集器的数据结构,等待下一次垃圾回收。



补充说明:

标记清除会产生大量碎片,为大对象分配内存的时候,会出现老年代还有很大的空间,但是缺少足够大的连续空间,不得不开启一次 Full GC。CMS 提供以下两个参数:

-XX:+UseCMSCompactAtFullCollection 收集开关参数,用于在收集器进行 FullGC 完开启内存碎片的合并整理过程。

-XX:CMSFullGCsBeforeCompaction 参数用于设置执行多少次不压缩的 FullGC 后,执行一次带压缩的 FullGC。

G1(Garbage first) 收集器

启用命令 -XX:+UseG1GC

G1 相比与 CMS 收集器,两个最突出的改进是:

  1. 基于"标记-整理"算法,不产生内存碎片。

  2. 可以控制 STW 时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。



G1 收集器避免全区域垃圾收集,把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。






发布于: 2020 年 07 月 22 日 阅读数: 31
用户头像

Alex🐒

关注

还未添加个人签名 2020.04.30 加入

还未添加个人简介

评论

发布
暂无评论
JVM 垃圾回收机制