垃圾回收,就是要把那些不再使用的对象找出来然后清理掉,释放其占用的内存空间。在java中判断对象死亡有两种方式:
(1)引用计数法
(2)可达性分析法
一、引用计数法
引用计数法简单、高效,它的做法是给对象添加一个引用计数器,每当有一个地方引用该对象,这个计数器就加1;当引用失效时,计数器就减1。如果计数器为0了,说明该对象不再被引用,成为死亡对象。不过这种算法有一个致命缺点,就是无法处理对象相互引用的情况,所以主流的虚拟机不再使用该方法。
二、可达性分析
它的做法是,通过一系列被称为“GC Roots”的对象作为起点,从这些起点开始往下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象没有和任何引用链相连,即称为该对象不可达,认为该对象死亡。如下图D、E、F三个对象不可达:
图1
2.1 哪些对象可以作为GC Roots
- 栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
面试题1:如何找到GC ROOTS,是遍历整个方法区、栈区吗?
现在的很多应用仅仅方法区就有数百兆,如果要逐个遍历里面的引用,那么必然会消耗很多时间。因此虚拟机应当是有办法直接得知哪些地方存在着对象引用。
在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来;以及在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息了。
2.2 引用类型
2.2.1 强引用
强引用就是我们日常开发中最常见的引用,只要强引用还在,对象就不会被回收。例如:
String str = new String("hello");
2.2.2 软引用
软引用需要专门声明,被软引用关联的对象,在垃圾回收时,如果没强引用引用他且内存不足时,会被回收,这个特性特别适合用来做缓存。
SoftReference<String> str = new SoftReference<String>("hello");
2.2.3 弱引用
弱引用也需要专门声明,被弱引用关联的对象,在垃圾回收时,如果没强引用引用他就会被回收。弱引用最常见的用途是实现可自动清理的集合或者队列。例如
WeakReference<String> str = new WeakReference<String>("hello");
参考:Netty源码-内存泄漏检测toLeakAwareBuffer - 简书
2.2.4 虚引用
虚引用是最弱的引用,需要特别声明,它完全不会影响对象的生存时间,唯一的作用是在对象被回收时发一个系统通知。例如
PhantomReference<String> phantom = new PhantomReference<>(new String("hello"), new ReferenceQueue<>());
2.3 起死回生
对象在被判定为死亡后,并不会立刻被回收,而是要经过一个过程才会被回收。在这个回收过程中,死亡对象还有可能活过来(详见2.4可达性分析--标记过程)。
2.4 可达性分析明细
2.4.1 基础概念--三色标记法
1、黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有直接子引用都已经扫描过。
黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
2、灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
3、白色: 表示对象尚未被垃圾收集器访问过。
在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
2.4.2 标记过程
1、在GC并发标记刚开始时,所以对象默认均为白色(注意:是默认为白色,而不是把堆全部扫描一遍放在白色队列中)。
2、将所有GCRoots直接引用的对象标记为灰色队列。
3、判断若灰色队列中的对象不存在子引用,则将其放入黑色队列,若存在子引用对
象,则将其所有的子引用对象放入灰色集合,当前对象放入黑色集合。
4、按照步骤三,以此类推,直至灰色队列中的所有对象变成黑色后,本轮标记完成,且当前白色集合内的对象称为不可达对象,既垃圾对象。
面试题:可达性分析初始阶段,为啥必须STW?
因为这项分析工作必须在一个能确保一致性的快照中进行。
这里的一致性的意思是指在整个分析期间整个执行系统看起来像被冻结在某个时间点上,不可以出现在分析过程中对象引用关系还在不断的变化,该点不满足的话分析结果的准确性就无法得到保证。
2.4.3 可达性分析存在的问题--多标、少标
(1)多标(也叫浮动垃圾)
比如E对象在GC扫描D对象时,E还正在被D引用,那么此时E就被标记为灰色,此时业务逻辑的变化,D指向E的引用被置空了,这时候E以及后续子引用本应该被当成垃圾回收,但是此时E已经被
标记为灰色,导致E对象以及其子对象没有被及时清理掉,变成了浮动垃圾,还有在并发标记开始后的新对象也叫浮动垃圾。
浮动垃圾的解决通常做法是直接全部当成黑色,本轮不会进行清除,下次GC进行回收。
(2)少标:(也叫漏标)
比如D对象引用E对象,E引用G,此时GC正好处于D已经变成黑色,E处于灰色,G是白色的情况下,此时因为业务逻辑的变化,E不引用G了,D对象引用了G,按照三色标记法看,黑色对象是已完成状态,不可能再去找子引用,所以G就不会变成灰色,这样就会造成白色对象此时正在被线程使用中,但是无法被标记成灰色或者黑色,造成一个正在被使用的对象被错误回收。(参考:三色标记法与读写屏障 - 简书)
注意:
(1)如果垃圾标记线程与用户线程并行运行,可能出现垃圾漏标、多标的问题。
(2)如果同一时刻,仅有垃圾回收串行或并行执行时但无用户线程运行,则不会有垃圾漏标、多标。
举例:
(1)年轻代垃圾收集:分代垃圾回收器,以及G1,年轻代垃圾回收会STW,不会有用户线程,所以不会存在垃圾漏标、多标的情况。
(2)年老代垃圾收集:串行、并行垃圾回收器不会有垃圾漏标、多标问题,但CMS、G1在老年代垃圾回收会有漏标、多标问题。
2.4.4 多标解决
本次GC不处理,下次GC处理回收
2.4.5 漏标解决
漏标只有同时满足以下两个条件时才会发生:
条件一:灰色对象 断开了 白色对象的引用(即灰色对象原来成员变量的引用发生了变化)
条件二:黑色对象 重新引用了 该白色对象(即黑色对象成员变量增加了新的引用)
(1)漏标解决方法一:写屏障+增量更新法(Incremental Update)
1、当一个白色对象被一个黑色对象引用,将这个新增的引用放在一个队列中,重新标记阶段STW对这个队列进行再次扫描;
2、写屏障是个aop操作:重新引用之前,加入一段代码,将这个新引用放在一个队列;
3、CMS垃圾回收器使用该方案。
(2)漏标解决方法二:写屏障+SATB算法(Snapshot At The Beginning)
1、当原来的引用断开之前,将原来的引用记录放在一个队列中,然后GC依然相当于可以按照最开始的快照进行扫描;
2、重新标记阶段来保证该队列中的对象在本次gc过程中是存活的;
3、写屏障是个aop操作:断开引用之前,加入一段代码,将这个被断开对象放在一个队列;
4、G1垃圾收集器使用该方法。
(3)漏标解决方法三:读屏障
【ZGC垃圾收集器使用】
(4)对比
- SATB 算法是关注引用的删除。
- 增量更算法关注引用的增加。
此处的读写屏障是指读写之前加一段处理代码,与多线程中的读写屏障不同,详细参考:三色标记法与读写屏障 - 简书