一、如何判断一个对象是死亡的?
1.引用计数
在一个对象被引用时加一,被去除引用时减一,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
优点:
引用计数收集器执行简单,效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
缺点:
①难以检测出对象之间的循环引用。
②增加了程序执行的开销。
2.可达对象分析
从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾。 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达,可被回收。
可以作为GC Roots的对象:
①虚拟机栈中局部变量引用的对象;
②本地方法栈中JNI(即一般说的Native方法)所引用的对象;
③方法区中静态变量和常量所引用的对象;
如图,对象实例1、2、4、6具有GC Roots可达性,也就是存活对象,不能被GC回收。 而对象实例3、5之间虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,就是GC需要回收的垃圾对象。
二、垃圾回收算法
如果一个对象不可能再被引用,那么这个对象就是垃圾,应该回收
1.标记清除算法
两个主要阶段:标记阶段、清除阶段。
标记阶段:垃圾收集器从一组根对象开始,遍历所有对象,通过引用计数或GC Root 引用分析标记所有存活的对象。
清除阶段:垃圾收集器回收所有未被标记的对象所占用的内存。
优点:
①实现简单,不需要对象进行移动
②对于新生代对象回收效果较好
缺点:
①效率问题:如果堆中包含大量对象,且大部分需要被回收,标记和清除效率会随着对象数量的增加而降低
②内存碎片:标记和清除过程会产生大量不连续的内存碎片,这些碎片会导致在分配大对象时需要更多的垃圾收集,增加了GC的频率。
2.标记整理算法
两个主要阶段:标记阶段、整理阶段。
标记阶段:遍历所有对象,通过GC Root 引用分析标记所有存活的对象。
整理阶段:将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间。
优点:
解决了标记和清理算法存在的内存碎片问题。
缺点:
需要进行局部对象移动,一定程度上降低了效率。
3.复制算法
核心思想是将原有的内存空间分为两块,每次只使用一块。在GC时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,再交换两个内存块的角色,完成垃圾回收。
优点:
按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:
①可用的内存大小缩小为原来的一半
②对于存活率高的对象会频繁进行复制
4.分代收集算法
JDK8堆内存一般是划分为年轻代和老年代,默认比例通常是1:2,不同年代根据自身特性采用不同的垃圾收集算法。
①对于老年代的对象,因为存活率高,没有额外的内存空间。因此适合采用标记清理算法或者标记整理算法进行回收。而如果在老年代中使用复制算法,在极端情况下,老年代对象的存活率可以达到100%,那么我们就需要复制这么多个对象到另外一个内存区域,这个工作量是非常庞大的。
②对于新生代,每次GC时都有大量的对象死亡,只有少量对象存活。比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。
在实际的 JVM 新生代划分中,并不是采用等分为两块内存的形式。而是划分为一块较大的Eden空间和两块较小的Survivor空间(from 区域、to 区域),其大小占比是8:1:1。
新对象会首先分配在Eden区,经历一次GC,Eden中的存活对象会被移动到from survivor区域,Eden区被清空;等Eden区满了,就再触发一次GC,Eden和from survivor中的存活对象又会被复制送入to survivor区域(这个过程非常重要,它保证了to survivor中来自Eden和from survivor两部分的存活对象占用连续的内存空间,避免了碎片化的发生),接着Eden和from survivor区域清空,to survivor与from survivor交换角色,如此周而复始。
通过这种方式,内存的空间利用率达到了90%,只有10%的空间是浪费掉了。而如果通过均分为两块内存,其内存利用率只有 50%,两者利用率相差了将近一倍。