在JVM内存区域中,虚拟机栈,本地方法栈,程序计数器的生命周期和线程绑定,栈中栈帧的数据结构和大小都是确定的,在出栈后即可被回收,因此在这三个区域中不需要太多考虑垃圾收集的问题,GC关注的区域主要集中在堆内存和方法区中,尤其是堆内存,是GC发生最频繁也是最关键的区域,想要指定一个好的垃圾收集策略主要需要考虑以下三个问题
- 哪些内存需要被回收
- 在什么时间点回收
- 怎样回收
本文就从以下三个角度出发来剖析JVM堆内存垃圾回收的策略
哪些内存需要被回收
引用计数法
引用计数法即判断一个对象被其他对象所引用的次数,如果某个对象被其它对象的引用次数为0,就认为该对象可以被回收,引用计数法是最简单的判断对象存活的方式,但现有的虚拟机实现中并不太会使用这种内存回收方法,主要原因是它难以解决对象引用关系中循环引用的情况,考虑如下场景:对象A引用B而对象B又同时引用A,在改情况下对象A、B被引用的次数各是1次,在这种情况下使用引用计数法对象A、B将永远都无法被回收
可达性分析
可达性分析是现在JVM垃圾收集器中普遍用来判断需要回收对象的方法,该方法定义了一群垃圾回收中对象引用链中的根节点GC Root,从GC Root开始顺着对象的引用链向下依次枚举对象,不能被枚举到的对象既需要被回收的不可达对象,为保证回收过程的安全和可靠,枚举的过程中需要基于一份某个时间点的内存快照,因此在整个枚举过程完成之前,所有内存中对象的状态都需要维持在这份快照中的状态,这也就意味着需要在这个过程中需要暂停所有用户线程的操作。这个过程被称为Stop The World,即使是在号称能够让用户线程停顿最少的CMS回收器中,在枚举GC Root时也需要暂停所有用户线程
枚举根节点
在进行可达性分析的时候,JVM需要先枚举出所有的GC Root根节点,由于方法区中的静态属性和常量都是确定的,因此比较容易枚举,但如果需要将栈中所有的数据一一枚举出来判断是否是指向堆内存中对象的引用效率将会十分的低下。JVM采取了一种OopMap的数据结构来解决这个问题,栈中每个栈帧持有一个OopMap,每个OopMap中维护了该栈帧中哪些地址代表着引用,并在代码执行到特定的地方更新,因此,在枚举GC Root时,只要遍历每个栈帧的OopMap就能高效的枚举出每个栈帧中的GC Root
GC Root节点类型
- 虚拟机栈的本地变量表中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中Native对象引用的对象
在什么时间点回收
之前谈到枚举根节点的时候提到过,OopMap只会在代码跑到特定的位置时更新,那也就意味着在在OopMap的两个更新点之间,OopMap中的维护的引用信息可能是过时的,因此只有在OopMap被更新的那个时间点才能够进行GC,这样的更新点被称为Safepoint,为保证GC时线程都能跑到最近的Safepoint才中断开始垃圾收集,主要有以下两种策略
- 抢占式中断:当垃圾收集器判断需要GC时,先中断所有的线程,依次检查所有线程是否都跑到了Safepoint上,然后唤醒没有跑到Safepoint的线程让其跑到后再中断开始GC,这个策略流程十分复杂冗长,现在已经没有主流的垃圾收集器使用该种方式来实现中断
- 主动式中断:当垃圾收集器判断需要GC时,不中断线程,而是给每个线程发一个标志位通知线程跑到最近的Safepoint后自行中断
Safe Region
线程只有在活动的情况下才能够响应中断,而休眠或阻塞中的线程是无法响应中断的,我们不能要求休眠或阻塞的线程必须在跑到Safepoint后才能休眠或阻塞,而JVM也不可能等待这些线程重新被分配到CPU资源跑到Safepoint后才进行垃圾回收。但在不活动状态下的线程,是不会影响到整个GC的过程的,因此当线程在即将进入不活动状态前需要将自己标记为进入Safe Region,GC时将会忽略标志自己进入Safe Region的线程所关联的内存区域,在线程被唤醒后,首先需要判断最近一次GC的根节点枚举是否已经完成,只有等到最近一次的根节点枚举完成了该线程才能离开Safe Region
怎样回收
标记——清除算法
找出需要被回收的对象并标记,然后清除该对象所占用的内存空间,这种回收算法将会有以下两个缺点:
- 标记和清除的过程都需要花费很多时间,效率较低
- 被标记的对象被清除后会内存空间会变得不连续,出现大量的内存碎片空间
复制算法
将内存空间分为相等的大小的两份,将对象创建在其中的一份中,在垃圾收集的过程中只需要将存货的对象移至另一份空间然后清除当前空间即可完成垃圾收集,这种收集方式操作简单,且不会产生碎片空间,但同时引入的问题就是内存产生了巨大的浪费,这个缺点导致了它不太适用于对象声明周期较稳定的内存区域,而可以适用于处理那些声明周期较短的对象(因为每次垃圾收集后存货的对象会较少,可以使用比较小的Survivor区域存放存货对象),事实上在JVM年轻代的垃圾回收算法中就使用了复制算法
标记——整理算法
找出需要被回收的对象并标记,然后将存活的对象移动到内存区域的最前端,然后清空边界外的内存区域,这种算法保证了清理后内存空间的连续性,但整理内存空间也带来了额外的开销