Android进阶笔记Android Jetpack 系列

重学Java系列-1. GC原理 & 垃圾回收算法

2022-01-04  本文已影响0人  今阳说

GC原理

  1. GC判断策略(例如引用计数法,可达性分析法)
  2. GC收集算法(标记清除法,标记清除整理法,标记复制清除法,分带法)
  3. GC收集器(例如Serial,Parallel,CMS,G1);

判断策略(哪些内存需要回收)

  1. 引用计数法:每个对象都有一个引用计数器,当对象被引用一次的时候,计数器+1,当对象引用失效的时候,计数值-1,实时性, 但不能解决循环引用的问题;
  2. 可达性分析法:从GC Root作为起点开始搜索,,那么整个连通图的对象都是存活的对象,对于GC Root无法到达的对象便成了垃圾回收的对象。
可以作为GCRoots的对象:
1. 虚拟机栈中引用的对象(栈帧中的局部变量区,也叫做局部变量表)。
2. 方法区中的类静态属性引用的对象。
3. 方法区中常量引用的对象。
4. 本地方法栈中JNI(Native方法)引用的对象。
四种引用
强引用置为null,会不会被回收?
基于可达性分析的内存回收原理
  1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。
  2. 对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。如下代码演示了一个对象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具体代码如下:
 public class GC {

     public static GC SAVE_HOOK = null;

     public static void main(String[] args) throws InterruptedException {
         // 新建对象,因为SAVE_HOOK指向这个对象,对象此时的状态是(reachable,unfinalized)
         SAVE_HOOK = new GC();
         //将SAVE_HOOK设置成null,此时刚才创建的对象就不可达了,因为没有句柄再指向它了,对象此时状态是(unreachable,unfinalized)
         SAVE_HOOK = null;
         //强制系统执行垃圾回收,系统发现刚才创建的对象处于unreachable状态,并检测到这个对象的类覆盖了finalize方法,因此把这个对象放入F-Queue队列,由低优先级线程执行它的finalize方法,此时对象的状态变成(unreachable, finalizable)或者是(finalizer-reachable,finalizable)
         System.gc();
         // sleep,目的是给低优先级线程从F-Queue队列取出对象并执行其finalize方法提供机会。在执行完对象的finalize方法中的super.finalize()时,对象的状态变成(unreachable,finalized)状态,但接下来在finalize方法中又执行了SAVE_HOOK = this;这句话,又有句柄指向这个对象了,对象又可达了。因此对象的状态又变成了(reachable, finalized)状态。
         Thread.sleep(500);
         // 这里楼主说对象处于(reachable,finalized)状态应该是合理的。对象的finalized方法被执行了,因此是finalized状态。又因为在finalize方法是执行了SAVE_HOOK=this这句话,本来是unreachable的对象,又变成reachable了。
         if (null != SAVE_HOOK) { //此时对象应该处于(reachable, finalized)状态
             // 这句话会输出,注意对象由unreachable,经过finalize复活了。
             System.out.println("Yes , I am still alive");
         } else {
             System.out.println("No , I am dead");
         }
         // 再一次将SAVE_HOOK放空,此时刚才复活的对象,状态变成(unreachable,finalized)
         SAVE_HOOK = null;
         // 再一次强制系统回收垃圾,此时系统发现对象不可达,虽然覆盖了finalize方法,但已经执行过了,因此直接回收。
         System.gc();
         // 为系统回收垃圾提供机会
         Thread.sleep(500);
         if (null != SAVE_HOOK) {
             // 这句话不会输出,因为对象已经彻底消失了。
             System.out.println("Yes , I am still alive");
         } else {
             System.out.println("No , I am dead");
         }
     }

     @Override
     protected void finalize() throws Throwable {
         super.finalize();
         System.out.println("execute method finalize()");
        // 这句话让对象的状态由unreachable变成reachable,就是对象复活
         SAVE_HOOK = this;
     }
 }
方法区的垃圾回收
如何判断废弃常量?
如何判断无用的类?
  1. 该类的所有实例都已经被回收,即Java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

  1. 标记清除法: 标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象, 效率不高,会产生大量不连续的碎片空间,可能导致为较大对象分配空间时,找不到足够的连续内存,提前触发GC;
  2. 复制清除法: 将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉,缺点是可用内存缩小了一半;(比较适合对象存活率比较低的场景新生代))
  3. 标记整理法:过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。(比较适合对象存活率比较高的场景(老年代))
  4. 分代收集算法: 不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块

垃圾收集器

新生代收集器
1. 串行GC(serial GC)
2. 并行GC(ParNew)
3. 并行回收GC(parallel scavenge)
老年代收集器
1. 串行GC(serial old)
2. 并行GC(parallel old)
3. 并发GC(CMS)
1. 初始标记,标记GCRoots能直接关联到的对象,时间很短。
2. 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
3. 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。
4. 并发清除,回收内存空间,时间很长。
1. 对CPU资源非常敏感,可能会导致应用程序变慢,吞吐率下降;
2. 无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,
这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用;
3. 由于采用的标记 - 清除算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次Full GC;
G1收集器
1. 并行和并发:使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
2. 分代收集:独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
3. 空间整合:基于标记 - 整理算法,无内存碎片产生。
4. 可预测的停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
它的内存划分怎么样的?
JAVA自动内存管理
  1. 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。
  2. 大对象直接进入老年代。如很长的字符串以及数组。
  3. 长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
  4. 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

上一篇 下一篇

猜你喜欢

热点阅读