202108121235 - 垃圾回收

那 JVM 是如何判断哪些对象应该被回收?哪些应该被保持呢?

在古代,刑罚中有诛九族一说。指的是有些人犯大事时,皇上杀一人不足以平复内心的愤怒时,会对亲朋好友产生连带责任。诛九族时首先需要追溯到一个共同的祖先,再往下细数连坐。堆上的垃圾回收也有同样的思路。我们接下来就具体分析 JVM 中是如何进行垃圾回收的。
JVM 的 GC 动作,是不受程序控制的,它会在满足条件的时候,自动触发。
在发生 GC 的时候,一个对象,JVM 总能够找到引用它的祖先。找到最后,如果发现这个祖先已经名存实亡了,它们都会被清理掉。而能够躲过垃圾回收的那些祖先,比较特殊,它们的名字就叫作 GC Roots。
从 GC Roots 向下追溯、搜索,会产生一个叫作 Reference Chain 的链条。当一个对象不能和任何一个 GC Root 产生关系时,就会被无情的诛杀掉。
如图所示,Obj5、Obj6、Obj7,由于不能和 GC Root 产生关联,发生 GC 时,就会被摧毁。

GC Roots

主要分三类:

  • 活动线程的相关引用
  • 类的静态变量的引用
  • JNI引用

    注意:
  • GC Roots指的是引用,非对象
  • GC是找出所有活对象,并将其他空间认定为“无用”。而非找出所有死掉的对象再回收。所以即使JVM堆再大,基于tracing的GC方式,回收速度非常快。

软引用

软引用用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
可以看到,这种特性非常适合用在缓存技术上。比如网页缓存、图片缓存等。

// 伪代码
Object object = new Object();
SoftReference<Object> softRef = new SoftReference(object);

弱引用

弱引用对象相比较软引用,要更加无用一些,它拥有更短的生命周期。
当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期,在 Java 中,用 java.lang.ref.WeakReference 类来表示。
它的应用场景和软引用类似,可以在一些对内存更加敏感的系统里采用。

// 伪代码
Object object = new Object();
WeakReference<Object> softRef = new WeakReference(object);

虚引用(幻象引用)

内存中哪块区域会发生OOM

最常见的是堆

OOM的原因:

  • 内存容量太小,需要扩容;或条调整堆空间
  • 错误的引用发生内存泄漏。
  • 接口传参没有校验,外部传参超出范围。比如数据库查询的每页条数等
  • 对对外内存无限制使用
    典型的内存泄漏场景,原因在于没有及时的释放自己的引用,比如一个局部变量,被外部静态集合引用。

垃圾回收算法

我们简要介绍了一些常见的内存回收算法,目前,JVM 的垃圾回收器,都是对几种朴素算法的发扬光大。简单看一下它们的特点:

  • 复制算法(Copy)
    复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费。
  • 标记-清除(Mark-Sweep)
    效率一般,缺点是会造成内存碎片问题。
  • 标记-整理(Mark-Compact)
    效率比前两者要差,但没有空间浪费,也消除了内存碎片问题。
    所以,没有最优的算法,只有最合适的算法。

HotSpot垃圾回收器

STW

如果在垃圾回收的时候(不管是标记还是整理复制),又有新的对象进入怎么办?
为了保证程序不会乱套,最好的办法就是暂停用户的一切线程。也就是在这段时间,你是不能 new 对象的,只能等待。表现在 JVM 上就是短暂的卡顿,什么都干不了。这个头疼的现象,就叫作 Stop the world。简称 STW。
标记阶段,大多数是要 STW 的。如果不暂停用户进程,在标记对象的时候,有可能有其他用户线程会产生一些新的对象和引用,造成混乱。
现在的垃圾回收器,都会尽量去减少这个过程。但即使是最先进的 ZGC,也会有短暂的 STW 过程。我们要做的就是在现有基础设施上,尽量减少 GC 停顿。
虽然说 Java 为我们提供了非常棒的自动内存管理机制,但也不能滥用,因为它是有 STW 硬伤的。

posted @ 2025-03-20 18:39  钱塘江畔  阅读(21)  评论(0)    收藏  举报