基本概念:

      在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收

什么对象可以看作垃圾?

没有被任何其他对象引用的对象

1. 如何判断对象是否是垃圾

  • 引用计数算法
    判断对象的引用数量,每个对象实例都有一个引用计数器,当它被引用时+1,完成引用时-1.任何时刻计数器数值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。

优点
执行效率高,程序执行受影响较小。

缺点
无法解决循环引用的问题,会导致内存泄漏(很致命,已经被摒弃)

  • 可达性算法

为了解决引用计数法的循环引用问题,java使用了可达性分析的方法。

所谓GC ROOT或者说Tracing GC的“根集合”就是一组比较活跃的引用。

基本思路就是通过一系列“GC Roots”的对象作为起始点,从这个被称为GC Roots 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被便利到的对象就被判定为存活;没有被便利到的就被判定为死亡

可以作为gc root的对象

  • 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中应用的对象。
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(native方法)引用的对象

2. 垃圾回收算法


JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

因此GC按照回收的区域又分了两种类型,一种是普通GC(MinorGC),一种时全局GC(FullGC)

  • 普通GC:只针对新生代区域的GC
  • 全局GC:针对年老代的GC,偶尔伴随对新生代的GC以及堆永久代的GC。

2.1 复制算法:MinorGC(普通GC)

新生代使用的MinorGC,这种GC算法采用的是复制算法(Copying),频繁使用

复制–>清空–>互换

2.1.1 原理

MinorGC会把Eden中的所有或的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old Generation中,也即一旦收集后,Eden区就变成空的了。

当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次MinorGC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即to区域又足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块Survivor区域(即to区)中,然后清理所有使用过的Eden以及Survivor区域(即from区),并且讲这些对象的年龄设置为1,以后对象在Survivor区没熬过一次MinorGC,就将对象的年龄+1,当对象的年龄达到某个值时(默认15,通过-XX:MaxTenuringThreshold来设定参数),这些对象就会成为老年代。

==-XX:MaxTenuringThreshold设置对象在新生代中存活的次数==

2.1.2 解释

HotSpot JVM把年轻代分为了三部分:1个Eden区和两个Survivor区,默认比例是8:1:1,一般情况下,新创建的对象都会被分配到Eden区,这些对象经过第一次的MinorGC后,如果仍然存活,将会被移到Survivor区。对象Survivor区中每熬过一次MinorGC,年龄就增加一岁,当他的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

==复制要交换,谁空谁是to==

2.1.3 劣势

复制算法弥补了标记清除算法中,内存布局混乱的缺点。

  1. 浪费了一般的内存,太要命了
  2. 如果对象的存活率很高,我们可以极端一点,假设是100%存活率,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度是,将会变的不可忽视。所以从以上描述不难看出,复制算法想要使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要客服50%的内存的浪费

2.2 标记清除(Mark-Sweep)

2.2.1 原理

  1. 标记(mark)

    从根集合开始扫描,对存活的对象进行标记

  2. 清除(Sweep)

    扫描整个内存空间,回收未被标记的对象,使用free-list记录可以区域。

2.2.2 劣势

  1. 效率低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲
  2. 清理出来的空闲内存不是连续的,我们的死亡对象都是随机的出现在内存的各个角落,限制把他们清除之后,内存的布局自然会乱七八糟,而为了应付这一点,JVM不得不维持一个内存的空闲列表,这又是一种开销,而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

2.3 标记整理(Mark-Compact)

2.3.1 原理

  1. 标记

    与标记-清除一样

  2. 压缩整理

    再次扫描,并往一段滑动存活对象

2.3.2 劣势

效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上说,效率要低于复制算法

2.4 小总结

  • 内存效率:复制算法>标记清除算法>标记整理算法
  • 内存整齐度:复制算法=标记整理算法>标记清除算法
  • 内存利用率:标记整理算法=标记清除算法>复制算法

分代收集算法

引用计数法:

  • 缺点:每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗
  • 较难处理循环引用