JVM垃圾回收

什么是垃圾

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

可达性分析法

这个算法的基本思想就是通过一系列的称为GC Roots的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。可作为GC Roots的对象包括

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

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

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

  • 本地方法栈中 JNI 引用对象

  • Java 虚拟机内部引用,比如基本数据类型对应的 Class 对象,异常类,系统类加载器等

  • synchronized 持有的对象

  • JMXBean、本地代码缓存等

引用

  • 强引用(Strong Reference):有引用,不会进行垃圾回收

  • 软引用(Soft Reference):系统要内存溢出异常之前进行回收

  • 弱引用(Weak Reference):生命周期到下一次垃圾回收之前

  • 虚引用(Phantom Reference):设置虚引用是希望这个对象被回收时收到系统通知

垃圾回收算法

标记清除算法

算法分为标记清除阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,会带来两个明显的问题

  • 效率问题

  • 空间问题(标记清除后会产生大量不连续的碎片)

标记-复制算法

它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

  • 效率高,没有碎片,顺序分配内存

  • 空间浪费

新生代对象 98% 朝生夕死,不需要 1:1 分配,新生代分为一块较大的 Eden 和两个较小的 Survivor,每次使用Eden和其中的一块 Survivor。回收时,将 Eden 和 Survivor 存活的对象拷贝到另外一块 Survivor,然后清理 Eden和刚才用的 Survivor。HotSpot 虚拟机默认 Eden 和 Survivor 的比例是 8:1,只有 10% 被浪费。如果存活对象超过 10%,通过分配担保机制进入老年代。

标记-整理算法

根据老年代的特点特出的一种标记算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。

跨代引用

为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,避免把整个老年代加入到 GC Roots 扫描范围。

为了考虑空间占用和维护成本,可以记录不同的精度。

  • 字长精度:精确到一个机器字长(寻址位数)

  • 对象精度:精确到一个对象,此对象有字段存在跨代指针

  • 卡精度:精确到一块内存区域,该区域内有对象存在跨代指针

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个对象的字段存在跨代指针,那就将对应卡表的元素的值标识为 1,称这个元素变脏(Dirty),没有则标识为 0。垃圾收集时,将卡表变脏的元素一起加入到 GC Roots 进行扫描即可。

写屏障

写屏障(Write Barrier)可以理解成虚拟机层面的 AOP,在引用对象赋值时会产生一个环形(Around) 通知,供程序执行额外动作,有写前屏障(Pre-Write Barrier)和写后屏障(Post-Write Barrier),一般都是写后屏障。

三色标记

提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。

  • 黑色:根对象,或者该对象与它的子对象都被扫描

  • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象

  • 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

缺点:

并发标记会产生漏标的情况

解决方案:

在 CMS 采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在 G1 中,使用的是 STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象。

分代回收算法

主要根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。新生代用复制,老年代用标记清除或标记整理。

年轻代晋升老年代

  • 大对象,通过-XX:PretenureSizeThreshold参数设置

  • 达到年龄,每次 Minor GC 后年龄 +1,通过-XX:MaxTenuringThreshold参数设置,取值在 0~15 之间,并行收集器默认为 15,CMS 默认是 6

Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.

  • 动态年龄判定,当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。比如 Survivor 空间为 1M,Survivor 中大于 5 岁的对象占用空间大于0.5M,那么大于等于 5 岁的对象直接进入老年代

垃圾收集器

图中虚线上方表示:新生代可用的垃圾收集器,虚线下方表示:老年代可用的垃圾收集器,实线相连代表:收集器之间可组合使用,CMS和Serial Old用红色实线相连代表:当CMS触发concurrent mode failure,此时会进入STW(stop the world),用Serial Old垃圾收集器来回收。

Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。它的单线程的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(Stop The World),直到它收集结束。新生代采用复制算法,老年代采用标记-整理算法。

优点是简单而高效(与其他收集器的单线程相比),不适用服务器环境。

JVM参数:

ruby 复制代码
-XX:+UseSerialGC -XX:+UseSerialOldGC

ParNew收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。新生代采用复制算法,老年代采用标记-整理算法。

它是许多运行在 Server 模式下的虚拟机的首选的新生代收集器,除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。

JVM参数:

ruby 复制代码
-XX:+UseParNewGC

ParallelScavenge收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,默认的收集线程数跟 cpu 核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。新生代采用复制算法,老年代采用标记-整理算法。

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。

JVM参数:

ruby 复制代码
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads -XX:MaxGCPauseMillis -XX:GCTimeRatio

Serial Old收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

Parallel Old收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和"标记-整理"算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。JDK9 中标记为废弃,JDK14 中将移除。

从名字中的 Mark Sweep 这两个词可以看出,CMS 收集器是一种标记-清除算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;

  • 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

  • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

  • 并发清除:开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

优点:

  • 并发收集、低停顿

缺点:

  • 对 CPU 资源敏感:默认回收线程是(CPU数量+3)/4

  • 无法处理浮动垃圾:标记过程中用户线程产生的新垃圾,需要在下次 GC 时清理。收集器默认在老年代达到68%时开始执行

  • 大量空间碎片:使用的是标记清除算法

  • 执行过程中的不确定性:会出现上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发 full gc,也就是concurrent mode failure,此时会进入 stop the world,用 serial old垃圾收集器来回收。

JVM参数:

ruby 复制代码
## 启用cms
-XX:+UseConcMarkSweepGC

## 并发的GC线程数
-XX:ConcGCThreads

## FullGC之后做压缩整理(减少碎片)
-XX:+UseCMSCompactAtFullCollection

## n次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-XX:CMSFullGCsBeforeCompaction

## 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-XX:CMSInitiatingOccupancyFraction

## 只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-XX:+UseCMSInitiatingOccupancyOnly

## 在CMSGC前启动一次minorgc,目的在于减少老年代对年轻代的引用,降低CMSGC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段
-XX:+CMSScavengeBeforeRemark

## 表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallellnitialMarkEnabled

## 在重新标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled

G1收集器

G1 是 JDK1.7 新特性,将整个堆划分成多个大小相等的独立区域(Region)。虽然还保留新生代和老年代,但他们是逻辑上的概念,物理上不再隔离。每个Region既可能属于新生代,也可能属于老年代。

Region类型

  • Eden

  • Survivor

  • Old

  • Humongous:大对象(大小超过Region的50%空间),会存在跨 Region 的对象

大小设置

默认是自动设置的,我们通过-Xms-Xmx来设置堆内存的大小,然后JVM启动时发现采用 G1 作为垃圾回收器(通过参数-XX:UseG1GC指定),会用堆内存大小除以 2048(默认 2048 个 Region),得到每个 Region 的大小。

Region的大小取值范围是1M~32M,且必须为2的N次幂,可以通过-XX:G1HeapRegionSize参数手动指定。

动态Region

初始情况下,堆内存的 5% 空间为新生代的大小,但是在系统运行期间,Region 的数量是动态变化的,不过新生代最多占比也不会超过 60%。

另外,一旦 Region 进行了垃圾回收,此时新生代的 Region 数量还会减少,这些其实都是动态的。

可以通过参数-XX:G1NewSizePercent来设置新生代的初始占比,默认 5%;通过参数-XX:G1MaxNewSizePercent来设置新生代的最大占比,默认 60%。 同样通过-XX:SurvivorRatio=8设置Eden 和 Survivor 比例。

回收价值

G1 之所以能够做到控制停顿时间,是因为它会追踪每个Region里的 回收价值 。所谓回收价值,是指每个 Region里有多少垃圾对象,如果进行回收,耗时多长,能够回收掉多少。

通过-XX:MaxGCPauseMills参数可以设定预期停顿时间,表示G1执行GC时最多让系统停顿多长时间,默认200ms。

回收步骤

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记:会 STW,但不耗时

  • 并发标记:不会 STW,但耗时。但 GC 线程和工作线程并行,影响不大

  • 最终标记:会 STW,但不耗时

  • 筛选回收:会 STW,根据设置的预期停顿时间计算回收价值,回收多次

  • 停止回收:一旦空闲的 Region 数据量达到了堆内存的 5%,就会立即停止回收

可以通过参数-XX:G1HeapWastePercent配置这个空闲 Region 的占比,默认为 5%。

  • 回收失败:需要将存活对象拷贝到其他 Region 中,如果万一在次过程中没有空闲的 Region 可以承载存活对象,就会触发 Full GC。此时,JVM 会立即停止程序,然后采用 Serial Old 收集器进行单线程标记、清除、压缩整理,空出一批 Region,这个过程是非常缓慢的。

G1 维护的卡表要比 CMS 的复杂的多,双向引用,会有接近 20% 的内存消耗。

相关推荐
秋千码途22 分钟前
小架构step系列08:logback.xml的配置
xml·java·logback
飞翔的佩奇23 分钟前
Java项目:基于SSM框架实现的旅游协会管理系统【ssm+B/S架构+源码+数据库+毕业论文】
java·数据库·mysql·毕业设计·ssm·旅游·jsp
时来天地皆同力.43 分钟前
Java面试基础:概念
java·开发语言·jvm
找不到、了1 小时前
Spring的Bean原型模式下的使用
java·spring·原型模式
阿华的代码王国1 小时前
【Android】搭配安卓环境及设备连接
android·java
YuTaoShao2 小时前
【LeetCode 热题 100】141. 环形链表——快慢指针
java·算法·leetcode·链表
铲子Zzz2 小时前
Java使用接口AES进行加密+微信小程序接收解密
java·开发语言·微信小程序
霖檬ing2 小时前
K8s——配置管理(1)
java·贪心算法·kubernetes
Vic101013 小时前
Java 开发笔记:多线程查询逻辑的抽象与优化
java·服务器·笔记
Biaobiaone3 小时前
Java中的生产消费模型解析
java·开发语言