一.理解安卓的内存机制
1.进程分配的内存
我们的ams通知zygote去fork一个进程时,系统就会为我们每个app分配内存,这个大小,我们可以通过adb shell cat /system/build.prop | grep heap就可以查看:
// 当 zygote fork 一个 app 进程分配一个虚拟机时
// 虚拟机的内存限制就是从这个文件读取的
dalvik.vm.headstartsize=16m // 起始内存大小
dalvik.vm.heapgrowthlimit=192m
dalvik.vm.heapsize=512m // 最大内存大小
dalvik.vm.heaptargetutilization=0.75 // 内存扩容指标
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=8m
2.进程回收的策略
Android将进程分为4个等级,它们按优先级顺序由高到低
依次是:
1.前台进程(前台正在交互的)
2.可见进程(正在进行用户当前知晓的任务,因此终止该进程会对用户体验造成明显的负面影响)
3.服务进程(比如后台服务下载的进程)
4.缓存进程 (是目前不需要的进程)
具体可以查看官网developer.android.google.cn/guide/compo…
我们在内存不足的时候,会按照从低到高依次回收进程以获取内存来保证系统的运转,这个也就是LowMemoryKiller的机制,我们可以通过adb shell cat /sys/module/lowmemorykiller/parameters/minfree
18432,23040,27648,32256,55296,80640,这个来查看回收进程的内存阈值,这个值的计算是把80640*4/1024换算的出来M的单位,也可以通过查看adj这个值来查看回收进程
adb shell cat /sys/module/lowmemorykiller/parameters/adj 0,100,200,250,900,950,他们是彼此对应的,adb shell cat /proc/${pid}/oom_adj,我们可以通过这个命令查看该进程的adj值
3.虚拟机的内存分配
jvm的运行时数据区
如上图,我们的jvm左边属于共享区域(方法区、堆区),所有的线程都能够访问;右边属于私有区域,每个线程都有自己独立的区域。
线程独占区:例如当我们程序运行时,我们的UI线程就拥有自己的java虚拟机栈,而线程中每个方法就是栈帧,栈帧里面又有局部变量、操作数、返回地址等,他们都是通过压栈的方式运行,程序计算器是为了存储当前线程运算的位置。
线程共享区:方法区:存放的静态常量、类信息、变量、常量池、静态方法等,堆区则是存放一些new出来的对象等。
GcRoot:方法区中的引用、虚拟机栈中栈帧的局部变量(也就是方法内的局部变量)都是GcRoot,也就是对象的起点,当虚拟机进行垃圾回收时,没有GcRoot的对象(堆区)就会被回收,如果当我们不需要这个对象,但是这个引用链没有被打断,对象就无法回收,我们的内存问题也就这样形成了。
二.常见内存问题
1.内存抖动
一般是在短时间内大量对象的创建和销毁导致,我们的解决办法通常是使用缓存池技术,将对象缓存起来,或者利用全局变量,尽量不要短时间内生成大量的对象。
2.内存泄漏
内存泄漏的本质是长生命周期持有短生命周期的引用,从而导致短生命周期对象无法被释放
常见的内存泄漏有:
1.handler导致的Activity泄漏,解决办法:我们尽量使用弱引用;
2.动画导致的Activity泄漏,解决办法:在界面销毁时溢出动画;
3.观察着模式导致的泄漏,解决办法:界面销毁时移除观察者
4.单例模式导致的内存泄漏,解决方法:尽量使用ApplicationContex;
5.非静态内部类导致的内存泄漏,解决办法:使用静态的代替非静态的;
6.资源对象使用后未关闭,解决办法:及时的关闭资源对象;
7.容器中的对象没有及时的清理,解决办法:及时的清理容器的对象引用;
3.内存溢出
内存溢出一般是在没有连续的内存、堆内存不够、线程超过限制导致,我们需要合理的使用内存,使用线程池技术,避免线程过多的创建。
三.辅助检测工具
1.LeakCanary
Leakcanary是一种简便定位内存泄露的方式,使用也比较简单,在build.gradle配置
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'即可,它的2.x版本也是利用contentProvider初始化起来的,它的核心原理将被观察的Activity销毁时,去检测Activity是否被回收,它使用一个弱引用KeyedWeakReference指向这个activity,并且给这个弱引用指定一个Referencequeue,同时创建一个key来标识该activity,如果KeyedWeakReference 持有的 Activity 对象如果被垃圾回收,该对象就会加入到引用队列 Referencequeue,如果能从Referencequeue取出,就移除key,也就说明不存在内存泄漏,否则就dump堆栈信息并打印日志。
下面是一个观察者或者说是单例引起的内存泄漏的小案例
interface Observe{
fun handler()
}
class LeakObservable {
private val observes: ArrayList<Observe> = arrayListOf()
//单例,模仿长生命周期引用短生命周期,导致短生命周期无法释放
companion object {
val sInstance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
LeakObservable()
}
}
fun addObserve(observe: Observe) {
if (!observes.contains(observe)) {
observes.add(observe)
}
}
fun removeObserve(observe: Observe) {
observes.remove(observe)
}
fun notifyObserve() {
observes.forEach {
println("handler${it}")
it.let {
it.handler()
}
}
}
}
class LeakActivity:AppCompatActivity() , Observe {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
}
override fun handler() {
println( "$this handler")
}
fun add(view: View) {
LeakObservable.sInstance.removeObserve(this@LeakActivity)
}
fun remove(view: View) {
LeakObservable.sInstance.addObserve(this@LeakActivity)
}
override fun onDestroy() {
super.onDestroy()
println("onDestroy===>>>")
}
}
当我们返回退出时,由于LeakObservable的单例持有LeakActivity的引用,导致其无法释放,所以我们的LeakActivity就会有以下的堆栈提示:
2.MemoryProfiler
我们在Leakcanary上看到,他把堆栈信息生成到data目录下了,我们把这个文件down下来
在Memory profile中打开,也可以看到LeakActivity产生了泄漏,具体的MemoryProfile一些其他
使用,可以查看官网developer.android.google.cn/studio/prof…
3.MAT
MAT 的全称是 Memory Analysis Tool,是对内存进行详细分析的工具,它是 Eclipse 插件,MAT 能帮助我们深入的进行分析并确定内存泄露和内存占用,导入 hprof 堆转储文件具体分析,我们Memory profile中的hprof无法在MAT中打开,需要将它转换为 MAT 可以识别的标准 hprof 文件,可以使用 SDK 自带的 hprof-conv 转换,它的路径在 sdk/platform-tools,命令执行:hprof-conv source.hprof des.hprof,然后用mt去打开它,一般我们通过三种方式查看 :
1.dominator去查看分析对象的引用关系。
Shallow Heap:对象自身占用的内存大小
Retained Heap:对象自身占用的内存+对象引用的对象所占用的内存
Percentage:实例对象占用比例。可以通过它很直观的看出哪些对象占用比例比较大需要关注优化内存,一般 bitmap 占用的比例会比较大
Merge Shortest Paths to GC Root 选项是用来显示距离 GC Root 最短的路径,根据引用类型会有多种选项,比如 with all references 就是显示包含所有的引用,一般我们要分析内存泄露需要排除软引用、弱引用和虚引用,因为这些引用是可以被回收的。选择后 MAT 会给出 LeakActivity 的GC引用链:
2. Histogram查看
Histogram 会罗列出内存中的对象、对象个数和占用内存大小
with outgoing references:它引用了哪些对象,with incoming references:哪些对象引用了它
我们的上面LeakActivity已经销毁了,但是依旧有实例,可以看看到底谁引用了它导致无法被回收。同样可以通过上面的GcRoot就可以查看到,这里就不做分析了。
3.OQL查看
OQL 全称为 Object Query Language,类似于 SQL 能够查询内存中满足指定条件的所有对象。查询格式如下:**
SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE ]
**比如我们上面要查找Activity
OQL 的官方文档:help.eclipse.org/latest/inde…
四.总结
内存优化问题是面试和平时开发中经常可能都会遇到的问题,也比较复杂,本文就常见问题做了简单的整理,文章的开头就android应用进程分配和回收策略做了简单的介绍,接着又对jvm运行时数据区的内存状态、GcRoot、以及运行时数据区里哪些是GcRoot做了简单的介绍,中间就一些常见内存问题以及一些解决办法,最后通过一个简单的小案例介绍了下常用的内存分析查找问题的工具.