概念
应用的开发离不开存储,存储分为网络、内存、SDCard文件存储以及外部SDCard2文件存储,开发中一定要注意好内存管理以免oom、卡顿等不好的用户体验,同时还要注意变量的回收,避免内存泄漏。下面呢先来了解一些基本的相关专业术语。
- RAM(random access memory)随机存取存储器即内存
- 寄存器(Registers):速度最快的存储场所,因为寄存器位于处理器内部,我们在程序中无法控制
- 栈(Stack):存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中
- 堆(Heap):堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器(GC)来管理。
- 静态域(static field): 静态存储区域就是指在固定的位置存放应用程序运行时一直存在的数据,Java在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量如静态的数据变量
- 常量池(constant pool):虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。
- 非RAM存储:硬盘等永久存储空间
堆栈的特点对比
栈:当定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
堆:当堆中的new产生数组和对象超出其作用域后,它们不会被释放,只有在没有引用变量指向它们的时候才变成垃圾,不能再被使用。即使这样,所占内存也不会立即释放,而是等待被垃圾回收器收走。这也是Java比较占内存的原因。
栈:存取速度比堆要快,仅次于寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
堆:堆是一个运行时数据区,可以动态地分配内存大小,因此存取速度较慢。也正因为这个特点,堆的生存期不必事先告诉编译器,而且Java的垃圾收集器会自动收走这些不再使用的数据。
栈:栈中的数据可以共享,它是由编译器完成的,有利于节省空间。
如果你对堆栈还不够清晰明了,那么请看下面一图
内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放(堆栈开辟的存储空间存储的值),结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏
内存分析
我们要怎么知道内存发生泄漏呢,需要借助内存分析工具MAT,或者使用开源项目LeakCanary,首先呢我们先到官网找到关于MAT的使用介绍,了解一个概要再来实践吧。
首先我们需要安装MAT,如果你是Eclipse开发还未转Android Studio,那么只需要安装MA插件即可,看下图(最新地址: http://download.eclipse.org/mat/1.5/update-site/)。
当然你如你用Android Studio ,但是不想用Eclipse来分析,那么可以下载独立的MAT,解压后得到下图效果,双击运行打开即可。
① Eclipse开发工具内建立一个测试的java程序,测试代码如下
import java.util.ArrayList;
import java.util.List;
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
while (1<2){
list.add("OutOfMemoryError soon");
}
}
}
运行就会爆内存泄漏问题
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at test.Main.main(Main.java:16)
② 生成xx.hprof文件,利用MAT进行分析,这里生成 xx.hprof文件有两种方案,eclipse 下的 java工程项目已run as >run config配置为例(另一种稍后提到)
添加VM arguments和配置输出xx.hprof文件的目录,运行爆上述错误,打开目录即可看到xx.hprof文件
当你满心欢喜的以为即将要成功的时候,突然给你整这么一处,这是肿么回事呢?
原因是: android的虚拟机导出的内存文件hprof文件格式与标准的 java hprof文件格式标准不一样,根本原因两者的虚拟机不一致导致的。只需要使用SDK中自带的转换工具转换就可以了,hprof-conv 源文件 目标文件在寻找解决这个问题的途中,遇到一个大b坑,http://blog.csdn.net/pugongying1988/article/details/9122699该篇博客告诉我在tools工具目录下,我命令行怎么都不行说找不到程序,我怀疑是不是我没安装插件,重装插件后还是不行,几经折腾在这里找到它
亮瞎了这是tools目录么,顿时万马奔腾,直呼尼玛!!回归正题,切换到该目录下调用hprof-conv 命令重新输出xx.hprof文件(为了方便,把新旧的xx.hprof文件都放在了改目录下)
说好的不是版本问题(不是绝对的),经过几次验证,特么就是eclipse导出版本问题(run as config配置导出的版本问题,具体原因没深究),最后通过DDMS导出的xx.hprof文件就没问题了(补充说明:原博客确定了是不是版本问题这样做是正确的,我们使用android studio开发,通过DDMS导出那个真不是版本问题,通过上面命令就可以了)
- Histogram
列出了集合的对象实例,每种类型的实例集合的 shallow size 和 retained size . shallow size指的是对象所消耗的内存大小,如每个对象引起消耗4个字节,或者8个字节,取决于你的操作系统(32位,还是64位), retained size的概念依赖于Retained set 的概念,Retained set 指的是当对象X被回收时,所有被垃圾回收器移除的对象集合, Retained size 即是Retained set所保持的内存大小。
具体操作如下,Overrview视图下面进入
按照规则过滤后列表,接着跟踪
再通过Path to GC Root(被JVM持有的对象,如当前运行的线程对象,被systemclass loader加载的对象被称为GC Roots, 从一个对象到GC Roots的引用链被称为Path to GC Roots, 通过分析Path to GC Roots可以找出JAVA的内存泄露问题,当程序不在访问该对象时仍存在到该对象的引用路径。 )跟踪变量没被回收的具体位置
根据提供Demo的xx.hprof文件跟踪发现HomeActivity里面调用了DrawableHelper类,内部mContext变量引起内存泄漏,这里的mContext变量如下(由于内部其他多个方法设置属性需要Context对象,本应该传入一次放到构造方法里面,private 不要static属性就好,但是呢不同的Activity调用就会造成Context对象问题,所以最好传入的Context对象为activity.getApplicationContext,亦或者其他今天属性方法传入参数,这样DrawableHeper不用保存Context实例引用,由此分析让我明白了一点:不是什么时候都适合用单例模式,要具体问题具体分析)
public class DrawableHelper {
protected static DrawableHelper mDrawableHelper;
protected static Context mContext;
private DrawableHelper() {
}
public static DrawableHelper getInstance(Context context) {
if (mDrawableHelper == null) {
synchronized (DrawableHelper.class) {
if (mDrawableHelper == null) {
mDrawableHelper = new DrawableHelper();
}
}
}
mContext = context;
return mDrawableHelper;
}
内存优化要素
① 重复字符串是内存浪费的一个典型例子:多个字符数组具有相同的内容。字符数组的内容通常会给出如何减少重复的思想。
② 空集合空间不存储任何数据。如果只有少数集合保存数据,考虑延迟初始化,即只在需要时创建集合。
③集合通常是创建一个默认初始容量。许多低填充率的集合表明,初始容量可以减少。
④ 软引用静态资源
⑤ 使用 Andorid 框架中优化过的数据容器,例如 SparseArray,SparseBooleanArray 和 LongSparseArray。类似于 HashMap 这一类的容器的效率不是很高,因为在每个 Map 中对于每一次的存放数据,他都需要独立一个单独的 Entry 对象进行传芳。而 SparseArray 由于禁止系统自动封装键值对,因此他更加有效率。并且你不需要担心丢失掉原有信息(AbsListView子类控件纪录checked属性和position)
⑥避免依赖注入框架,使用类似于 Xutils的ViewUtils注解模块的依赖注射框架,或许会使你的代码变得更加漂亮,因为他们能够减少你需要写的代码,并且为测试或者在其他条件改变的情况下,提供一种自适应的环境。但是,这些框架在初始化的时候会因为注释而消耗大量的工作在扫描你的代码上,这会让你的代码在进行内存映射的时候花费更多的资源。虽然这些内存能够被 Android 进行回收,但是等待整个分页被释放需要很长一段时间。
⑦使用混淆器ProGuard移除不必要的代码。
⑧ 不要因为某个需求而使用大体积类库,比如圆形头像只需要一个圆形头像即可没必要使用开源的PhotoView库,相对轻量级的CircleImageView跟适合或者自己自定义裁剪控件。
⑨ 常量用static final修饰,(context不要用static修饰,容易造成内存泄漏)
⑩ 不用的变量,即使释放NULL
第三方库LeakCanary实践
地址:https://github.com/square/leakcanary
项目依赖
dependencies {
debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.4-beta2‘
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2‘
testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2‘
}
Application初始化库
LeakCanary.install(this);
运行后如果程序出现内存泄漏,会有提示,找到Leaks图标打开可以查看泄露位置,分析原因,同时我们还可以通share heap dump,分享.hprof文件到电脑,通过MAT进行详尽的定位分析。(.hprof文件比较大建议wifi下进行传输)
小结
东拼西凑还是完成了这篇blog,LeakCanary集成到项目帮助我们分析泄露位置,如果还不能清晰的分析出具体原因,可以到处.hprof文件通过MAT分析,最后针对个人做个简短小结:
①以后开发一定要注意单例模式的运用了,太多的instance,并不是所有的类都需要。
② static 修饰常量都改为static final(个别不能final尝试去掉static修饰)
③Context 、Activity都不要用static修饰
④ 内存泄漏:开辟的堆栈的存储引用的管理,干掉非存活状态的堆栈引用,及时释放。
参考资料
http://blog.csdn.net/a396901990/article/details/37914465
http://www.jianshu.com/p/c49f778e7acf
http://blog.csdn.net/pugongying1988/article/details/9122699
http://wiki.eclipse.org/MemoryAnalyzer#HPROF_dumps_from_Sun_Virtual_Machines
http://blog.csdn.net/xu_fu/article/details/45678373
http://blog.csdn.net/tiantangrenjian/article/details/39182293
推荐博客
http://blog.csdn.net/a396901990/article/details/38904543
http://blog.csdn.net/a396901990/article/details/38707007