JVM学习笔记:垃圾回收机制

  • 第一门使用内存动态分配和垃圾收集技术的语言—Lisp
  • C语言、C++等需要程序员手动释放开辟的内存。
  • 如果程序员忘记释放某些内存,就会产生内存泄漏,随着程序一直运行,内存泄漏越多,可能出现内存溢出(OOM)错误。
  • 内存泄漏: 对象不会再被程序使用了,垃圾回收机制又回收不掉,称为内存泄漏
  • 内存溢出OOM:产生对象的速度 > 垃圾回收的速度 ; 内存泄漏也是产生OOM的原因之一
  • 垃圾回收机制是Java的招牌能力,极大地提高了开发效率,使得程序员无序关心内存的释放,专注于业务代码。

概述

什么是垃圾?

运行的程序中没有任何引用指向的对象

若不及时回收清理,垃圾对象所占空间一直得不到释放,可能最终导致OOM内存溢出

垃圾回收机制的意义?

  1. 如果不及时的清理垃圾,垃圾所占内存空间越来越多,最后没有足够的空间来产生其他新对象,程序OOM
  2. 清除内存中的碎片空间,碎片整理将所占用的堆内存移动到堆的一端,以便JVM将整理出的内存分配给新的对象
  3. 随着应用程序的业务越来越庞大、复杂,用户越来越多,没有垃圾回收就不能保证程序的正常运行

Java垃圾回收机制

自动内存管理

  • 不需要程序员去手动释放内存,降低内存泄漏内存溢出的风险
  • 程序员只需要专注业务代码,不再关心内存的管理

自动内存管理对于程序员的影响

  1. 程序员过度依赖"自动内存管理",会弱化程序员在程序出现OOM时定位问题和解决问题的能力
  2. 了解JVM的自动内存分配和回收原理,才能在OOM时,快速根据日志信息定位问题和解决问题
  3. 需要排查内存泄漏和内存溢出时 | 当垃圾收集成员并发的瓶颈时
    必须对垃圾回收机制做出必要的监控和调节

那些内存需要回收?

  1. 是重点回收区域
    • 频繁收集Yong区
    • 较少收集Old区
  2. 方法区 基本不回收

垃圾标记阶段算法

  • 标记阶段就是为了判断对象是否存活
  • 需要从堆中区分存活对象与死亡对象
  • 如何判断:一个对象不再被其他存活对象引用时,判定为垃圾

引用计数算法(JVM不使用)

引用计数算法(Reference Counting)
对每个对象保存一个整形的引用计数器,记录对象被引用的情况

对于obj对象

  • 有其他对象引用了obj,计数器就 + 1 ; 引用断开,计数器 - 1
  • 引用计数器为0时,表示对象不再被使用了,标记为垃圾,可被会回收

优点:

  • 实现简单、判定效率高、回收器没有延迟

缺点:

  • 需要每个对象使用一个计数器的属性,增加了空间开销
  • 每次建立引用或引用断开,需要更新计数器,增加了时间开销
  • 无法处理循环引用的情况

可达性分析算法(JVM使用)

也叫根可达算法、根搜索算法

可达性分析算法和引用计数算法相比较:

  • 可达性分析算法解决了循环引用的问题
  • 同样简单、高效

可达性分析算法的思路

先建立一个GCRoots的概念

思路:

  1. 从根对象GCRoots为起点,从上到下搜索被GCRoots所连接的对象是否可达
  2. 使用可达性分析算法,内存中存活的对象都会被GCRoots直接or间接连通,走过的路径称为引用链(Reference Chain)
  3. GCRoots和目标对象没有引用链,是不可达的,该对象可标记为垃圾对象
    在这里插入图片描述

GCRoots可以是哪些对象?

  1. 栈中引用的对象(本地方法栈/虚拟机栈)
  2. 方法区中Java类的引用类型静态变量
  3. 方法区中常量的引用(如:字符串常量池里的引用)

总结:
虚拟机栈、本地方法栈、方法区、字符串常量池等对堆空间进行引用的都可以作为GCRoots进行可达性分析

垃圾回收阶段算法

复制算法

  • 使用两块内存空间(对应新生代中的幸存者 s0s1 )
  • 将s0中存活的对象复制到s1中,再清空s0,下一次复制s1到s0,清空s1
  • s0和s1 交替执行

优点:效率高,不产生内存碎片

缺点:需要2倍的空间

在这里插入图片描述
在新生代中使用复制算法
在这里插入图片描述

清除算法

  1. 从根节点开始标记所有被引用的对象(可达性分析算法)
  2. 并不是直接清除垃圾对象 ;记录垃圾对象,放到一个空闲列表中
  3. 有新对象需要分配空间,在空闲列表中判断空间是否够用,够用的话就用新对象替换垃圾对象
    在这里插入图片描述

优点:不需要移动(对比复制算法),实现简单
缺点:

  • 清理后空闲内存不连续,会产生内存碎片
  • 效率不高

压缩算法

  1. 从根节点开始标记所有被引用的对象(可达性分析算法)
  2. 针对清除算法的不足(内存碎片),整理存活的对象(压缩对象到内存一端),清理垃圾(边界外的空间),不会产生碎片

在这里插入图片描述

优点:

  • 对比复制算法:不使用额外空间(2倍内存)
  • 对比清除算法:算法执行后,内存区域不分散,不产生碎片空间

缺点:

  • 对比复制算法:比复制算法效率低
  • 移动对象时,如果对象被其他对象引用了,还要调整引用的地址
  • 需要暂停所有用户线程,STW

清除算法 VS 压缩算法

压缩算法的效果 = 清除算法 + 一次内存碎片整理

清除算法是一种不移动对象的回收算法(使用空闲列表记录位置),会产生内存碎片
压缩算法是一种会移动式对象的,不会产生内存碎片

垃圾回收算法总结

  • 复制算法效率最高,但是多浪费一倍空间
  • 清除算法不移动对象,会产生内存碎片
  • 压缩算法比复制算法多了标记阶段
  • 压缩算法比清除算法多了内存整理的阶段

分代收集

没有完美应用所有场景的垃圾回收算法,在不同的分区采用不同的回收算法,将利用率达到最大

分代收集思想:不同对象生命周期不同,根据回收的频率来选择合适的垃圾回收算法

  • Http请求中的Session对象、线程、Socket连接…与业务直接相关,生命周期较长
  • String对象,经常改变,产生大量对象,可能用一次就不再用了

.

  • 年轻代
    空间较小(年轻代:老年代= 1:2)对象生命周期短,存活率较低,回收频繁
    使用复制算法实现,复制算法的效率与当前存活对象的大小有关
    复制算法内存利用率低的问题,可以通过hotspot中的两个survior设计缓解
  • 老年代
    对象的生命周期长,存活率较高,回收没有年轻代频繁
    使用清除算法+压缩算法实现
    1. 标记(Mark)阶段的开销与存活对象数量成正比
    2. 清除(Sweep)阶段开销与管理区域的大小成正比
    3. 压缩(Compact)阶段开销与存活对象的数据成正比

垃圾回收器

垃圾回收器是对垃圾回收算法的实现

垃圾回收器分类

在这里插入图片描述

按线程数分类

  • 单线程垃圾回收器(Serial)–串行:只有一个GC线程,GC线程工作时其他用户线程暂停

    在这里插入图片描述

  • 多线程垃圾回收器(Parallel)–并行:多个线程同时工作,多核CPU下效率大大提升,也会在暂停用户线程
    在这里插入图片描述

按工作模式分类

  • 独占式:垃圾回收线程工作时,用户线程全部暂停(STW)
  • 并发式:垃圾回收线程与用户线程一次执行,不用暂停(从CMS这款垃圾回收器开始引入并发执行)
    在这里插入图片描述

按内存工作区域分类

  • 年轻代垃圾回收器:Serial、ParNew、Parallell
  • 老年代垃圾回收器:Serial Old、CMS、Parallel Old

HotSpot垃圾回收器

在这里插入图片描述
HotSpot提供了6种垃圾回收器
两个回收器连线代表这两个可以搭配使用

CMS垃圾回收器

概述

CMS追求低停顿(降低STW的时间),采用了用户线程与垃圾回收线程并发执行

CMS之前 单线程or多线程垃圾回收器都是独占式

回收过程

  1. 初始标记:
    STW,单线程独占, 标记所有与GCRoots直接关联的对象
  2. 并发标记:
    垃圾回收线程与用户线程并发执行(用户线程不用暂停),使用可达性分析算法标记处所有对象。
  3. 重新标记:
    STW,多线程独占,标记出新的垃圾对象
  4. 并发清除:
    垃圾回收线程与用户线程并发执行(清除垃圾对象,使用标记-清除算法)

优点:与用户线程并发执行,降低了STW的时间,不会明显的停顿
缺点:

  • 基于标记-清除算法,会产生内存碎片
  • 与用户线程并发,占用了线程导致程序变慢,吞吐量降低
  • 无法处理浮动垃圾(垃圾回收线程并发标记时,用户线程不暂停,标记完后随时可能产生新的垃圾对象,只能等待下次标记处理)

三色标记算法

将对象分为了三种状态

  • 黑色:该对象和它的属性全部被标记过了(如GCRoots对象)
  • 灰色:该对象被垃圾回收器扫描过,还有没被扫描过的引用
  • 白色:没有被扫描过,表示不可达

三色标记算法过程

  1. 刚开始确定的GCRoots为黑色
  2. 将与黑色对象关联的对象置为灰色
  3. 从灰色对象遍历,将灰色对象置为黑色,将新的黑色对象关联的对象置为灰色
  4. 重复第3步,遍历到没有灰色对象结束
  5. 清除白色对象(垃圾对象)

三色标记可能出现的问题

漏标

在这里插入图片描述
A 关联了 B ,B 又关联了D,E;

当前A是黑色(GCRoots),B是灰色

此时A与B的联系断开,B、D、E都可以被当做垃圾回收掉

但是,B已经是灰色,黑色对象只会遍历一次,下次从灰色对象开始遍历,将灰色对象置为黑色(三色标记算法第3步)

等待下一轮的垃圾回收,B、D、E就是浮动垃圾

本该作为垃圾回收掉的对象,又侥幸活了下来,这就是漏标

错标

A关联了B,B关联了C

此时断掉B、D的联系;但A与D又建立了联系;

黑色对象A只会遍历一次,因此 D 被标记为白色(垃圾对象),回收掉,但A又引用了D,这时程序就会运行错误
在这里插入图片描述

解决错标

在这里插入图片描述
打破错标产生的两个必要条件之一,就可以解决错标的问题
原始快照

  • 记录 灰色指向白色引用断开 的关系
  • 本次扫描结束后,再从灰色对象开始扫描一遍,重新进行标记

增量更新

  • 记录 黑色与白色建立引用 的关系
  • 本次扫描结束后,从记录中的黑色对象开始,重新标记

总结:
CMS与其他回收器区别在于:GC标记对象的同时,用户线程不会暂停,可能修改了对象的引用关系,导致标记垃圾,回收垃圾出问题
引入了三色算法来解决:对象可以有3种标记状态(黑、灰、白),将用户线程修改的对象引用关系记录下来,在重新标记阶段修正对象引用关系

G1垃圾回收器

G1 也是并发标记-清除

  • G1将堆中的每个区域分为更小不相关的区域(Region)
    使用不同的Region表示Eden、survivor0、survivor1、Old
  • G1避免整个堆进行垃圾收集,维护一个优先列表,根据区间的优先级(垃圾数量)来回收垃圾数量最多的空间Region
  • 由于侧重于回收最多垃圾的区域,所以叫垃圾优先(Garbage First)

G1垃圾回收器的回收过程

  1. 初始标记:单线程,标记出GCRoots直接关联的对象,用户线程暂停(该阶段速度较快)
  2. 并发标记:从GCRoots开始可达性分析,找出存活对象,和用户线程并发执行(该阶段较耗时)
  3. 最终标记:修正标记阶段用户线程修改的对象引用
  4. 筛选回收:对各个区域Region的回收价值(垃圾最多)和成本(STW时间最短)进行排序,回收时会暂停用户线程
    在这里插入图片描述

G1的使用场景

  • 控制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 GC。

G1 回收器

-XX:+UseG1GC //手动指定使用 G1 收集器执行内存回收任务
-XX:G1HeapRegionSize //设置每个 Region 的大小。

对象的 finalization 机制

Java允许开发人员编写 对象被销毁之前 的自定义处理逻辑

finalize() 方法

对象在销毁前,会调用finalize()方法

  • 垃圾回收器回收一个垃圾对象时,会先调用这个对象的finalize()方法,且finalize()方法只会被调用一次
  • finalize()方法可以重写,常用于进行一些资源的释放、清理(关闭文件/套接字/数据库连接…)

为什么不能主动去调用finalize()方法?

  1. 调用finalize()可能导致对象复活
  2. finalize()执行时间是不确定的,完全有GC线程决定
  3. 如果finalize()是死循环or循环次数特别多,会严重影响GC的性能

虚拟机中对象分为3种状态

  1. 可触及的:从根节点开始可以到达该对象

  2. 可复活的:对象的所有引用都被切断,但对象可能在finalize()中复活

  3. 不可触及的:对象调用了finalize()方法,且没有复活,进入不可触及状态(不能复活,finalize()只会调用一次)
    对象只有不可触及时,才能被回收

回收过程

判断一个对象A是否可以回收,至少经历两次标记过程

  1. 如果从GCRoots不能到达A,进行第一次标记
  2. 如果A没有重写finalize()或者已经调用过一次finalize(),A被判定为不可触及的
  3. 如果A重写了finalize() 并且 还没有调用过,A对象会被放入一个队列中,由一个Finalizer线程触发队列中对象的finalize()方法
  4. finalize()是对象复活的机会,GC会对队列中的对象进行第二次标记,如果A和存活对象建立联系,第二次标记时A会被取消回收。如果之后再次没有引用指向A,对象会直接编程不可触及状态(一个对象的finalize()只能调用一次)

代码演示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();//调用垃圾回收器
			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();
		}
	}
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值