1. 内存的分配策略概述
程序运行时的内存分配有三种策略, 分别是静态的, 栈式的, 和堆式的,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、堆区和栈区。
静态存储区(方法区):内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在。它主要存放静态数据、全局 static 数据和常量。
栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区:亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在适当的时候用 free 或 delete 释放内存(Java 则依赖垃圾回收器)。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉。
堆和栈的区别:
在函数中(说明是局部变量)定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,java 会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
堆内存用于存放所有由 new 创建的对象(内容包括该对象其中的所有成员变量)和数组。在堆中分配的内存,由 java 虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号
堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是 4G),所以堆的空间比较灵活,比较大。栈是一块连续的内存区域,大小是操作系统预定好的,windows 下栈大小是 2M(也有是 1M,在编译时确定,VC 中可设置)。
对于堆,频繁的 new/delete 会造成大量内存碎片,使程序效率降低。对于栈,它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。
所以我们可以得出结论:
1). 局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。因为它们属于方法中的变量,生命周期随方法而结束。
2). 成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体),因为它们属于类,类对象终究是要被 new 出来使用的。
3). 我们所说的内存泄露,只针对堆内存,他们存放的就是引用指向的对象实体。
2. 内存泄露产生的原因
在 Java 中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器 (Garbage Collection,GC) 完成的,程序员不需要通过调用函数来释放内存,但它只能回收无用并且不再被其它对象引用的那些对象所占用的空间。
Java 的内存垃圾回收机制是从程序的主要运行对象 (如静态对象 / 寄存器 / 栈上指向的堆内存对象等) 开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收。GC 为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
在 Java 中,这些无用的对象都由 GC 负责回收,因此程序员不需要考虑这部分的内存泄露。虽然,我们有几个函数可以访问 GC,例如运行 GC 的函数 System.gc(),但是根据 Java 语言规范定义,该函数不保证 JVM 的垃圾收集器一定会执行。因为不同的 JVM 实现者可能使用不同的算法管理 GC。通常 GC 的线程的优先级别较低。JVM 调用 GC 的策略也有很多种,有的是内存使用到达一定程度时,GC 才开始工作,也有定时执行的,有的是平缓执行 GC,有的是中断式执行 GC。但通常来说,我们不需要关心这些。
但是我们仍然可以去监听系统的 GC 过程,以此来分析我们应用程序当前的内存状态。那么怎样才能去监听系统的 GC 过程呢?其实非常简单,系统每进行一次 GC 操作时,都会在 LogCat 中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下所示:
- 1.D / dalvikvm: <GC_Reason > <Amount_freed > ,
- <Heap_stats > ,
- <Pause_time >
首先第一部分 GC_Reason,这个是触发这次 GC 操作的原因,一般情况下一共有以下几种触发 GC 操作的原因:
GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发 GC 操作来释放内存。
GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行 GC 操作来释放内存。
GC_HPROF_DUMP_HEAP: 当生成 HPROF 文件的时候,系统会进行 GC 操作,关于 HPROF 文件我们下面会讲到。
GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行 GC 操作,比如调用 System.gc() 方法来通知系统。或者在 DDMS 中,通过工具按钮也是可以显式地告诉系统进行 GC 操作的。
接下来第二部分 Amount_freed,表示系统通过这次 GC 操作释放了多少内存。
然后 Heap_stats 中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)。
最后 Pause_time 表示这次 GC 操作导致应用程序暂停的时间。关于这个暂停的时间,在 2.3 的版本当中进行过一次优化,在 2.3 之前 GC 操作是不能并发进行的,也就是系统正在进行 GC,那么应用程序就只能阻塞住等待 GC 结束。虽说这个阻塞的过程并不会很长,也就是几百毫秒,但是用户在使用我们的程序时还是有可能会感觉到略微的卡顿。而自 2.3 之后,GC 操作改成了并发的方式进行,就是说 GC 的过程中不会影响到应用程序的正常运行,但是在 GC 操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了。
我们来看看 Java 中需要被回收的垃圾:
- {
- Person p1 = new Person();
- ……
- }
引用句柄 p1 的作用域是从定义到 "}" 处,执行完这对大括号中的所有代码后,产生的 Person 对象就会变成垃圾,因为引用这个对象的句柄 p1 已超过其作用域,p1 失效,在栈中被销毁,因此堆上的 Person 对象不再被任何句柄引用了。 因此 person 变为垃圾,会被回收。
这里我们需要讲述一个关键词:引用,通过 A 能调用并访问到B,那就说明A持有B的引用,或 A 就是 B 的引用,B的引用计数+1.
(1)比如 Person p1 = new Person(); 通过P1能操作 Person 对象,因此P1是 Person 的引用;
(2)比如类 O 中有一个成员变量是 I 类对象,因此我们可以使用 o.i 的方式来访问 I 类对象的成员,因此 o 持有一个 i 对象的引用。
GC 过程与对象的引用类型是严重相关的,我们来看看 Java 对引用的分类 Strong reference, SoftReference, WeakReference, PhatomReference
在 Android 应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
软 / 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软 / 弱引用的对象列表,从而为缓冲器清除已失效的软 / 弱引用。
假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生 OutOfMemory 异常。这时,我们可以考虑使用软 / 弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:
首先定义一个 HashMap,保存软引用对象。
- private Map < String,
- SoftReference < Bitmap >> imageCache = new HashMap < String,
- SoftReference < Bitmap >> ();
再来定义一个方法,保存 Bitmap 的软引用到 HashMap
- public class CacheSoftRef {
- //首先定义一个HashMap,保存引用对象
- private Map < String,
- SoftReference < Bitmap >> imageCache = new HashMap < String,
- SoftReference < Bitmap >> ();
- //再来定义一个方法,保存Bitmap的软引用到HashMap
- public void addBitmapToCache(String path) {
- //强引用的Bitmap对象
- Bitmap bitmap = BitmapFactory.decodeFile(path);
- //软引用的Bitmap对象
- SoftReference < Bitmap > softBitmap = new SoftReference < Bitmap > (bitmap);
- //添加该对象到Map中使其缓存
- imageCache.put(path, softBitmap);
- }
- //获取的时候,可以通过SoftReference的get()方法得到Bitmap对象
- public Bitmap getBitmapByPath(String path) {
- //从缓存中取软引用的Bitmap对象
- SoftReference < Bitmap > softBitmap = imageCache.get(path);
- //判断是否存在软引用
- if (softBitmap == null) {
- return null;
- }
- //通过软引用取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空,如果未被回收,
- //则可重复使用,提高速度。
- Bitmap bitmap = softBitmap.get();
- return bitmap;
- }
- }
使用软引用以后,在 OutOfMemory 异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免 Crash 发生。
如果只是想避免 OutOfMemory 异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
所以我们得出内存泄漏的原因:堆内存中的长生命周期的对象持有短生命周期对象的强 / 软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是 Java 中内存泄露的根本原因。
3. 内存泄漏的检测
Allocation Tracker 操作
1. 首先进入你要追踪的界面
2. 点击 Start Tracking 按钮,开始跟踪内存分配轨迹
3. 操作你的界面,尽量时间短点
4. 点击 Get Allocations 按钮,抓去内存分配轨迹信息,显示在右边的面板中,默认以内存大小排序,你可以以分配顺序排序或者仍以列排序。
5.logcat 中会显示出这次的轨迹共抓到内存分配轨迹记录数,可以简单的理解分配了多少次内存,这个数值和 Alloc order 的最大值是相等的
6. 如果你不想看那么多乱七八糟的,你可以使用 Filter 来过滤,输入包名就可以了。
跟踪内存轨迹
如果这个时候我们想单独获取某次操作的内存轨迹,首先一定要记得 Stop Tracking 再 Start Tracking 一下,让追踪点初始化一下,然后就进行我们需要观察内存变化的操作,然后点击 Get Allocations,这个时候我们从首页进入一个详情页, 看一下我们的内存分配轨迹:
上图中,我们可以看出来,在第 635 次内存分配中,分配的是 IntroduceFragment 对象,占用内存 224 字节,处理线程 Id 为 3245,在 java.lang.Class 的 newInstance 方法中分配的。从 trace 信息可以看出来该方法一步一步被调用的信息。
无论是 MAT 工具的内存分析,还是 AndroidStudio 中自带的分析工具亦或是 LeakCanary,原理都是一样的,都是 dump java heap 出来进行分析,找到泄漏的问题,只是 LeakCanary 帮我们把分析的工作做了。但值得一提的是,LeakCanary 并不是万能的,有些内存泄漏,它也无法检测出来。
集成后什么都不用做,按照正常测试,当有内存泄漏发生后,应用会通过系统通知栏发出通知,点击通知就可以进入查看内存泄漏的具体信息。在这里举个实践中的例子。把 LeakCanary 集成到项目中后,等 App 启动后一会,系统通知到了,点击通知,跳转到泄漏的详情页面进行查看:
LeakCanary 是 Square 开源了一个内存泄露自动探测神器 。这是项目的 github 仓库地址: 。使用非常简单,在 build.gradle 中引入包依赖:
那么通过上面 DDMS 工具,现在我们已经可以比较轻松地发现应用程序中是否存在内存泄露的现象了。
到此为止需检测的进程就可以被监视。
点击 Heap 视图中的 "Cause GC" 按钮
点击 Heap 视图
点击 Devices 视图界面中最上方一排图标中的 "Update Heap"
在 Devices 中,点击要监控的程序。
来源: http://www.bubuko.com/infodetail-1972710.html