垃圾标记阶段:对象存活判断
- 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,需要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
- 简单地说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
判断对象存活一般有两种方式:引用计数器算法和可达性分析算法。
引用计数算法
对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
对于一个对象A,只要有任何对象引用了A,则A的引用计数器就+1,引用失效时,引用计数器-1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨别;判定效率高,回收没有延迟性
缺点:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销;每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销;引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命的缺陷,导致在Java的垃圾回收器中没有使用这类算法。
可达性分析算法
能够有效解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
基本思路:
- 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
GC Roots包括以下元素:
- Java虚拟机栈中引用的对象
- 本地方法栈内引用的对象
- 方法区中类的静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用
- 等
除了这些固定的GC Roots集合之外,根据用户所选的垃圾收集器以及当前回收的内存区域不同,还可以有其它对象“临时性”地加入,共同构成完整的GC Roots集合。比如:分代收集和局部回收
在使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。
这点也是导致GC进行时必须“Stop The World”的一个重要原因
对象的finalization机制
- Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作。
不建议主动调用对象的finalize()方法,应该由垃圾回收机制调用:
- finalize()可能导致对象复活
- finalized()方法的执行时间是没有保障的,它完全由GC线程决定。极端情况下,若不发生GC,则finalize()方法将没有执行机会
- 一个糟糕的finalized()会严重影响GC的性能
- 由于finalize()方法的存在,虚拟机中对象一般处于三种可能的状态:
- 可触及的:从根节点开始,可以到达这个对象
- 可复活的:对象的所有引用都被释放,但是对象可能在finalize()中复活
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。因为finalize()只会调用一次
如果要判断一个对象是否可回收,至少要经历两次标记过程:
- 如果对象objA到GC Roots没有引用链,则进行第一次标记
- 进行筛选,判断此对象是否有必要执行finalized()方法
- 如果对象没有重写finalize()方法,或者finalize()已经被调用过了,则虚拟机视为“没有必要执行”,objA被判定为不可触及的
- 如果对象重写finalize()方法,且尚未执行过,则通过一个低优先级的线程触发其finalize()方法执行。
- finalize()方法是对象逃脱死亡的最后机会,稍后GC会对队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况下,finalized方法不会再次调用,对象会直接变成不可触及的状态。
清除阶段
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。
标记-清除算法
当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行第一项是标记、第二项则是清除。
- 标记:从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
- 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点:效率不高、进行GC的时候,需要停止整个应用进程、这种方式清理出来的内存是不连续的,产生内存碎片
何为清除?
所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载,判断垃圾的位置空间是否够,如果够,就存放。
复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:没有标记和清除过程,实现简单,运行高效;复制过去以后保证空间的连续性,不会出现碎片问题
缺点:此算法的缺点,就是需要两倍内存空间;对于G1这种分拆成为大量的region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
特别的当系统中存活对象很多,复制算法就不太理想。
应用场景
在新生代中,对象往往都是朝生夕死。回收性价比很高,可以用复制算法对新生代中的垃圾对象进行收集
标记-整理算法
第一阶段和标记清除算法一样,从根节点开始标记所有的被引用对象;第二阶段将所有的存活对象压缩到一段,按顺序排放。之后清理边界外所有的空间。
优点:消除了标记-清除算法当中,内存区域分散的缺点;消除了复制算法中,内存减半的高额代价
缺点:从效率上来说,标记-整理算法要低于复制算法;
分代收集算法
不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采用不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
- 年轻代
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低、回收频繁。这种情况复制算法的回收整理,速度最快,复制算法的效率只和当前存活对象的大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotSpot中的两个survivor的设计得到缓存
- 老年代
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般由标记清除算法和标记整理算法混合实现:标记阶段的开销和存活对象的数量成正比;清理阶段的开销与管理区域的大小呈正相关;整理阶段的开销与存活的数量成正比。