JVM-04.垃圾回收机制

思维导图:点击查看思维导图.

1. 判断一个对象可以被回收

1.1.引用计数法

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

1.2.可达性分析

通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定是可回收对象。不可达对象变为可回收对象至少要经过两次标记。两次标记后仍然是可回收对象,则将面临回收。
打印垃圾回收详细参数

-XX:+PrintGCDetail -verbose:gc

2.四种引用

2.1. 强引用

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。只有所有GC Roots对象都不通过[强引用]该对象,该对象才会被垃圾回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

2.2. 软引用

软引用需要用 SoftReference 类来实现,当系统内存足够时它就不会被回收,当系统内存空间不足时它会被回收。可以配合引用队列来释放软引用自身。软引用通常用在对内存敏感的程序中(内存不够,不重要的资源使用软引用)。
list对SoftReference强引用,byte[]对list弱引用
打印结果:
前4个对象为null已被回收!!!配合软引用队列后为软引用队列自身也将被回收,打印一个有值的对象地址!!

public static final int int_4MB = 4 * 1024 * 1024;
// -Xms15m -Xmx15m -Xmn10m
public static void main(String[] args) {
    List<SoftReference<byte[]>> list = new ArrayList<>();
    // 引用队列
    ReferenceQueue<byte[]> queue = new ReferenceQueue();
    for (int i = 0; i < 5; i++) {
        // 关联引用队列,当软引用所关联的byte[]被回收时,软引用会自己加到queue引用队列中
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[int_4MB],queue);
        System.out.println(softReference.get()); // 打印了5个 [B@1540e19d ......
        list.add(softReference);
    }
    // 打印五个,前四个为null
    System.out.println("----------------------");
    for (SoftReference<byte[]> reference : list) {
        System.out.println(reference.get());  // 打印4个null,一个[B@6d6f6e28
    }
    // 将为null的移除
    // remove() 和 poll()都是用来从队列头部删除一个元素
    // queue为空时remove抛异常,poll返回null
    Reference<? extends byte[]> poll = queue.poll();
    while (poll != null) {
        list.remove(poll);
        poll = queue.poll();
    }
    // 添加了五个,发现只剩下一个了
    System.out.println("----------------------");
    for (SoftReference<byte[]> reference : list) {
        System.out.println(reference.get()); //打印 一个[B@6d6f6e28
    }
}

2.3. 弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,只要垃圾回收机制一运行,不管内存空间是否足够,都会回收该对象。可以配合引用队列来释放软引用自身。
在这里插入图片描述

2.4. 虚引用

必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,有Reference Handler线程调用虚引用相关方法直接释放内存。虚引用的主要作用是跟踪对象被垃圾回收的状态。

2.5.终结器引用

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),在有Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象。
在这里插入图片描述

3.垃圾回收算法

在这里插入图片描述

3.1.标记清除法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标注清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。但是会带来两个明显的问题
       1. 效率问题 (如果需要标记的对象太多,效率不高)
       2. 空间问题(标记清除后会产生大量不连续的碎片)
在这里插入图片描述

3.2. 复制算法(copying)

为了解决标记清除法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
       特点:
       1.内存效率高,不会产生碎片
       2.内存空间会被压缩一半
       3.存活对象过多复制算法的效率会大大降低

在这里插入图片描述

3.3. 标记整理算法(Mark-Compact)

根据老年代的特点特出的一种标记算法,结合了复制算法与标记清除法。 标记阶段和标记清除算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
       特点:速度慢,不会有内存碎片

在这里插入图片描述

3.4. 分代收集理论

分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 将堆划分为老年代(Old Generation)和新生代(Young Generation)。根据不同区域选择不同的算法。

默认情况下各区占比:
新生代:老年代 = 1:2
伊甸园:From:To:= 8:1:1

老年代:每次垃圾回收时只有少量对象需要被回收
新生代:每次垃圾回收时都有大量垃圾需要被回收
在这里插入图片描述

  • 对象首先分配在伊甸园区Eden
  • 新生代空间不足时,触发Minor GC,伊甸园和From存活的对象使用Copy算法复制到To区,存活对象加1并交换From区和To区
  • Minor GC会触发STW(Stop The World),暂停其他的用户线程,等待垃圾回收结束,用户线程才恢复运行
  • 当对象年龄超过阈值时,会晋升到老年代,最大年龄是15(4bit)
  • 当老年代空间不足时,会先尝试触发Minor GC,如果之后空间仍然不足,会触发Full GC,STW时间会更长
  • 如果老年代空间不足,会触发OutOfMemory

新生代中,每次收集都会有大量对象(一般近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可 以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选 择 “标记-清除”“标记-整理” 算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以 上

老年代对象:

  • 静态变量
  • 缓存
  • Spring容器等

JVM 垃圾回收相关参数:

参数含义
-Xms堆初始大小
-Xmx或-XX:MaxHeapSize=size堆最大大小
-Xmn或(-XX:NewSIze=Size±XX:MaxNew=Size)新生代大小
-XX:InitialSurvivoeRatio=ratio和-xx:+UseAdaptiveSizePolicy幸存区比例(动态)
-XX:MaxTenuringThreshold=threshold晋升阈值
-XX:+PrintTernuringDistribution晋升详情
-XX:+PrintGCDetails -verbose:gc打印GC详情
-XX:+ScaveengeBeforeFullGCFullGC前MinorGC

垃圾回收分析:
在这里插入图片描述
在这里插入图片描述
注意:大对像在新生空间不足,老年代空间足够时会直接放入老年代。如果在一个线程内,不会导致真个程序结束(一个线程内的OutOfMemory不会导致整个主线程结束)

下面使用VisualGC插件演示垃圾回收,可使用jvisualVm查看:

// 演示代码
public class TestGC {
    byte [] a = new byte[1024 *100]; // 100k 每次new该对象时占用100k
    public static void main(String[] args) throws InterruptedException {
        ArrayList<TestGC> list = new ArrayList<TestGC>();
        while (true) {
            list.add(new TestGC());
            Thread.sleep(10);
        }
    }
}

在这里插入图片描述

3.5.垃圾回收器

在这里插入图片描述

3.5.1. Serial收集器 (串行)

基本参数含义
-XX:+UseSerialGC新生代使用 Serial 垃圾收集器
-XX:+UseSerialOldGC老年代使用Serial Old 垃圾收集器
  • 单线程
  • 必须暂停其他所有的工作线程( “Stop The World” ),直到收集结束
  • 新生代采用复制算法,老年代采用标记整理算法
  • 简单而高效(没有多线程交互的开销)
  • 堆内存较小,适合个人电脑

新生代版本: Serial收集器
老年代版本: Serial Old收集器
Serial收集器:最基本,最久远的垃圾收集器,适合堆内存小程序,现已过时
Serial Old收集器:在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS垃圾收集器的后备方案- XQ

3.5.2. Parallel 收集器 与 ParNew 收集器(吞吐量优先)

Parallel Scavenge 与 ParallelOld :

基本参数含义
-XX:+UseParallelGC新生代使用 Parallel Scavenge 垃圾收集器 JDK8 默认
-XX:+UseParallelOldGC老年代使用 ParallelOld 垃圾收集器 JDK8 默认
-XX:ParallelGCThreads=X垃圾回收使用线程数,默认服务器核数
-XX:+UseAdaptiveSizePolicy采用自适应的方式调整新生代
-XX:GCTimeRatio=99默认值99 即 1% 的时间用于垃圾收集
-XX:MaxGCPauseMills = XXms最大占比毫秒数,与上面的参数有冲突,设置一个即可
-XX:TargetSurvivorRatioSurvivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个 年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
  • Serial收集器的多线程版本
  • 新生代采用复制算法,老年代采用标记整理算法
  • JDK8默认的新生代和老年代垃圾收集器
  • 吞吐量即CPU中用于运行用户代码的时间与CPU总消耗时间的比值 Parallel 关注点是高效率的利用CPU
  • 堆内存较大,多核CPU(默认与系统核数相同,可以使用参数- XX:ParallelGCThreads指定)
  • 单位时间内,STW时间最短
  • 在注重吞吐量以及 CPU资源的场合,都可以优先考虑 Parallel 收集器和Parallel Old收集器

新生代版本: Parallel 收集器
老年代版本: ParallelOld 收集器在这里插入图片描述

ParNew收集器:

含义基本参数
新生代使用 ParNew 垃圾收集器-XX:+UseParNewGC
  • 与Parallel收集器十分相似,为了配合CMS使用而衍生出来的版本
  • 新生代采用复制算法,老年代采用标记整理算法
  • 许多运行在Server模式下的虚拟机的首选垃圾收集器,除了Serial收集器外,只有它能与CMS收集器配合工作,并在很长一段时间里占据主流。
    在这里插入图片描述

3.5.3. CMS收集器(响应时间优先)

基本参数含义
-XX:+UseConcMarkSweepGC老年代使用 CMS 垃圾收集器
-XX:ConcGCThreads并发的 GC 线程数
-XX:+UseCMSCompactAtFullCollectionFullGC 后做压缩整理(内存碎片整理,会STW)
-XX:CMSFullGCsBeforeCompaction=0多少次FullGC后会进行一次压缩整理,默认0(每次)
-XX:CMSInitiatingOccupancyFraction=92老年代使用达到该比例时会触发FullGC,默认92% ,相当于还有8%不能使用,大对象过多可适当调小比例
-XX:+UseCMSInitiatingOccupancyOnly只使用设定的回收阈值,不设置仅第一次时使用设定的阈值,后面会自动调整(一般不用配置)
-XX:+CMSScavengeBeforeRemark在CMS GC前启动一次 MinorGC,减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80% 都在标记阶段
-XX:+CMSParallellnitialMarkEnabled初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled重新标记的时候多线程执行,缩短STW
  • 多线程
  • 并发收集、低停顿,以获取最短回收停顿时间为目标的收集器,十分注重用户体验
  • 是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作(初始标记与查询标记STW)
  • 标记清除算法实现
  • 老年代垃圾收集器,通常配合ParNew使用
  • 堆内存较大,多核CPU
  • 尽可能让单次的STW时间最短
           缺点明显:
           1.对CPU资源敏感(会和服务抢资源)
           2.无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了
           3.使用“标记-清除”算法会导致大量空间碎片产生(配置压缩整理参数会进行内存整理)
           4.它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
           5.执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发 FullGC ,也就是"concurrent mode failure",此时会进入 STW,用 Serial Old 垃圾收集器来回收

在这里插入图片描述

CMS垃圾收集过程步骤:
1.初始标记:仅标记GC Roots的直接关联对象,并且暂停其他所有线程(STW)
2.并发标记:并发标记阶段就是从GC Roots 的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程(无STW), 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
3.重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,会暂停应用程序线程(STW)。主要用到 三色标记 里的 增量更新算法 做重新标记。
4.并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理
5.并发重置:重置本次GC过程中的标记数据。

  • 初始标记和并发标记速度较快,重新标记速度慢
  • 重新标记这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
  • 重新标记时会扫描整个堆内存
3.5.3.1.三色标记

垃圾回收器中的应用: 在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生 三色标记: 把GCroots可达性分析遍历对象过程中遇到的对象, 按照 “是否访问过” 这个条件标记成以下三种颜色:
黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若 在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
在这里插入图片描述
上图对应代码如下:

// 假设从某一位置开始做CMS垃圾回收某一过程
public class Test{ 
	public static void main(String[] args) {
		// 初始标记a对象
		A a = new A(); 
		// 1.开始做并发标记,下面一行代码未执行
		// 2.从A开始扫描,A中成员变量b,d,扫描完,标记A为黑色
		// 3.D对象d=null,无法扫描到D默认白色
		// 4.开始扫描B中成员变量c,d
		// 5.c指向的C被扫描完,C无任何引用标记为黑色
		// 6.d还未开始扫描,B标记为灰色,D默认为白色
		// 读 
		D d = a.b.d; 
		// 应用程序与垃圾收集程序仍并发执行
		// 执行到a.b.d = null;
		// 并发标记开始扫描B中对象d
		// b.d被置为null,无法扫描到,还是默认的白色
		// 写
		a.b.d = null;
		// 并发标记继续执行,a对象A已被标记为黑色,表示已扫描完
		// 此时将a.d = d,即A指向d,d指向D
		// A为黑色,不会继续扫描,D仍然是白色
		// --> 发生漏标
		// 写 
		a.d = d;
		// 重新标记 --> STW直接将新增应用编程黑色
	}
} 
class A { 
	B b = new B();
	D d = null; 
} 
class B { 
	C c = new C();
 	D d = new D();
}
class C {
} 
class D {
}

多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(GCRoot)被销毁,这个 GCRoot 引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。 另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障

上述列子就演示了流标的情况,漏标会导致被引用的对象被当成垃圾误删除。这会导致严重的问题。有两种方式解决给问题:
       1.增量更新(Incremental Update): 当黑色对象插入新的指向白色对象的引用时, 将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次(重新标记)。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
       2.原始快照(Snapshot At The Beginning,SATB): 是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮 GC 清理中能存活下来,待下一轮GC的时候重新扫描,这个对象也有可能是浮动垃圾)

以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
在赋值操作前后,加入一些处理(可以参考AOP的概念):
写屏障:

// 写前操作
// 写屏障实现增量更新:记录新的应用对象
// 写屏障实现SATB: 获取旧值 -> 记录原来的引用对象
beforeWrite();
// 写操作 -->赋值操作 a.d = d
write()
// 写后操作
afterWrite();

读屏障:

// 读前操作 
// 记录读取到的对象
beforeRead();
// 读操作
read();
// 读后操作
afterRead();

使用可达性分析的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式 以是广度/深度遍历等。对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:
       CMS: 写屏障 + 增量更新
       G1: 写屏障 + SATB
       ZGC: 读屏障
读/写屏障还有一些其他的功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每种垃圾回收器都会有所不同。

3.5.3.2.记忆集与卡表

新生代的 GCRoots 可达性分析扫描过程中有可能出现跨代的现象(例如老年代引用了新生代对象)。这种对象如果再去老年代扫描,效率过低。因此在新生代中引入了(Remember Set)的数据结构(记录从非收集区到收集区的指针集合)避免扫描整个老年代
所有设计部分区域收集的垃圾收集器(例如CMS、G1、ZGC、Shenandoah等)都会有这个问题。
hotspot使用了目前最常见的 卡表”(cardtable) 的方式实现了记忆集关于卡表与记忆集的关系, 可以类比为Java语言中HashMap与Map的关系。卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为 “卡页”
一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。 GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。

3.5.4. G1(Garbage-First)收集器 (同时注重响应时间与吞吐量)

  • JDK9 默认垃圾回收器
  • 同时注重吞吐量和低延时,默认暂停目标200ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region
  • 整体上是标记+整理算法,两个区域之间是复制算法

G1抛弃了之前固定区域的分代思想,将Java堆划分为多个大小相等的独立区域 ,即:Region,保留了年轻代和老年代的概念,但它们不再隔开,而是可以不连续Region 集合,G1 还使用了Humongous区,用于存放大对象。JVM 最多可以有 2048 个 Region。一般Region大小等于堆大小除以 2048,比如堆大小为8192M,则 Region 大小为 4M,推荐默认的计算方式,但也可以使用参数(-XX:G1HeapRegionSize)进行调节。

G1 默认新生代对堆内存的占比是 5% (其他区域暂未分配空间(下图灰色区域),随着程序运行,新生代 Region 逐渐增加,占整个空堆内存空间的比例也会增加,但最多占比不会超过堆内存的 60%,“-XX:G1NewSizePercent” 可设置新生代对堆内存占比, “-XX:G1MaxNewSizePercent”可设置新生代堆内存最多占比)。比如堆大小为 8192M,那么初始年轻代占据 400MB 左右的内存,对应大概是100个 Region。年轻代中的 Eden 和 Survivor 对应的 Region 仍然是默认 8:1:1。注意:一个 Region 可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说 Region 的区域功能可能会动态变化。

G1对于对象什么时候会转移到老年代与前面的垃圾收集器基本一致。但是G1专门设置了用于大对象分配的 Region — Humongous 区。G1 中大对象的判定规则就是是否超过一个 Region 大小的 50%。而且一个大对象如果太大,可能会横跨多个 Region 来存放。Humongous区不会发生拷贝YoungGC 时会优先考虑回收Humongous区的大对象。 Humongous 区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销在这里插入图片描述

基本参数含义
-XX:+UseG1GC使用 G1 垃圾收集器 JDK9 默认,JDK8 使用此参数开启
-XX:ParallelGCThreads垃圾回收使用线程数,默认服务器核数
-XX:G1HeapRegionSize指定每个Region大小(必须为2的n次幂),建议默认
-XX:MaxGCPauseMillis目标暂停时间(默认200ms)
-XX:G1NewSizePercent新生代内存初始空间(默认占整个堆的5%),JVM会根据实际情况自动调整
-XX:G1MaxNewSizePercent新生代最大内存空间,默认60%
-XX:TargetSurvivorRatioSurvivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个 年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent老年代触发MixedGC阈值,例如有2048个Region,当有超过45%的Region都得是Old的时候就可能触发MixedGC
-XX:G1MixedGCLiveThresholdPercentRegion中的存活对象低于此值时才会回收该Region(默认85%),如果超过这个值,存活对象过多,回收的的意义不大
-XX:G1MixedGCCountTarget在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一 会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长
-XX:G1HeapWastePercentGC过程中空出来的Region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立 即停止混合回收,意味着本次混合回收就结束了

G1垃圾回收过程
在这里插入图片描述
初始标记(initial mark): 暂停所有的其他线程(STW ),并记录下GC Roots直接能引用的对象,速度很快
并发标记(Concurrent Marking): 同CMS的并发标记
最终标记(Remark): 暂停所有的其他线程(STW ),同CMS的重新标记
筛选回收(Cleanup): 暂停所有的其他线程(STW ),筛选回收阶段首先对各个 Region 的回收价值成本进行排序,根据用户所期望的GC停顿时间(默认200ms,可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划

例如:老年代此时有 1000 个 Region 都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收这 800 个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个 Region 中的存活对象复制到另一个 Region 中,这种不会像 CMS 那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region (Garbage First 由来)。比如一个 Region 花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1会优先选择后面这个 Region 回收。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。

G1被视为JDK1.7以上版本Java虚拟机的一个重要进化特征。它具备以下特点:

  • 并行与并发: G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短 STW 停顿时间。部分其他收集器原本需要停顿Java线程来执行 GC 动作,G1 收集器可以在程序执行过程中边运行边回收。
  • 分代收集: 虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合: 与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿: 这是G1相对于CMS的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内完成垃圾收集。

毫无疑问, 可以由用户指定期望的停顿时间是G1收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。不过,这里设置的“期望值”必须是符合实际的, 不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。它默认的停顿目标为两百毫秒,一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发 Full GC 反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

G1垃圾收集分类

YoungGC
        YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的 Region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC。

MixedGC
        老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent,默认45%)设定的值则触发,回收所有的 Young部分 Old (根据期望的GC停顿时间确定Old区垃圾收集的优先顺序)以及Humongous区,正常情况G1的垃圾收集是先做 MixedGC,主要使用复制算法,需要把各个 Region 中存活的对象拷贝到别的 Region 里去,拷贝过程中如果发现没有足够的空 Region 能够承载拷贝对象就会触发一次

Full GC
        Full GC 停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批 Region 来供下一次 MixedGC 使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)。Full GC 除了收集年轻代和老年代之外,也会将Humongous区一并回收。

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

  • Young Collection + Concurrent Mark:新生代垃圾收集 + 并发标记
    在这里插入图片描述

  • Mixed Collection :混合收集
    在这里插入图片描述
    有选择的回收老年代,回收垃圾最多的老年代

  • Full GC
    在这里插入图片描述

3.5.5. ZGC收集器收集器

JDK11加入的低延迟的垃圾回收器
ZGC主要目标:

  • 支持TB量级的堆
  • 最大GC停顿时间不超10ms
  • 奠定未来GC特性的基础
  • 最糟糕的情况下吞吐量会降低15%

ZGC内存布局
ZGC收集器是一款基于Region内存布局的, 暂时不设分代的, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整 理算法的, 以低延迟为首要目标的一款垃圾收集器。
大中小Region: 小Region固定2M,中型固定32M,大型不固定容量,值存放一个对象,不会发生拷贝。
在这里插入图片描述


安全点与安全区域

安全点
        就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。 这些特定的安全点位置主要有以下几种:
        1.方法返回之前
        2. 调用某个方法之后
        3. 抛出异常的位置
        4. 循环的末尾
大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的。

安全区域
        Safe Point 是对正在执行的线程设定的。 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。 因此 JVM 引入了 Safe Region。 Safe Region 是指在一段代码片段中,引用关系不会发生变化在这个区域内的任意地方开始 GC 都是安全的


为什么G1用SATB?CMS用增量更新?
        SATB相对增量更新效率会高(但SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话 G1 的代价会比 CMS 高,所以 G1选择 SATB 不深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。


什么场景适合使用G1
        1. 50%以上的堆被存活对象占用
        2. 对象分配和晋升的速度变化非常大
        3. 垃圾回收时间特别长,超过1秒
        3. 8GB以上的堆内存(建议值)
        4. 停顿时间是500ms以内


G1垃圾收集器优化建议
        假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的 60% 了,此时才触发年轻代GC。 那么存活下来的对象可能就会很多,此时就会导致 Survivor 区放不下那么多的对象,就会进入老年代中。 或者是年轻代GC过后,存活下来的对象过多,导致进入 Survivor 区后触发了动态年龄判定规则,达到了 Survivor 区域的 50%,也会快速导致一些对象进入老年代中。
        所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证年轻代GC别太频繁的同时,还得考虑每次GC过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发MixedGC。


每秒几十万并发的系统如何优化JVM
        Kafka 类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署 Kafka 需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于Eden区的 Young GC 是很快的,这种情况下它的执行还会很快吗?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按 Kafka 这个并发量放满三四十G的 Eden 区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为 YoungGC 卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用 G1 收集器,设置 -XX:MaxGCPauseMills 为 50ms,假设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。
        G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。


如何选择垃圾收集器
        1. 优先调整堆的大小让服务器自己来选择
        2. 如果内存小于100M,使用串行收集器
        3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
        4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选
        5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器
        6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值