说明
本文是学习内存优化时个人的总结,由于本人是刚开始接触Android的性能优化方面的知识,肯定有很多知识点上的不足和错漏,请各位谅解。
App内存组成以及限制
Android 给每个 App 分配一个 VM ,让App运行在 dalvik 上,这样即使 App 崩溃也不会影响到系统。系统给 VM 分配了一定的内存大小, App 可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出 VM 最大内存,就会出现内存溢出 crash 。由程序控制操作的内存空间在 heap 上,分 java heapsize 和 native heapsize
Java申请的内存在 vm heap 上,所以如果 java 申请的内存大小超过 VM 的逻辑内存限制,就会出现内存溢出的异常。native层内存申请不受其限制, native 层受 native process 对内存大小的限制。那么如何查看系统对APP的内存限制呢?
(1)如果你的手机root过,我们可以通过 adb shell 在 命令行窗口查看,命令如下:
adb shell cat /system/build.prop
这里主要关注三个属性即可:
1.heapstartsize:App启动的初始分配内存
2.heapgrowthlimit:APP能够分配到的最大限制
3.heapsize:开启largeHeap=‘true’的最大限制
作为应用的开发者,这几个值我们是无法改变的(root过或者手机系统开发者除外),我呢只需要知道有这么几个值即可。
(2)通过代码获取
ActivityManager activityManager =(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();//以m为单位
Android内存分配与回收机制
内存分配
Android的Heap空间是一个 Generational Heap Memory 的模型,最近分配的对象会存放在 Young
Generation 区域,当一个对象在这个区域停留的时间达到一定程度,它会被移动到 Old
Generation ,最后累积一定时间再移动到 Permanent Generation 区域。
1、Young Generation(新生代)
由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当次Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。
2、Old Generation(老年代)
一般情况下,年老代中的对象生命周期都比较长。
3、Permanent Generation(持久代)
用于存放静态的类和方法,持久代对垃圾回收没有显著影响。
总结:内存对象的处理过程如下:
1、对象创建后在Eden区。
2、执行GC后,如果对象仍然存活,则复制到S0区。
3、当S0区满时,该区域存活对象将复制到S1区,然后S0清空,接下来S0和S1角色互换。
4、当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到Old Generation。
5、当这个对象在Old Generation区域停留的时间达到一定程度时,它会被移动到Old
Generation,最后累积一定时间再移动到Permanent Generation区域。
系统在Young Generation、Old Generation上采用不同的回收机制。每一个Generation的内存区域都
有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,
会触发GC操作,以便腾出空间来存放其他新的对象。
执行GC占用的时间与Generation和Generation中的对象数量有关:
Young Generation < Old Generation < Permanent Generation
Gener中的对象数量与执行时间成正比。
4、Young Generation GC
由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
5、Old Generation GC
由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。
可回收对象的判定
可达性算法:
从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。
Java定义的GC Roots对象:
虚拟机栈(帧栈中的本地变量表)中引用的对象。
方法区中静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。
GC类型
kGcCauseForAlloc:分配内存不够引起的GC,会Stop World。由于是并发GC,其它线程都会停止,直到GC完成。
kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。
kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。
GC算法
1.标记清除算法
分为两步
标价: 标记的过程其实就是,遍历所有的GC Roots,然后将所有的 GC Roots可达的对象标记为存活的对象。
清除:清除的过程将遍历堆中所有的对象中没有标记的对象全部清除掉
特点:
(1)扫描两次
(2)位置不连续,存在碎片
(3)两遍扫描
2.复制算法
描述:
(1)复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。
(2)当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。
(3)此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。
特点:
(1)实现简单,运行高效
(2)空间利用率只有一半
(3)没有碎片
3.标记整理算法
描述:
y与标记/清除算法类似,分为两步
(1)标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
(2)整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
特点:
(1)没有内存碎片
(2)效率偏低
(3)两遍扫描,指针需要移动
Android低内存杀进程机制
Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:
Empty process(空进程)
Background process(后台进程)
Service process(服务进程)
Visible process(可见进程)
Foreground process(前台进程)
系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况
下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。
ActivityManagerService 会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收( lowmemorykiller , Oom_killer )。这里只是大概的流程,中间过程还是很复杂的
什么是OOM
OOM(OutOfMemoryError)内存溢出错误,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草。
OOM分类
内存泄露的解决方法
1.常见的分析工具
(1)MAT
(2)Memory Profile
(3)LeakCanary
Memory Profile检测内存泄露
首先我们写一个测试Demo,在MainActivity中打开SecondActivity,在伴随对象中持有SeconnActivity的实例,然后关闭SecondActivity的实例。secndActivity的到代码如下:
class SecondActivity : AppCompatActivity() {
private lateinit var mButton: AppCompatButton
private var mWeakRef: WeakReference<String>? = null
companion object {
var context123: Context? = null
var weakReferenceObj: WeakReference<String>? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context123 = this
setContentView(R.layout.activity_second)
mButton = findViewById(R.id.btn_click)
mWeakRef = WeakReference("lala")
weakReferenceObj = mWeakRef
mButton.setOnClickListener {
finish()
}
}
}
首先我们用Memory Proflile 分析该内存泄露
步骤如下
(1)运行项目
(2)点击Profile进入分析界面,点击左上角的+添加分析的项目
(3)绑定成功回进入分析界面,点击memory,进入内存分析
(4)打开SecondActivity页面后在关闭该页面,点击Capture head dump,再按下record捕捉内存视图
(5)选择show activity/fragment Leaks 既可以看到发生内存泄露的相关activity或fragment
(6)点击下方的Instance List的相关实例,点击Reference便可以看到相关对象的持有情况。
(7)分析相关的引用持有情况,这里可以看到,我们自己写的mContext123,分析该mContext何时被赋值的
companion object {
var context123: Context? = null
var weakReferenceObj: WeakReference<String>? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context123 = this
setContentView(R.layout.activity_second)
mButton = findViewById(R.id.btn_click)
mWeakRef = WeakReference("lala")
weakReferenceObj = mWeakRef
mButton.setOnClickListener {
finish()
}
}
发现该mContext123持有了一个SecondActivity的实例,当该SecondActivity对象想要执行销毁时,因为被mContext123持有而无法被销毁,从而造成了内存泄露。
至此Meomery Profile的内存泄露检测说明完毕
LeakCanary检测内存泄露
(1)首先引入LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
(2)然后运行项目,LeakCanary会自动检测内存泄露的点,如果检测到会在通知栏显示一条通知
(3)点击后进入Leack Canary 可以看到发生了泄露
(4)点击该条目,可以看到发生泄露的点,可以看到与Memory Profile找的泄漏点一致。
至此,LeakCanary检测内存泄露的说明讲解完毕