java 对象的生命周期

主要分7个阶段

  1. Created : 创建阶段
    主要分以下几个步骤
    • 为对象分配存储空间
    • 构造对象
    • 从父类到子类对static成员进行初始化,类的static成员初始化在ClassLoader加载该类的时候进行
    • 父类成员变量按照顺序初始化,递归调用超类的构造方法
    • 子类成员变量按照顺序初始化,一旦对象被创建,子类的构造方法就调用该对象并未某些变量赋值
  2. In Use : 应用阶段
    此时对象至少被一个强引用持有。
  3. InVisible : 不可见阶段
    当一个对象处于不可见阶段,说明程序本身不再持有该对象的任何强引用,虽然该对象仍然是存在的。
    简单的说就是程序的执行已经超出了该对象的作用域,但是该对象仍然可能被某些已经装载的静态变量线程或者JNI等强引用持有,这些特殊的强引用称为 GC ROOT。被这些GC ROOT强引用的对象会导致该对象的内存泄漏,因而无法被GC回收。
  4. UnReachable : 不可达阶段
    该对象不再被任何强引用持有。
  5. Collected : 收集阶段
    当GC已经对该对象的空间重新分配做好了准备的时候,对象进入收集阶段,如果该对象重写了 finalize(),则执行它。
  6. Finalized : 终结阶段
    等待垃圾回收器回收该对象空间。
  7. Deallocated : 对象重新分配阶段
    GC对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失。

用一张图片表示如下 添加图片

内存分配策略

  • 对象/变量的内存分配由程序自动负责
  • 共3种,静态分配,栈式分配,和堆式分配,分别指向 静态变量,局部变量和对象实例

添加图片

名词解释

  • 寄存器: 速度最快的存储场所。因为寄存器处于 CPU 内部,在程序中无法控制。
  • 栈(Stack):存放基本类型的数据和对象引用。但对象本身不存放在栈中,而是堆中
  • 堆(Heap): 存放由 new 创建的对象和数组。在堆中分配的内存,由 Java 虚拟机的 GC 来管理,JVM 所管理的内存中最大的一块
  • 静态存储区域(Static Field): 在固定的位置存放应用程序运行试一直存在的数据, Java 在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量,如静态的数据变量
  • 常量池(Constant Pool): JVM必须为每一个被装载的类型维护一个常量池。常量池就是该类型所有用到的常量的有序集合,包括直接常量(基本类型,String)和对其他类型,字段和方法的符号引用。

JVM 将内存划分为几块,分别如下所示

  1. 方法区: 存储类信息,常量,静态变量等。 所有线程共享
  2. 虚拟机栈:存储局部变量表,操作数栈等,与线程的生命周期同步,对这个区域规定了两种异常状况
    • StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
    • OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。
  3. 本地方法栈:不同与虚拟机栈为Java方法服务,他是为Native方法服务。
  4. 堆: 内存中最大的一块区域,每一个对象实际分配的内存都在堆上 ,而在虚拟机栈中分配的只是引用。这些引用会指向真正存储的对象。此外,堆也是GC所主要作用的区域。并且内存泄漏也是发生在这个区域。 所有线程共享
  5. 程序计数器: 存储当前线程执行目标方法执行到了第几行。

如下图 添加图片

举例

public class Sample {    
    // 该类的实例对象的成员变量s1、mSample1 指向对象存放在堆内存中
    int s1 = 0;
    Sample mSample1 = new Sample();   

 	// 方法中的局部变量s2、mSample2存放在 栈内存
 	// 变量mSample2所指向的对象实例存放在 堆内存
    public void method() {        
        int s2 = 0;
        Sample mSample2 = new Sample();
    }
}
// 变量mSample3的引用存放在栈内存中,然而变量mSample3所指向的对象实例存放在堆内存
Sample mSample3 = new Sample();

GC算法

主要包括

标记-清除算法

  • 实现原理: 标记出所有需要回收的对象。然后统一回收所有被标记的对象
  • 特点
    1. 标记和清除效率不高。
    2. 产生大量不连续的内存碎片。

复制算法

  • 实现原理: 将内存划分为大小相等的两块。一块内存用完之后复制存活对象至另一块。清理这一块内存。
  • 特点
    1. 实现简单,运行高效。
    2. 浪费一半空间,代价大。

标记-整理算法

  • 实现原理:记过程与 ”标记-清除“ 算法一样。存活对象往一端进行移动。清理其余内存。
  • 特点
    1. 避免 ”标记-清除” 算法导致的内存碎片。
    2. 避免复制算法的空间浪费。

分代收集算法

大多数虚拟机厂商所选用的算法

  • 特点
    1. 结合多种收集算法的优势。
    2. 新生代对象存活率低 => “复制” 算法(注意这里每一次的复制比例都是可以调整的,如一次仅复制 30% 的存活对象)。
    3. 老年代对象存活率高 => “标记-整理” 算法。

详情查看 扫盲系列 - JVM 的垃圾回收

Low Memory Killer 机制

LMK 机制是针对于手机系统所有进程而制定的,当我们手机内存不足的情况下,LMK 机制就会针对我们所有进程进行回收,而其对于不同的进程,它的回收力度也是有不同的,目前系统的进程类型主要有如下几种:

  1. 前台进程
  2. 可见进程
  3. 服务进程
  4. 后台进程
  5. 空进程

从前台进程到空进程,进程优先级会越来越低,因此,它被 LMK 机制杀死的几率也会相应变大。此外,LMK 机制也会综合考虑回收收益,这样就能保证我们大多数进程不会出现内存不足的情况。

详情Android 内存管理机制

内存泄漏

详情: Android 内存泄漏总结

图片Bitmap 相关

详情: Android 性能优化 – Bitmap 优化

内存抖动

定义: 内存大小不断浮动的现象

  • 原因:
    程序频繁的分配内存或者GC频繁的回收内存。主要表现在大量临时的小对象频繁创建

  • 后果:
    1. GC频繁的回收导致卡顿,甚至内存溢出OOM
    2. 大量临时的对象频繁创建,会导致内存碎片,使得当需要内存分配的时候,虽然总体上还有剩余内存可分配,但是由于这些内存不连续,导致无法整块分配 系统则视为内存不够,出现OOM
  • 优化方案
    尽量避免频繁创建大量,临时的对象

代码质量和数量

代码本身的质量(如数据结构,数据类型等)和数量(代码量的大小)可能导致大量的内存问题,如占用内存大,内存利用率低等,主要可以从以下几个方面入手

  1. 减少不必要的类,方法,库,并且使用混淆
  2. 使用性能更高的数据结构。例如。SparseArray,SparseBooleanArray,LongSparseArray,
    • SparseArray 就避免掉了基本数据类型转换成对象数据类型的时间。
    • ArrayMap提供了和HashMap一样的功能,但避免了过多的内存开销,方法是使用两个小数组,而不是一个大数组。并且ArrayMap在内存上是连续不间断的。
  3. 使用占内存小是数据结构,避免使用枚举类型
  4. 根据不同的应用场景,使用不同的引用类型。关于Java引用类型,请查看: 扫盲系列 - Java 引用类型
  5. 减少AutoBoxing。自动装箱的核心就是把基础数据类型转换成对应的复杂类型。在自动装箱转化时,都会产生一个新的对象,这样就会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特别是HashMap这类容器,进行增、删、改、查操作时,都会产生大量的自动装箱操作。
    • 检测方式: 使用TraceView查看耗时,如果发现调用了大量的integer.value,就说明发生了AutoBoxing。

其他优化技巧

  1. 调大 虚拟机Dalvik的堆内存大小
    即 在AndroidManifest.xml的application标签中增加一个android:largeHeap属性(值 = true),从而通知虚拟机 应用程序需更大的堆内存。但不建议不鼓励该做法
  2. 获取当前可使用的内存大小
    调用 ActivityManager.getMemoryClass()方法可获取当前应用可用的内存大小(单位 = 兆)
  3. 获取当前的内存使用情况
    在应用生命周期的任何阶段,调用 onTrimMemory()获取应用程序, 当前内存使用情况(以内存级别进行识别),可根据该方法返回的内存紧张级别参数 来释放内存。
  4. 当视图变为隐藏状态时,则释放内存
    当用户跳转到不同的应用/视图不再显示时, 应释放应用视图所占的资源
  5. 内存复用
    • 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
    • 视图复用:可以使用ViewHolder实现ConvertView复用。
    • 对象池:显示创建对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。
    • Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。
  6. item被回收不可见时释放掉对图片的引用

内存优化的意义

  1. 减少OOM,提高应用稳定性。
  2. 减少卡顿,提高应用流畅度。
  3. 减少内存占用,提高应用后台运行时的存活率。
  4. 减少异常发生和代码逻辑隐患。

搬运地址:

Android APP性能优化的四个方面的总结

Android性能优化:这是一份全面&详细的内存优化指南

Android性能优化之内存优化

深入探索 Android 内存优化(炼狱级别)

吐血整理!究极深入Android内存优化(二)

面试官: 说一下你做过哪些性能优化?

Android 工程师进阶 34 讲