前言
开发程序过程中常常涉及到内存的申请以及回收过程,由于表现形式不明显而且Java有自动垃圾回收机制,普遍情况下不会过度关注内存,容易疏漏导致抛出异常。同时OOM等内存问题所抛出的堆栈信息有可能不是问题的直接原因,因此还存在排查难等问题。
1 分类
- 内存抖动:短时间内创建大量新生代对象导致GC频繁,通过观察Profiler Memory发现内存呈锯齿状。
- 内存泄漏:已分配内存由于某种原因未能被回收,导致可用内存逐渐减少。例如,界面被销毁,但是Activity还被持有引用,导致Activity无法被回收。
- 内存溢出:无法申请到所需的内存空间时。例如,加载图片过大导致发生程序异常,提示OutOfMemory。
2 工具使用
- Profiler
- Memory Analyzer(和Proiler差不多,不使用)
- LeakCanary
3 内存抖动
首先使用Profiler进行排查,然后再根据记录抖动位置的代码进行排查。以下面这个方法为例:
/**
* 排序后打印二维数组,一行行打印
*/
fun imPrettySureSortingIsFree() {
val dimension = 300
val lotsOfInts =
Array(dimension) { IntArray(dimension) }
val randomGenerator = Random()
for (i in lotsOfInts.indices) {
for (j in lotsOfInts[i].indices) {
lotsOfInts[i][j] = randomGenerator.nextInt()
}
}
for (i in lotsOfInts.indices) {
var rowAsStr = ""
//排序
val sorted = getSorted(lotsOfInts[i])
//拼接打印
for (j in lotsOfInts[i].indices) {
rowAsStr += sorted[j]
if (j < lotsOfInts[i].size - 1) {
rowAsStr += ", "
}
}
}
}
fun getSorted(input: IntArray): IntArray {
val clone = input.clone()
Arrays.sort(clone)
return clone
}
1.运行后观察Profiler发现内存GC频繁,我们可以点击record
记录下内存的变化,查看这段时间的内存分配情况。
2.前面说过造成内存抖动的原因,因此可以根据内存分配的数量进行分析。点击
Allocations
进行排序之后,发现char[]分配的对象是最多的,因此char[]成为了首先怀疑的对象。
3.点击列表的实例查看char[]分配回调栈,通过对比发现内容都一致,因此可以基本断定是该实例导致内存抖动。
- 最后发现可能是MemoryPerformanceActivity中Oncreate的handleMessage或者imPrettySureSortingIsFree方法引起的抖动,我们就可以点击右键选择Jump to spurce跳转,并解决问题。
4 内存泄漏
导致内存泄漏的原因有很多,例如Cursor、IO操作未关闭,Bitmap、Context未回收等等。我们可以通过Profler Memory堆转储功能进行内存泄漏分析。以下面方法为例:
class MemoryPerformanceActivity : AppCompatActivity(), CallBack {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_memory_performance)
val imageView: ImageView = findViewById(R.id.imageView)
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
imageView.setImageBitmap(bitmap)
CallBackManager.addCallBack(this)
}
override fun mack() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
object CallBackManager {
private var mCallBacks = arrayListOf<CallBack>()
fun addCallBack(callBack: CallBack) {
mCallBacks.add(callBack)
}
fun removeCallBack(callBack: CallBack) {
mCallBacks.remove(callBack)
}
}
1.来回跳转数次到MemoryPerformanceActivity,通过Profiler观察内存的变化,主要是查看Activity结束时Total是否有减少,我们可以如果占用内存越来越高,则可能发生内存泄露。
2.猜测可能发生内存泄漏之后,我们可点击
强制GC
去除引用,再点击Dump java heap
,即可得到hprof文件
进行分析。
3.我们可以点击
Arrange by packge
按包名进行排序或者点击Filter
搜索myApplication。通过观察可以发现强制GC之后MemoryPerformanceActivity仍然存在6个对象。我们想要看到的是1个对象,因此这并不是我们预期想要看到的。
4.点击MemoryPerformanceActivity查看实例,可以看到每个Aactivity的实际内存分配为94000左右,引用深度Depth为3,再通过References查看引用,可以看出来是mCallBacks持有了6个Activity的引用,其Retained Size的大小为570803。最后看到
shadow$_klass_in_CallBackManager
,发现是CallBackManager
引用了mCallBacks
,点击右键选择Jump to source
跳转到CallBackManager进行问题分析。
5.通过分析我们发现CallBackManager 是个静态类,持有的对象与App生命周期一样长,因此需要手动将CallBack移除。
override fun onDestroy() {
super.onDestroy()
CallBackManager.removeCallBack(this)
}
5 内存溢出
当申请不到需要的内存时就会发生内存溢出(OOM),在开发过程中我们常见的OOM就有Bitmap加载不合理图片造成的。我们可以在线下通过ARTHoot去监测出不合理的图片,在线下就将问题暴露出来。
从错误堆栈信息不一定能看出内存溢出的准确问题点,因为内存溢出可能只是表象,造成内存溢出的原因有可能是因为内存泄漏,导致可用内存越来越少,最后分配不到我们所需的空间,导致内存溢出。
6 LeakCanary
leakcanary是由square开源的框架,可以帮助我们快速发现内存泄漏,减少OOM。集成很简单,只需要下面这一步,原理就是利用了Provider的Oncreate比Application的生命周期还要早,内部实现了Provider并进行框架初始化。
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
}
集成之后Leakcanary就会自动帮我们收集内存泄漏信息。
总结
- MAT和Profiler使用上差不多,因此建议使用Profiler即可,发现问题可直接跳转到相应的代码位置,提高排查效率。
- 在开发初期集成LeakCanary去收集内存泄漏信息,早发现早解决。