Java常见的GC Root

我们知道Java 进行GC的时候会从GC root进行可达性判断,常见的GC Root有如下:

  1. 通过System Class Loader或者Boot Class Loader加载的class对象,通过自定义类加载器加载的class不一定是GC Root
  2. 处于激活状态的线程
  3. 栈中的对象
  4. JNI栈中的对象
  5. JNI中的全局对象
  6. 正在被用于同步的各种锁对象
  7. JVM自身持有的对象,比如系统类加载器等。

在调查内存泄漏原因的时候可以根据GC Root来推导

了解了这些,我们来看一下常用的GC算法

  1. 标记回收算法
    从GC root进行遍历,把可达对象都标记,剩下那些不可达的进行回收,这种方式需要中断其他线程,并且可能产生内存碎片

  2. 复制算法
    把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了。

  3. 标记压缩算法
    和标记回收差不多,但是在回收的时候会对可达对象进行整理,将其压缩到内存的一段,避免内存碎片

  4. 分代算法
    将内存区域分代,对不同的代使用不同的回收算法,通常分为新生代,老年代,和永久带。
    新生代一般包含三个区域,Eden区和两个Survivor区,新生代一般采用复制算法

老年代一般采用标记压缩算法

永久带一般是方法区,JVM规范并没有强制要求说必须要回收这个区域。

在Dalvik虚拟机中的堆其实是分为了两个,一个是Zygote堆,一个是Active堆,Zygote堆中主要是预加载的各种资源和对象,这个堆很少被写。我们分配的堆内存一般都是在Activie堆进行分配的。

在说到Java堆内存的时候,我们必须要对这几个指标很了解:

  1. Xms: starting size,在Dalvik虚拟机启动的时候会预先分配的一块大小的堆内存
  2. Xmx: 最大堆内存,超过这个值就会OOM
  3. XX: 不受控情况下最大的堆内存,就是我们在AndroidManifest中申请largeHeap属性的时候所允许的最大堆内存。

在GC中还有一个非常重要的指标,叫做堆目标利用率U,就是已分配的堆大小和堆总共的大小的比值,在GC的时候,垃圾回收器会尽可能把利用率往目标利用率靠近。

如果我们手动生成一些几百K的对象,来增大堆内存的大小,然后想着这样不用每次都需要对堆扩容,可以减少GC。但是这样是不存在的,反而会引起频繁的CONCURRENT GC,因为在分配这些对象的空间的时候会引发GC,然后这些对象的很快被使用完销毁,仍然会导致GC,因为JVM会把利用率往目标利用率上靠近,堆大小仍然会缩小到之前的大小。

看一下GC的类型

  1. GC_FOR_MALLOC:在堆上为对象分配内存的时候空间不足
  2. GC_CONCURRENT:JVM自动触发的gc,一般是在堆内存快要耗尽的时候触发
  3. GC_EXPLICIT:程序猿主动调用System.gc触发
  4. GC_BEFORE_OOM: 在OOM之前最后的努力了

看一下分配内存情况下GC的三次流程:

  1. 首先不扩容堆大小的情况下分配内存,如果成功则成功(这是轻量级操作),如果失败,则进行第一次GC
  2. 第一次GC会打印出GC_FOR_MALLOC,这次GC不会回收软引用对象
  3. 第一次GC完成之后会再次尝试分配内存,如果还是分配失败,则进行第二次GC
  4. 第二次GC会扩容堆内存,然后在尝试分配内存。
  5. 再次尝试分配内存,如果分配失败就要进行第三次,也就是最后一次GC了
  6. 最后一次GC会回收软引用所指向的对象,如果还不行的话,这次会打印出GC_BEFORE_OOM,如果还是失败,则会抛出OOM异常了。

当通过GC成功为一个对象分配了内存之后,JVM会判断一下当前的剩余内存是否达到了GC_CONCURRENT的阈值,如果达到了,JVM会自动执行一下并行GC,打印出GC_CONCURRENT. 那这个阈值是怎么来的呢? 会根据我们设置的 堆目标利用率来计算。

Android的Dalvik虚拟机大部分是采用的 标记清理和拷贝算法,几乎没有采用分代算法的。我们知道 标记清理算法 很容易产生内存碎片。 所以我们在写代码的时候要尽可能避免产生大量的小对象。 小对象能复用的时候尽量去复用。

从5.0以后,Android的虚拟机从Dalvik切换到了ART,我们需要了解一下ART中对于GC的处理。

ART将堆内存分为了四个部分(Dalvik分为了两个部分):Image Space, Zygote Space, Allocation Space, Large Object Space。

Image Space(主要是Android自己的一些需要预加载的类)和Zygote Space合起来相当于Dalvik中的Zygote堆。

Allocation Space相当于Dalvik的Activie堆。

Large Object Space是一些离散地址的集合,主要是用来分配大对象。Large Object Space中分配的都是大于12KB的数组对象。

ART GC和Dalvik GC的区别

ART GC和Dalvik GC在分配内存时候的触发逻辑是一样的。他们主要的区别在算法的选择上。

Dalvik GC一般只会有一种GC算法,但是ART GC会有多种GC算法,并且会在不同的时机选择不同的算法。

下面就来讲一下ART GC选择不同算法的时机:

  1. 非并发GC流程:
    1.1 通过InitializePhase初始化GC
    1.2 挂起所有ART运行时线程
    1.3 通过MarkingPhase方法执行标记
    1.4 通过ReclaimPhase方法执行清理
    1.5 恢复所有被挂起的线程
    1.6 调用FinishPhase结束GC

  2. 并行GC流程(实际上并非真正的并行)
    2.1 通过InitializePhase初始化GC
    2.2 获取堆上的锁
    2.3 调用MarkingPhase方法并行标记(此处是真正并行的
    2.4 释放堆上的锁。

2.5 挂其所有的ART运行时线程
2.6 调用HandleDirtyObjectPhase方法处理并行标记阶段被修改的对象(此刻并没有真正回收)
2.7 恢复所有ART进程

2.8 不断重复上面三部,直到所有被标记的对象都被处理完成

2.9 接下来就是调用ReclaimPhase进行回收,然后调用FinishPhase进行结束。

所以无论是否并发,都存在挂起所有ART进程的情况,不同的是并发的时候,单次挂起ART进程的时间会更短(如果并发清理只进行了一次循环,那么就只需要挂起单次,时间消耗会更少。如果并发清理执行了多次的循环,那么时间消耗可能会比非并发要多)。

从上面可以看出,ART并行回收在Mark阶段,Reclaim阶段和Finish阶段是不需要挂起线程的,只有在HandleDirtyObjectPhase阶段才需要挂起线程。

ART GC相对于Dalvik GC不仅是在算法(包括引入更多算法和在同一个算法上做优化)上进行了优化,减少了暂停时间,而且单独开辟了Large Object Space对大对象进行处理。而且ART GC在处理后台应用的时候会对后台应用的内存进行 标记压缩GC,从而减少内存碎片。

下面在稍微说一下分代收集,这是大部分JVM上采用的垃圾收集机制(Android并没有采用)。

分代收集的概念是指在不同的代上使用不同的垃圾回收算法,一般来讲 在新生代上使用复制算法,在老年代上使用标记清理或者标记压缩算法。在永久代上使用标记压缩算法(JVM规范并没有要求要对永久代进行回收)。

垃圾收集器是GC的具体实现,不同的垃圾收集器针对的代也是不一样的,简单介绍一下。

Serial收集器:主要针对针对新生代,什么都不配置的话JVM默认的收集器,采用复制算法
ParNew收集器:主要 针对新生代, Serial的多线程版本。

Parallel Scanvenge: 主要针对新生代,主要用在服务器上,这种垃圾收集器也是采用复制算法,但是关注CPU吞吐量,强调CPU运行垃圾回收的时间和CPU执行用户代码的时间的比例。

Serial Old收集器:主要针对老年代,采用标记清理和标记压缩算法,单线程版本

Parallel Old收集器:主要针对老年代,多线程版本。

CMS(Concurrent Mark Sweep):大名鼎鼎的并行标记清理收集器,主要针对老年代,多线程,主要关注停顿时间,这个在嵌入式设备上非常有名,因为用户对于停顿时间很敏感。

CMS的原理是三次标记一次清除。
第一次标记先找到应用中所有的GC root,这次标记需要暂停用户线程。但是时间非常非常短。
第二次标记不需要暂停用户线程,根据第一次标记的结果去寻找不可达对象。
第三次标记也需要暂停用户线程,因为在第二次标记的过程中,GC root可能发生了变化,这个时候就要在把变化的重新标记一下。

三次标记完成之后就会执行清理过程。

由于在第二次并行标记的时候用户线程仍然在执行,所以需要预留足够的内存给用户线程使用,所以CMS并不会在老年代满了之后才执行Full GC, 一般是在老年代使用了一大半的时候就会执行一次Full GC.

同样的,CMS由于采用标记清理算法,也会导致内存碎片的产生。

并发清理是指:垃圾清理的过程中用户线程还可以继续工作,所以CMS是并发收集器。

并行清理是指:有多个垃圾回收线程在执行清理,所以Parallel收集器是并行收集器。

    原文作者:董成鹏
    原文地址: https://www.jianshu.com/p/dcfe84c50811
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞