JVM垃圾回收

概述

  • Java是支持自动垃圾回收的,有些语言不支持自动垃圾回收(C++)
  • 自动垃圾回收不是Java的首创

垃圾是什么?

  • 在 JVM 中垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

哪些区域需要回收垃圾?

  • 堆,方法区

为什么要GC

  • 如果不及时对内存进行垃圾清理,那么,这些垃圾所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用,甚至可能导致内存溢出,也就是常说的 OOM
  • 在回收时还可以将内存碎片进行整理(数组必须使用连续空间)

内存溢出和内存泄露

  • 内存溢出:经过垃圾回收后,内存中仍然无法储存创建的新对象,内存不够用溢出。
  • 内存泄露:IO流 close jdbc连接 close 没有关闭,一些对象已经不使用了,但是垃圾回收器不能将其判定为垃圾,这些对象就在后台默默占用内存,这种情况被称为内存泄露,大量的这种对象存在,一是导致内存溢出的原因。

自动垃圾回收

  • 好处: 解放程序员,对内存的管理更加合理,自动化
  • 坏处: 会影响程序员在内存管理方面的能力,如果出现异常无法精准定位问题所在
    在回收次数上频繁收集新生代Young区,较少收集Old区,基本不手机方法区(元空间)

垃圾回收相关算法

标记阶段算法

  • 判断你对象是否是垃圾对象,是否有引用指向对象

引用计数法(Reference Couting)(在现在的JVM中没有使用)

  • 有个计数器来计算对象的引用数量
	String s1 = new String("aaa");
	String s2 = s1; //有两个引用变量指向aaa
	s2 = null; -1
	s1 = null; -1
	//最终指向aaa的引用变量为0

优点: 实现简单,垃圾对象容易辨识;判定效率高,回收没有延迟性
确定: 需要单独的字段存储计数器,这样的做法增加了存储空间的开销;每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销;最严重的问题是,无法处理循环引用的情况(多个对象之间互相引用,没有其他外部引用指向他们,计数器不为0不能回收,产生内存泄露),因此在Java中垃圾回收器中没有这类算法。

可达性分析算法(跟搜索算法)

  • 实现思路:从一些为跟对象(GCRoots)的对象出发去查找,与根据对象直接或间接连接的对象就是存活对象,不与跟对象引用连接的对象就是垃圾对象
    在这里插入图片描述

GC Roots 可以是哪些元素?

  • 在虚拟机栈中引用的对象
  • 方法区中存储的静态成员指向的独享
  • 作为同步锁使用的synchronized
  • 在虚拟机内部使用的对象

对象的finalization机制

  • 在对象销毁之前还会调用finalize()方法

  • 当一个对象被标记为垃圾后,在真正被回收之前会调用一次object类中的finalize()方法
    -ps:自己不要调用finalize,留给垃圾回收器去调用

  • 由于 finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态:
    可触及的:从根节点开始,可以到达这个对象。这种对象不是垃圾,不用回收
    可复活的:对象的所有引用都被释放,但是对象有可能在 finalize()中复活。
    不可触及的:对象的 finalize()被调用,并且没有复活,那么就会进入不可触及状态。此时这种对象就是必须被回收的垃圾。
    以上 3 种状态中是由于 finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收

具体流程:
判定一个对象obj是否被能被回收,需要进行两次标记过程:

  • 如果obj 到GCRoot没有引用链,进行第一次标记
  • 进行进一步筛选:
    • 如果obj没有重写finalize方法,或者已经被调用过一次,则obj被判定为不可触及的,进行回收
    • 如果obj重写了finalize方法,且没有被执行过,则obj会被插入到队列中,由一个由jvm自动创建的低优先级Finalizer线程,触发其finalize方法
    • 在该线程中,如果obj在finalize方法中与GCRoots上的任何一个对象创建了联系,那么在第二次标记的时候obj就会被移出“即将被回收”的集合里,完成复活。如果再次出现没有引用的情况,finalize方法就不会再次触发
public class CanReliveObj {

	public static CanReliveObj obj;//类变量,属于 GC Root
	//此方法只能被调用一次
	 @Override
	protected void finalize() throws Throwable {
		//super.finalize();
		System.out.println("调用当前类重写的finalize()方法");
		obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
	}
	public static void main(String[] args) {
		try {
			obj = new CanReliveObj();
			// 对象第一次成功拯救自己
			obj = null;
			System.gc();//调用垃圾回收器,触发FULL GC  也不是调用后立刻就回收的,因为线程的执行权在操作系统
			System.out.println("第1次 gc");
			// 因为Finalizer线程优先级很低,暂停2秒,以等待它
			Thread.sleep(2000);
			if (obj == null) {
				System.out.println("obj is dead");
			} else {
				System.out.println("obj is still alive");
			}

			System.out.println("第2次 gc");
			// 下面这段代码与上面的完全相同,但是这次自救却失败了
			 obj= null;
			System.gc();
			// 因为Finalizer线程优先级很低,暂停2秒,以等待它
			Thread.sleep(2000);
			if (obj == null) {
				System.out.println("obj is dead");
			} else {
				System.out.println("obj is still alive");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

/**
控制台输出结果:
	第一次 gc
	调用当前重写的finalize()方法
	obj is still alive
	第二次 gc
	obj is dead
*/

垃圾回收阶段算法

标记-复制算法:

  • 将内存分为大小相等的两份,把当前使用的空间中存活的对象复制到另一个空间中,将正在使用的空间中垃圾对象清除。
    优点:可以减少内存碎片
    缺点:如果需要复制的对象数量多,效率就会低
    适用场景:存活对象少 新生代适合使用标记复制算法

标记-清除算法:

  • 清除不是真正的把对象垃圾清除掉
  • 将垃圾对象地址维护到一个空闲列表中,后面有新对象到来时,覆盖掉垃圾对象即可。
  • 特点:
    • 实现简单
    • 效率低,回收后有碎片产生

在这里插入图片描述

标记-压缩算法(标记-整理):

  • 先从根节点标记所有被应用对象,然后将所有存活对象压到内存的一端,按顺序排放。之后清除边界外的所有空间
  • 特点
    • 消除了标记-清除算法中的碎片空间,解决了标记-复制算法需要2倍空间的问题
    • 从效率上来说是三种算法中最慢的;移动对象的同时如果对象被其他对象引用,还需要引用对象的地址;在移动过程中会暂停用户应用程序,造成STW

在这里插入图片描述
标记-压缩算法与标记-清除算法的比较

  • 标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法
  • 二者的本质差异在于标记-清除算法是一种非移动式的回收算法(空闲列表记录位置),标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
  • 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

垃圾回收算法小结

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

在这里插入图片描述

垃圾回收相关概念

System.gc()

  • 默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用会显示触发Full GC,同时对老年代新生代进行回收,尝试释放丢弃对象占用的内存
  • 一般情况下不手动进行System.gc(),在特殊情况下可以使用

STW(Stop The World)

  • 指的是在进行GC时,会产生应用程序的停顿,这种停顿会让整个应用程序线程都停掉无响应,有点像卡顿,这样的情况被称为STW

垃圾回收器

  • 垃圾回收器是垃圾回收的具体实现者,垃圾回收算法是垃圾回收器的方法论

垃圾回收器分类

  • 按照线程数量:
    单线程回收器(Serial,Serial old)
    多线程回收器(Parallel)
    单线程垃圾回收器
    只有一个线程进行垃圾回收,适用于简单小型的使用场景,垃圾回收时其他用户线程会暂停。
    在这里插入图片描述

多线程垃圾回收器
使用多个线程进行垃圾回收,在多CPU的情况下大大的提升了垃圾回收的效率 同样会停止用户线程
在这里插入图片描述

  • 按照工作模式:
    独占式:垃圾回收线程执行时,其他线程暂停
    并行式:垃圾回收线程可以和用户线程同时执行(只在一部分阶段并行)
    **独占式垃圾回收器**
**并行式垃圾回收器**

  • 按照工作内存区间:

  • 老年代垃圾回收器

  • 年轻代垃圾回收器
    *下图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明他们能搭配使用。虚拟机所处的区域则代表他们存在于新生代还是老年代收集器。
    在这里插入图片描述

垃圾回收指标

  • 吞吐量:运行用户代码占总运行代码的时间
  • 垃圾收集开销:垃圾收集占用的时间与总运行时间的比例
  • 暂停时间:执行垃圾收集时占用的时间
  • 回收的速度
  • 占用内存大小:Java堆区所占用内存的大小

CMS垃圾回收器

  • CMS(ConcurrentMarkSweep,并发标记清除)是以获取最短停顿时间为目标的垃圾回收器,它在垃圾回收时使得用户线程和回收线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿

垃圾回收的过程

  • 初始标记:先进行STW,使用一条初始线程对所有与GCRoots直接关联的对象进行标记;
  • 并发标记:垃圾回收线程与用户线程并行,在此过程中进行可达性分析,标记处所有需要回收的废弃对象
  • 重新标记:STW,使用多条标记线程并发执行,并将刚才过程中新出现的废弃对象标记出来
  • 并发清除:使用一条GC线程,与用户线程并发执行,清除刚才标记的对象,这个过程最耗时。

在这里插入图片描述

由于并发标记和并发清除耗时最长,且可以与用户线程同时工作,因此总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS的优点
可以做到并发收集

CMS的弊端
1.CMS 是基于标记-清除算法来实现的,会产生内存碎片。
2.CMS 在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分
线程而导致应用程序变慢,总吞吐量会降低。
3.CMS 收集器无法处理浮动垃圾(floating garbage)。

三色标记算法(CMS)

  • 由于CMS有并发执行的过程,所以在标记垃圾对象时,有不确定性。
  • 所以在标记时将对象分为三种状态
  • 黑色:例如GCRoots 确定是存活的对象
  • 灰色:与黑色对象关联的对象,其中还有未扫描完的,之后还需要再次扫描
  • 白色:与黑色灰色都无关联的,垃圾收集算法不可达的对象

三色标记的过程:
1.先确立GCRoots,把GCRoots标记为黑色
2.与GCRoots相连接的标记为灰色
3.再次遍历灰色,灰色变为黑色,如果原来灰色下面有关联其他对象,则将其变为灰色。
4.重复步骤3,最终只保留黑色,灰色,回收白色对象
可能会出现漏标和错标的现象

  • 漏标:假设 GC 已经在遍历对象 B 了,而此时用户线程执行了 A.B=null 的操作,切断了 A 到 B 的引用,本来执行了 A.B=null 之后,B、D、E 都可以被回收了,但是由于 B 已经变为灰色,它仍会被当做存活对象,继续遍历下去。最终的结果就是本轮 GC 不会回收 B、D、E,留到下次 GC 时回收,也算是浮动垃圾的一部分。
    在这里插入图片描述
  • 错标:假设 GC 线程已经遍历到 B 了,此时用户线程执行了以下操作:B.D=null;//B 到 D 的引用被切断A.xx=D;//A 到 D 的引用被建立B 到 D 的引用被切断,且 A 到 D 的引用被建立。此时 GC 线程继续工作,由于 B 不再引用 D 了,尽管 A 又引用了 D,但是因为 A 已经标记为黑色,GC 不会再遍历 A 了,所以 D 会被标记为白色,最后被当做垃圾回收。可以看到错标的结果比漏表严重的多,浮动垃圾可以下次 GC 清理,而把不该回收的对象回收掉,将会造成程序运行错误。
  • 在这里插入图片描述

解决错标的问题
错标只有满足:灰色指向白色的引用全部断开;黑色建立了指向白色的引用;这两种情况才会发生,只需要解决这两种的其中一种原因就能避免错标的问题
解决办法:原始快照和增量更新。
原始快照打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。
G1回收器(Garbage-First)

  • 将堆内存的各个区域又分为较小的多个区,对这些个区域进行监测,对某一个区域中垃圾数量大的区域优先回收,也是并发收集的。
  • G1主要针对配备多核 CPU 及大容量内存的机器,以极高概率满足 GC 停顿时间的同时,还兼具高吞吐量的性能特征
    适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用

查看 JVM 垃圾回收器设置垃圾回收

//打印默认垃圾回收器
-XX:+PrintCommandLineFlags -version
JDK 8 默认的垃圾回收器
//年轻代使用 Parallel Scavenge GC
//老年代使用 Parallel Old GC
//打印垃圾回收详细信息
-XX:+PrintGCDetails -version
//设置默认垃圾回收器
//Serial 回收器
-XX:+UseSerialGC 年轻代使用 Serial GC, 老年代使用 Serial Old GC
//ParNew 回收器
-XX:+UseParNewGC 年轻代使用 ParNew GC,不影响老年代。

CMS 回收器
-XX:+UseConcMarkSweepGC 老年代使用 CMS GCG1 回收器
-XX:+UseG1GC 手动指定使用 G1 收集器执行内存回收任务。
-XX:G1HeapRegionSize 设置每个 Region 的大小
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

勇者六花i

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

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

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

打赏作者

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

抵扣说明:

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

余额充值