目录介绍

  • 01.GC回收机制
    • 1.1 先思考三个问题
    • 1.2 内存垃圾回收机制
    • 1.3 关于GC概念介绍
    • 1.4 如何监听GC过程
    • 1.5 GC过程与对象的引用类型关系
  • 02.GC回收什么
    • 2.1 理解内存分配
    • 2.2 哪些对象需要回收
  • 03.检测垃圾的算法
    • 3.1 引用计数法
    • 3.2 处理垃圾的算法
  • 04.处理垃圾的算法
    • 4.1 标记-清除(Mark-sweep)
    • 4.2 复制(Copying)
    • 4.3 标记-整理(Mark-Compact)
    • 4.4 分代收集算法(当今最常用的方法)
  • 05.如何对对象划分
    • 5.1 将对象按其生命周期划分
    • 5.2 年轻代
    • 5.3 年老代
  • 06.GC中对象的六种可触及状态
  • 07.垃圾回收器的类型

01.GC回收机制

1.1 先思考三个问题

  • 在了解回收机制之前,必须要了解内存
    • 先思考三个问题
      • JVM是怎么分配内存的
      • 识别哪些内存是垃圾需要回收
      • 最后才是用什么方式回收
    • 栈的内存管理是顺序分配的,而且定长,不存在内存回收问题;而堆 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题

1.2 内存垃圾回收机制

  • 是从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收
  • GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

1.3 关于GC概念介绍

  • 有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC
  • 通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。
    通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间

1.4 如何监听GC过程

  • 系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下
    D/dalvikvm: , ,
  • 第一部分GC_Reason,这个是触发这次GC操作的
1
2
3
4
5
原因,一般情况下一共有以下几种触发GC操作的原因:
- GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。
- GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。
- GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。
- GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。
  • 第二部分Amount_freed,表示系统通过这次GC操作释放了多少内存
  • 第三部分Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)
  • 第四部分Pause_time表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,Android在2.3的版本当中进行过一次优化,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。虽说这个阻塞的过程并不会很长,也就是几百毫秒,但是用户在使用我们的程序时还是有可能会感觉到略微的卡顿。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了

1.5 GC过程与对象的引用类型关系

Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference

  • 软引用和弱引用
  • 在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术
  • 软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。
  • 内存泄漏的原因:堆内存中的长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因

02.GC回收什么

2.1 理解内存分配

  • Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。
  • java一般内存申请有两种:静态内存和动态内存 。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。

2.2 哪些对象需要回收

03.检测垃圾的算法

  • 垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?一般有以下几种方法:引用计数法,可达性分析算法。

3.1 引用计数法

  • a.引用计数法:
    • 给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收。
    • 引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。

3.2 可达性分析算法

  • 可达性分析算法
    • 以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常量池中引用的对象,本地方法中引用的对象等

04.处理垃圾的算法

4.1 标记-清除(Mark-sweep)

* 算法和名字一样,分为两个阶段:标记和清除。标记所有需要回收的对象,然后统一回收。这是最基础的算法,后续的收集算法都是基于这个算法扩展的。
* 不足:效率低;标记清除之后会产生大量碎片。

4.2 复制(Copying)

- 此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

4.3 标记-整理(Mark-Compact)

- 此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

4.4 分代收集算法(当今最常用的方法)

* 这是当前商业虚拟机常用的垃圾收集算法。分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
* 为什么要运用分代垃圾回收策略?在java程序运行的过程中,会产生大量的对象,因每个对象所能承担的职责不同所具有的功能不同所以也有着不一样的生命周期,有的对象生命周期较长,比如Http请求中的Session对象,线程,Socket连接等;有的对象生命周期较短,比如String对象,由于其不变类的特性,有的在使用一次后即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对会很长,而且对于存活时间较长的对象进行的扫描工作等都是徒劳。因此就需要引入分治的思想,所谓分治的思想就是因地制宜,将对象进行代的划分,把不同生命周期的对象放在不同的代上使用不同的垃圾回收方式。

05.如何对对象划分

5.1 如何对对象划分

  • 将对象按其生命周期划分
    • 年轻代(Young Generation)
    • 年老代(Old Generation)
    • 持久代(Permanent Generation)
    • 其中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。

5.2 年轻代

  • 年轻代
    • 是所有新对象产生的地方。
    • 年轻代被分为3个部分——Enden区和两个Survivor区(From和to)
    • 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。

5.3 年老代

  • 年老代
    • 在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。这时候MajsorGC会清理些老年代垃圾,通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。
    • 持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响。

06.GC中对象的六种可触及状态

  • 1.强可触及:对象可以从根结点不通过任何引用对象搜索到
  • 2.软可触及:对象不是强可触及的,但是可以从根结点开始通过一个或多个(未被清除的)软引用对象触及
  • 3.弱可触及:对象既不是强可触及也不是软可触及的,但是从根结点开始
  • 4.可复活的:对象既不是强可触及、软可触及,也不是弱可触及,但是仍然可能通过执行某些终结方法复活到这几种状态之一
  • 5.影子可触及:不上以上任何可触及状态,也不能通过终结方法复活,并且它可以从根结点开始通过一个或多个影子引用对象触及(影子引用不会被垃圾收集器清除,由程序明确地清除)
  • 6不可触及:就是已经准备回收的状态

07.垃圾回收器的类型

  • Java 提供多种类型的垃圾回收器。 JVM 中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。
    • Serial 收集器:新生代收集器,使用复制算法,使用一个线程进行 GC,串行,其它工作线程暂停。
    • ParNew 收集器:新生代收集器,使用复制算法,Serial 收集器的多线程版,用多个线程进行 GC,并行,其它工作线程暂停。 使用 -XX:+UseParNewGC 开关来控制使用 ParNew+Serial Old 收集器组合收集内存;使用 -XX:ParallelGCThreads 来设置执行内存回收的线程数。
    • Parallel Scavenge 收集器:吞吐量优先的垃圾回收器,作用在新生代,使用复制算法,关注 CPU 吞吐量,即运行用户代码的时间/总时间。 使用 -XX:+UseParallelGC 开关控制使用 Parallel Scavenge+Serial Old 收集器组合回收垃圾。
    • Serial Old 收集器:老年代收集器,单线程收集器,串行,使用标记整理算法,使用单线程进行GC,其它工作线程暂停。
    • Parallel Old 收集器:吞吐量优先的垃圾回收器,作用在老年代,多线程,并行,多线程机制与 Parallel Scavenge 差不错,使用标记整理算法,在 Parallel Old 执行时,仍然需要暂停其它线程。
    • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。 使用 -XX:+UseConcMarkSweepGC 进行 ParNew+CMS+Serial Old 进行内存回收,优先使用 ParNew+CMS,当用户线程内存不足时,采用备用方案 Serial Old 收集。