Java&Android 基础知识梳理(4) - 垃圾收集器与内存分配策略

一、概述

GC需要考虑的三个问题:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

在分析内存区域的时候,我们把Java运行时数据区分为两个部分:

  • 程序计数器、虚拟机栈、本地方法栈:每个栈帧中分配多少内存在类结构确定下来就已知,因此这些区域的内存分配和回收具备确定性,方法结束或线程结束时,内存就跟着被回收了。
  • Java堆、方法区:由于一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,只有在程序处于运行期间才能知道会创建哪些对象,因此这些区域的内存分配和回收是动态的。

二、如何判断哪些是“存活”的实例

2.1 引用的分类

引用的定义:如果reference类型的数据中存储的数值代表的另外一块内存的起始地址,就称这块内存代表引用。
引用的分类:

  • 强引用(Object a = new Object()):只要强引用存在,垃圾回收器永远不会回收掉被引用的对象。
  • 软引用(SoftReference):有用但并非必须,在系统将要发生OOM异常之前,将会把这些对象列进回收范围中进行第二次回收。
  • 弱引用(WeakReference):非必须对象,被弱引用的对象只能生存到下一次垃圾收集发生前。
  • 虚引用(PhantomReference):不会对生存时间产生影响,也无法通过虚引用来取得一个对象实例,设置虚引用的唯一目的就是能在这个对象被垃圾回收器回收时收到一个系统通知。

2.2 引用计数法

给对象添加一个引用计数器,当有一个地方引用它时就加一,引用失效时就减一,当计数器的值为零时表示它不可用。
但是它无法解决相互循环引用问题

2.3 可达性分析

通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链时,表示这个对象不可用,GC Roots的类型有:

  • 虚拟机栈中的局部变量表中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

2.4 finalize方法对于内存回收的影响

当某个对象在经过可达性分析后,发现它到GC Roots没有任何引用链时,那么它会被第一次标记,并进行第一次筛选,筛选的结果有两种情况:

  • 没有覆盖finalize()方法或者虚拟机已经调用过它的finalize()方法:直接回收。
  • 其它情况:把这个对象放置在一个F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行这个对象的finalize()方法,如对象要在finalize方法中拯救自己,只要重新与引用链的某个变量关联即可,那么在第二次标记时它将被移出“即将回收”的集合,否则它将被回收。

这种方法代价高昂,不确定性大,无法保证各个对象的调用顺序,因此可以忘记这个方法的存在。

三、方法区的回收

对于方法区(HotSpot中的永久代)主要回收两部分内容:废弃常量和无用的类。

  • 废弃常量
    以常量池中字面量的回收为例,如果一个字符串abc被放入了常量池中,但是没有任何一个String对象引用它,那么就会被清理出常量池,常量池中其它类(接口)、方法、字段的符号引用也类似。

  • 类,同时满足三个条件:

    • 该类的所有实例已经被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

四、垃圾收集算法基础

4.1 标记 - 清除算法

  • 概念
    首先标记出所有需要回收的对象,在标记完成后统一进行回收。
  • 缺点:
    • 标记和清除两个过程效率不高。
    • 产生内存碎片,导致需要分配较大对象时,无法找到足够的连续内存而需要触发一次GC操作。

4.2 复制算法

  • 概念
    将可用内存划分为大小相等的两块,每次只使用其中的一块,当一块内存用完了。则触发一次GC操作,将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

  • 缺点
    将内存缩小为了原来的一半。

  • 现在商业虚拟机采用这种算法的改良版来实现新生代的回收
    它把内存按8:1:1分为Eden/survivor0/survivor1三块:
    需要分配内存时,首先尝试在Eden区分配,如果Eden区无法分配,那么尝试把活着的对象放到survivor0中去:

  • 如果survivor0可以放入,那么放入之后清除Eden区。

  • 如果survivor0不可以放入,那么尝试把Edensurvivor0的存活对象放到survivor1中:

    • 如果survivor1可以放入,那么放入survivor1之后清除Edensurvivor0,之后再把survivor1中的对象复制到survivor0中,保持survivor1一直为空。
    • 如果survivor1不可以放入,那么直接把它们放入到老年代中,并清除Edensurvivor0,这个过程也称为分配担保
  • 适用情况
    由于复制算法在对象成活率较高时,需要较多的复制操作,效率会变低,所以在老年代中不能采用该算法。

4.3 标记 - 整理算法

  • 概念
    和标记 - 清除算法类似,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  • 优点
    解决了标记- 清除算法导致的内存碎片问题在存活率较高时复制算法效率低的问题

4.4 分代收集算法

当前商业虚拟机采用的方式,根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代:

  • 新生代:每次垃圾收集时只有少量存活,选用复制算法的改良版,也就是上面说到的Eden/survivor0/survivor1的分配方式。
  • 老年代:对象存活率较高,且没有分配担保,必须用标记 - 清除或标记 - 整理算法来实现。

五、Minor GCMajor GC/Full GC

  • Minor GC:发生在新生代的垃圾回收动作,非常频繁,回收速度也较快,采用的垃圾收集器有SerialParNewParallel Scavenge
  • Major GC/Full GC:发生在老年代的GC,经常伴随至少一次的Minor GCMajor GC的速度一般会比Minor GC慢十倍以上,采用的垃圾收集器有CMSSerial OldParallel Old

六、对象分配的原则

  • 对象优先在Eden区分配
    Eden区没有足够空间,触发一次Minor GC

  • 大对象直接进入老年代
    例如很长的字符串以及数组,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

  • 长期存活的对象将进入老年代
    如果Eden区出生并进过第一次Minor GC后,仍然存活,并且被成功复制到survivor区中,那么对象年龄变为一,当对象在survivor中每熬过一次Minor GC,年龄就增加一,当年龄增加到一定程度,就会晋升到老年代中。

  • 动态对象年龄绑定
    如果survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代,无须到达要求的年龄。

  • 空间分配担保
    在发生Minor GC前,检查老年代最大可用连续空间是否大于新生代所有对象总空间:

  • 大于,那么操作是安全的,不对老年代进行Full GC

  • 小于,检查HandlePromotionFailure设置值是否允许失败:

    • 允许:检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小:
      • 大于:不对老年代进行Full GC。在这之后,因为有可能出现某次存活对象激增的情况,这种属于冒险行为,如果出现了担保失败(也就是Edensurvivor0的存活对象既无法放入survivor1,也无法放入老年代的连续空间中),那么会在失败之后对老年代进行Full GC
      • 小于:先对老年代进行一次Full GC
    • 不允许:先对老年代执行一次Full GC

你可能感兴趣的:(Java&Android 基础知识梳理(4) - 垃圾收集器与内存分配策略)