一、对象是否死亡?
1、引用计数法
给对象添加一个引用计数器,每当对象被引用一次时,计数器+1,引用失效时,计数器-1.任何时刻计数器为0的对象就是不可能再被使用的,也就是对象已经死亡。但是jvm没有选择这个算法来管理内存,因为它不能解决对象之间相互循环引用的问题。
2、可达性分析算法
将“GC roots”(类静态属性引用的对象或者常量引用的对象等)作为起始点,向下搜索(搜索所走过的路径称为引用链),当一个对象到GC roots没有任何引用相连(也就是不可达)时,则说明此对象是不可用的。
3、对象死透了吗?
在可达性分析中不可达的对象不是非死不可的,他们还有两次免刑的机会。(两次标记过程)
第一次筛选此对象是否有必要执行finalize()方法,如果没有必要执行,就会放过他们。
如果有必要执行,就会将他们放到一个F-Queue队列中,然后由虚拟机自动建立一个finalize方法去执行他们。
在执行finalize时,对象还可以自救一次,只要重新与引用链上的任何一个对象建立关联即可。否则它将会真的被回收。
二、垃圾收集算法
1、标记 – 清除算法
首先将要回收的对象进行标记,然后将标记的对象统一回收。(后面两个算法的基础)
缺点:
- 效率问题,标记和清除的效率都不高
- 空间问题,标记清除之后会产生大量不连续的空间碎片,导致后来分配大对象时,找不到足够的连续内存而不得不提前触发一次GC
2、复制算法
将内存分成两块,当一块的内存快用完了,就将里面还存活的对象复制到另一块内存中,并把这块内存全部清理,这样就避免了有内存碎片的情况。(新生代使用的算法)
但由于在新生代中,绝大部分的对象都是“朝生夕死”的,所以JVM把内存分为了一个比较大的Eden空间和两个较小的survivor空间。默认Eden:survivor = 8:1 。当回收时,将Eden和survivor0中还存活的对象复制到survivor1中。清理掉Eden和survivor0中的内存空间。当survivor中空间不够用时,就直接分配担保至老年代内存中了。(当对象的年龄到达指定年龄时,也会进入老年代中)
为什么需要两个survivor呢?
因为从Eden中送到survivor中的对象不会送走,会待在那里,如果发现survivor空间不够,也会进行GC,这里就是直接使用的标记-清除算法,从而也会产生大量的内存碎片,所以需要两个survivor进行来回复制,以保证有一个survivor是空的,而不会有大量的内存碎片啦。
3、标记 – 整理算法
与标记-清除类似,不同的是,在标记后,将对象进行整理,让所有存活的对象移动到一端,把其余的空间都清理掉,这样也不会留下空间碎片了。(老年代使用的算法)
三、Hotspot的算法实现
1、枚举根节点
GC停顿:为了保证一致性,必须在分析期间将整个执行系统冻结,直到分析结束,否则无法保证准确性。
在Hotspot的枚举根节点时,使用了一个OopMap数据结构来达到可以直接得知哪些地方存放着对象引用。在类加载完成的时候,Hotspot就把对象内扫描偏移量上是什么类型的数据计算出来,在JIT编译时,也会在特定的位置记录下栈和寄存器中哪些位置是引用。
2、安全点
程序执行时只在特定的地方才停下来开始GC,这些地方称为安全点。以程序“是否具有让程序长时间执行的特征”为标准来选定safepoint。一般这些点就是具有方法调用,循环跳转等功能的指令。
Hotspot采用主动式中断来暂停线程。每个线程去轮询放在安全点上(或者创建对象需要分配内存的地方)的标志,发现中断为真时就把自己中断挂起。
3、安全区域
当线程处于sleep或者blocked状态(也就是没有分配CPU的状态)时,如果它响应了中断,然后中断结束后又被分配内存,这显然是不合理的。所以引出了一个安全区域来解决。
在安全区域中的线程,不会被GC的标志所影响,将那些没有分配CPU的线程放在安全区域,也就不会因为GC而导致CPU分配错误,但是已经进入安全区域的线程,比如等待整个GC结束后才能离开。