1,什么是内存泄漏?
比较正常的语言描述我也不懂,按照我的理解就是GC回收不了的那些内存区域就算是内存泄漏,也就是说某些内存中存在的对象不在GC回收的控制的范围内,导致这些内存无法被及时回收掉而长期存在于内存中。这里最好需要了解一下Android GC机制的原理,上网找些资料来看看比较好,譬如Android 操作系统的内存回收机制
如上所示,已知Android的内存回收机制是从一个叫GC Roots的对象开始,通过一定的算法去查找所有跟GC Roots是否有直接引用或者间接引用,例如Object A和Object B跟GC Roots是直接引用,那么A和B的内存就不会被回收,虽然Object C和Object D跟GC Roots没有直接的引用关系,但是明显它们跟Object B有直接的关系跟GC Roots是间接的可达关系,so,C和D也不会被回收。再看看Object H,明显它跟GC Roots没有直接可达的关系,那么实际上Object H算是无用对象了,在内存中会等待GC的回收。Object J和Object K虽然跟Object I有着直接的引用关系,但是Object I跟GC Roots没有直接或者间接的可达关系,此时I、J、K的对象所占用的内存也是可以被GC回收掉的。那么如果Object C和Object D已经被使用完了,结束了使命了,以后也不会被使用到,成为无用的对象了,但是由于他们跟GC Roots保持着间接的引用关系,导致C和D占用的内存无法被gc回收掉,使得应用程序可使用的内存变小了,那么此时就可以说C和D引起了内存泄漏。
内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。
2,可以作为GC Root 引用点的是:
(1)JavaStack中的引用的对象。
(2)方法区中静态引用指向的对象。
(3)方法区中常量引用指向的对象。
(4)Native方法中JNI引用的对象。
(5)Thread——“活着的”线程。
3,解决内存泄漏的步骤
(1)粗略判断法
首先我们在项目中,肯定是通过肉眼看代码的形式估计是很难看出应用中是否存在内存泄漏的问题的,而且在确认哪里有泄漏的前,我们需要预估一下,应用是否存在内存泄漏的情况,这个步骤相当简单,我一般是通过在设备中打开App,然后跳几个页面,操作几个功能后,就退出App。这时候通过Android Monitor面板上的Monitors上提供的内存分析工具查看,如下图所示:
我们在退出App的情形下,点击System Information --> Memory Usage就会生成内存使用状态的分析日志
Applications Memory Usage (kB):
Uptime: 507591 Realtime: 507591
** MEMINFO in pid 2808 [com.cambridge] **
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 16825 16692 0 0 39168 35044 4123
Dalvik Heap 32993 32952 0 0 34874 33035 1839
Dalvik Other 981 980 0 0
Stack 940 940 0 0
Ashmem 10100 10032 0 0
Other dev 4 0 4 0
.so mmap 2049 192 236 0
.apk mmap 26295 116 25752 0
.ttf mmap 47 0 8 0
.dex mmap 8232 228 8004 0
.oat mmap 3205 0 824 0
.art mmap 1900 988 308 0
Other mmap 2503 8 1968 0
Unknown 19662 19660 0 0
TOTAL 125736 82788 37104 0 74042 68079 5962
App Summary
Pss(KB)
------
Java Heap: 34248
Native Heap: 16692
Code: 35360
Stack: 940
Graphics: 0
Private Other: 32652
System: 5844
TOTAL: 125736 TOTAL SWAP (KB): 0
Objects
Views: 77 ViewRootImpl: 0
AppContexts: 2 Activities: 1
Assets: 3 AssetManagers: 2
Local Binders: 14 Proxy Binders: 20
Parcel memory: 10 Parcel count: 42
Death Recipients: 0 OpenSSL Sockets: 0
SQL
MEMORY_USED: 215
PAGECACHE_OVERFLOW: 42 MALLOC_SIZE: 62
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 24 14 1/23/2 /data/user/0/com.cambridge/databases/xUtils_http_cookie.db
4 24 85 3/22/4 /data/user/0/com.cambridge/databases/xUtils_http_cookie.db (2)
上面就是App退出后,Memory Usage给出的内存情况,主要看一下Objects一栏,表示该内存中对象的数量,可以看到其中View对象占77个,Activity对象还存在1个,说明系统没有回收掉的对象还存在这么多,有可能是存在着内存泄漏的问题。
粗略的判断可以总结为,使用Android Monitor中的Memory Usage查看Views和Activity的对象是否为0来判断App是否存在内存泄漏的情况,但是这种粗略的判断是无法定位具体哪里发生泄漏的,下面才是查看具体泄漏的情况。
注:adb shell dumpsys meminfo 包名 -d。我们也可以直接使用命名行的方式直接生成Memory Usage的信息。
(2)确定内存泄露的大概范围
在Android Studio中我们可以打开Android Monitor面板,通过一系列的动作的执行来判断大概是哪些地方发生了内存泄漏的问题,具体步骤是这样的,比如我这里先打开我的App的首页,然后等首页稳定后(加载完毕),点击Monitor中的GC,执行几下垃圾回收,让首页的内存状态稳定下来,记录这时的内存使用量
如上图所示,我们在首页的时候,点击上面的小黄点,执行GC,然后记录下,当前的内存占用,大概是30.95M左右。这时我们跳转到某个页面,再执行一些操作,最后返回到首页,再看一遍内存的状态,如下:
上面的图中,1表示刚刚跳转发生时进入第二个页面发生了内存抖动,第二个页面申请了大量的内存,其实是这样的,第二个页面显示的是一张大图。然后我们看2的位置,是返回首页后,我又执行了2遍GC,造成内存回收了一部分。然后看看现在的内存状况,占用了31.86M,跟之前没有跳转时的30.95M相比,在同一个Activity里内存多消耗了近1M多,久可以大致的判断其实在跳转第二个页面时,第二个页面发生了内存泄漏问题,也就是说现在大致可以将内存泄漏的范围确定在第二个Activity内了。
(3)Heap Snapshot进一步确定内存泄漏的位置
通过Heap Snapshot工具进一步查找内存泄漏的位置
如上所示,我们可以点击Monitor面试上的Dump Java heap按钮去生成一个.hprof的文件,该文件包含了再这一时间点下的App内存的各种状况,打开.hprof文件如下图所示
上图打开后的hprof文件的样子,
A区域:列举了堆内存中所有的类
其中我们先选择Package Tree View,表示查看我们自己app下的类,然后看看图中方框中4个属性的意思:
Total Count:内存中该类的对象个数。
Heap Count:堆内存中该类对象的个数。
Sizeof:物理大小。
Shallow Size:该对象本身占用内存的大小。
Retained Size:释放该对象后,节省的内存大小。
B区域:当我们点击某个类时,右边的B区域会显示该类的实例化对象,这里面会显示有多少个实体,以及详细信息。
Depth:深度。
Shallow Size:对象本身内存大小。
Dominating Size:管辖的内存大小。
C区域:显示哪些对象应用了B区域中的对象。
通过Heap Snapshot工具进一步查找内存泄漏的位置,就是反复操作和生成hprof文件,进行对比,发现哪些对象有异常情况(例如突然多了一个实例),那么就点击该实例,在C区域中查找我们自己代码中的引用,通过查找到的线索去回溯相关代码,找到内存泄漏的地方加以解决。比较麻烦!!!
(4)使用MAT进行查找
上面介绍了Heap Snapshot,看样子通过Heap Snapshot查找内存泄漏的位置是比较麻烦的,所幸Heap Snapshot帮我们生成了.hprof文件了,那么就可以通过更高级的MAT工具打开.hprof文件进行分析了。MAT下载地址
打开MAT,File--Open Heap Dump--选择.hprof文件导入,然后MAT会生成一个内存泄漏的Overview。
MAT工具最常用的是Histogram
如上所示,Histogram可以显示.hprof文件里涉及的所有的对象,都一一列表在里面,并且其中涉及到多少引用的数量等等。下面我们就要通过一种最简单的方式来使用Histogram来查找内存泄漏的问题。
我们可以在app里针对同一个操作,产生2个.hprof文件,通过Histogram将2个.hprof文件的对象信息进行对比就能很快了解,2次操作中哪些对象变化了。
通过上图的操作,可以选择另外一个.hprof文件进行对比,对比结果如下:
通过生成的对比后的结果可以看到,其实2个.hprof文件之间的对比就是对比它们涉及到的对象引用的数量的变化,我们可以在上图看到数字的变化,0代表2次操作过程该对象没有变化,+代表增加了引用,-代表减少了引用,尤其是+号代表的对象,要格外注意,有可能就是发生内存泄漏的地方了。
如果我们通过对象找到了可能发生内存泄漏的对象时,这时候回到某一个.hprof文件的Histogram列表里,找到这个对象,然后右键点击
选择List Object,其中“with outgoing references”代表该对象引用了哪些对象,“with incoming references”代表哪些对象引用了该对象。选择“with incoming references”,进入下面的视图
上面的视图列举了发生内存泄漏的对象被哪些对象引用的情况,然后我们右键消除一些软引用弱引用等等情况
生成了下面的视图
好了,撸完了,很蛋疼,只学习了Heap Snapshot和MAT的使用步骤,但是博客中的Demo是我真是项目中的,居然没有找到合适的内存泄漏的地方来演示一下,所以上面的例子可以忽略了,只记着heap snapshot和MAT的使用即可啊~~