Android性能优化二:内存优化之GC

一、概述

内存管理, 一直是编程中的一个大的问题. 在较老的语言中, 例如C++语言中, 内存管理是显式的, 也就是说使用者自己申请内存使用, 自己释放内存. 这就是为什么C++语言中除了构造函数, 还有析构函数. 我们在创建对象的时候调用构造函数创建, 系统会在对象结束其作用域的时候调用析构函数, 我们需要做的就是在析构函数中释放掉我们申请的相关资源, 以便释放内存地址.

显然, 这种显式的由编程人员自己控制释放内存的方式很容易出问题, 忘了, 漏了, 都可能导致内存问题. 也不符合程序员要懒的特征。

故而, Java语言中引入了自动内存管理的机制, 也就是GC。 大部分的现代面向对象语言, 也都是采用自动内存管理机制。

内存自动管理回收机制可以解决大部分, 但不是所有的内存问题, 这也是为什么我们要讨论内存泄露.

二、何为GC

GC 是 garbage collection 的缩写, 垃圾回收的意思. 也可以是 Garbage Collector, 也就是垃圾回收器。

1.垃圾回收器的职责

垃圾回收器有三大职责:

  • 分配内存;
  • 确保任何被引用的对象保留在内存中;
  • 回收不能通过引用关系找到的对象的内存.

2.垃圾回收的一般流程

这里写图片描述

三、一些概念

1.Heap和Stack

简单说下:

  • Heap内存是指java运行环境用来分配给对象和JRE类的内存. 是应用的内存空间.
  • Stack内存是相对于线程Thread而言的, 它保存线程中方法中短期存在的变量值和对Heap中对象的引用等.
  • Stack内存, 顾名思义, 是类Stack方式, 总是后进先出(LIFO)的.
  • 我们通常说的GC的针对Heap内存的. 因为Stack内存相当于是随用随销的.
    这里写图片描述

2.GC Root

所谓GC Root我们可以理解为是一个Heap内存之外的对象, 通常包括但不仅限于如下几种:

  • System Class 系统Class Loader加载的类. 例如java运行环境中rt.jar中类, 比如java.util.* package中的类.
  • Thread 运行中的线程
  • JNI 中的本地/全局变量, 用户自定义的JNI代码或是JVM内部的.
  • Busy Monitor 任何调用了wait()或notify()方法, 或是同步化的(synchronized)的东西. 可以理解为同步监控器.
  • Java本地实例, 还在运行的Thread的stack中的方法创建的对象.

3.活对象/垃圾

如果这个对象是引用可达的, 则称之为活的(live), 反之, 如果这个对象引用不可达, 则称之为死的(dead), 也可以称之为垃圾(garbage).
这个引用可达与不可达就是相对于GC Root来说的:

这里写图片描述

四、Java的内存管理机制

1.关于JVM

我们平常在查看我们的java版本时, 你会发现:

$ java -version
java version "1.8.0_74"
Java(TM) SE Runtime Environment (build 1.8.0_74-b02)
Java HotSpot(TM) 64-Bit Server VM (build 25.74-b02, mixed mode)

其中有个HotSpot VM的东西, 那么这个是什么呢? 和JVM有什么关系呢?

HotSpot VM是JVM的一种实现, 包含了服务器版和桌面应用程序版, 现时由Oracle维护并发布.


我们当前使用的sun(oracle)的java版本(应该是1.3以上)都是内置的HotSpot VM实现. 所以接下来的分析也都是基于HotSpot VM的, 但是还是简称JVM

2.Java堆中各代分布

JVM使用分代式的内存管理方式, 将Heap分成三代 — 新生代, 老一代, 持久代:

这里写图片描述

Young Generation

  • 翻译为新生代,
  • 用来存储new的对象,
  • 该区域的内存管理使用Minor GC(小GC),
  • 进一步分为Eden space,Survivor 0和Survivor 1三个部分

Old Generation

  • 翻译为老年区
  • 新生代中执行小颗粒的GC幸存下来的“老”对象,
  • 该区域的内存管理使用Major GC(Full GC,大GC)

Permanent Generation

  • 翻译为持久代
  • 是指内存的永久保存区域,主要存放Class和Meta的信息,Class在被 Load的时候被放入该区域. 它和和存放Instance的Heap区域不同,GC不会在主程序运行期对持久代进行清理,所以如果你的APP会Load很多CLASS的话,就很可能出现PermGen space错误。

3.JVM使用的GC算法是什么?

  • 分代收集。

  • 即将内存分为几个区域,将不同生命周期的对象放在不同区域里;

  • 在GC收集的时候,频繁收集生命周期短的区域(Young area);

  • 比较少的收集生命周期比较长的区域(Old area);

  • 基本不收集的永久区(Perm area)。

4.Minor GC(小GC)和Major GC(大GC)有什么区别?

Minor GC

  • 收集 生命周期短的区域(Young area)。
  • 执行非常频繁, 而且速度特别快

Major GC

  • 收集生命周期短的区域(Young area)和生命周期比较长的区域(Old area)对整个堆进行垃圾收集。
  • 一般会比小GC慢十倍以上

大小GC都会发出”Stop the World”事件, 也就是说中断程序运行, 直至GC完成,他们的收集算法不同,所以使用的时间也不同。 Minor GC 效率也会比较高,我们要尽量减少 Major GC(Full GC) 的次数。 当显示调用System.gc() 时,gc does a full collection(both young generation and tenured generation).

五、GC的流程

了解了内存Heap的几个区域, 我们再来看下垃圾收集器是怎么利用这几个区域来管理内存和回收垃圾的.

1.创建新的对象

每当我们使用new创建一个对象时, 这个对象会被分配到新生代的Eden区域:

这里写图片描述

2.当eden区域满时

当Eden区域内存被分配完时, 小GC程序被触发:

这里写图片描述

引用可达的对象会移到Survivor(幸存者)区域–S0, 然后清空Eden区域, 此时引用不可达的对象会直接删除, 内存回收, 如下:

这里写图片描述

3. Eden再次满时

当Eden区域再次分配完后,小GC执行, 引用可达的对象会移到Survivor(幸存者)区域, 而引用不可达的对象会跟随Eden的清空而删除回收.

需要注意的是, 这次引用可达的对象移动到的是S1的幸存者区.

而且, S0区域也会执行小GC, 将其中还引用可达的对象移动到S1区, 且年龄+1. 然后清空S0, 回收其中引用不可达的对象.

此时, 所有引用可达的对象都在S1区, 且S1区的对象存在不同的年龄. 如下:

这里写图片描述

当Eden第三次满时, S0和S1的角色互换了:

这里写图片描述

依此循环.

4. 当Survivor区的对象年龄达到”老年线”时

上面1~3循环, Survivor区的对象年龄也会持续增长, 当其中某些对象年龄达到”老年线”, 例如8岁时, 它们会”晋升”到老年区:

这里写图片描述

如此1~4步重复, 大体流程是这样的
这里写图片描述

六、GC的日志

1.Minor GC日志

这里写图片描述

2.Major GC(Full GC)日志

这里写图片描述

七、GC的触发条件

空闲时
1.当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。

堆内存不足
2.Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。

八、减少GC开销

1.不要显式的调用System.gc()

此函数建议JVM进行Major GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

2.尽量减少临时对象的使用

临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

3.对象不用时最好显式置为Null

一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

4.尽量使用StringBuffer,而不用String来累加字符串

由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer 是可变长的,它在原有基础上进行扩增,不会产生中间对象。

5.能用基本类型如Int,Long,就不用Integer,Long对象

基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

6.分散对象创建或删除的时间

集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片, 从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC 的机会。

持续更新中(GC的种类)……

猜你喜欢

转载自blog.csdn.net/lixpjita39/article/details/79451947