当前位置: 首页 > 科技观察

内存泄漏基础知识从入门到精通三部曲

时间:2023-03-13 03:19:25 科技观察

1.首先从一个内存泄漏实例开始本节基本概念的内容:例1:(单例导致内存对象释放失败而泄漏)可以看出工具类ImageUtil是一个单例,引用了activity的上下文。试想这个场景,应用起来后,翻屏。翻屏后会销毁旧的MainActivity,重建新的MainActivity,导致单例ImageUtil再次getInstance。不幸的是,由于实例不再为空,ImageUtil不会被重建,它仍然持有之前的Context,也就是之前的MainActivity实例的上下文,这将导致两个问题:功能问题:使用ImageUitl访问上下文时出现异常相关内容时可能会出现(因为当前上下文不是当前活动的上下文);内存泄漏:旧的context被一个生命周期较长的静态变量持有,activity无法释放,导致泄漏!(所以静态变量很容易造成内存泄漏!)使用工具可以看到ImageUtil引用了MainActivity,导致mainActivity常驻内存泄漏。备注:本系列中部分概念和示例引用自网络。二、内存泄漏,我们要研究的泄漏对象到底是什么?首先我们了解一下程序运行时需要的内存分配策略:根据编译原理,程序运行时的内存分配策略有三种,分别是static、stack和heap。对应的,有三种存储策略使用的内存空间,主要是静态存储区(也叫方法区)、堆区和栈区。它们的功能不同,使用方法也不同。静态存储区(方法区):在程序编译的时候就已经分配了内存,这块内存在程序的整个运行过程中都存在。主要存放静态数据、全局静态数据和常量。栈区:函数执行时,可以在栈上创建函数中局部变量的存储单元,函数执行结束时自动释放这些存储单元。栈内存分配操作内置于处理器的指令集中,效率很高,但分配的内存容量有限。堆区:又称动态内存分配。程序运行时,使用malloc或new申请任意大小的内存,程序员负责在适当的时候用free或delete释放内存(Java依赖垃圾收集器)。动态内存的寿命可以由我们决定。如果我们不释放内存,程序最后会释放动态内存。然而,良好的编程习惯是:如果不再使用动态内存,则需要将其释放。接下来重点说一下堆和栈的区别:函数中定义的一些基本类型的变量和对象引用变量(表示是局部变量)是在函数的栈内存中分配的。当在代码块中定义一个变量时,java会在栈上为这个变量分配内存空间。当超出变量的作用域时,java会自动释放为该变量分配的内存空间,该内存空间可以立即用于其他用途。堆内存用于存放new创建的所有对象(包括对象的所有成员变量)和数组。堆中分配的内存由java虚拟机自动垃圾收集器管理。在堆中生成一个数组或对象后,还可以在栈中定义一个特殊的变量。该变量的值等于数组或对象在堆内存中的首地址。栈中的这个特殊变量一旦设置了数组或对象的引用变量,程序中就可以使用栈内存中的引用变量来访问堆中的数组或对象。引用变量相当于数组或对象的别名或代号。堆是一个不连续的内存区域(因为系统使用链表来存储空闲内存地址,自然是不连续的),堆的大小受限于计算机系统中的有效虚拟内存(32bit系统理论上是4G),所以堆空间更灵活更大。堆栈是一块连续的内存区域,大小由操作系统预先确定,Windows下堆栈大小为2M(或1M,编译时确定,VC中可以设置)。对于堆,频繁的new/delete会造成大量的内存碎片,降低程序效率。对于栈来说,是一个先进后出的队列,入口和出口一一对应,不分片,运行稳定且效率高。举个关于变量存储位置的例子2:结论:局部变量的基本数据类型和引用存储在栈中,引用的对象实体存储在堆中。——因为它们在方法中属于变量,所以生命周期以方法结束。所有成员变量都存储在堆中(包括基本数据类型、引用和被引用的对象实体)——因为它们属于一个类,类对象最终被new使用。回到我们的问题:什么是内存泄漏问题?我们这里所说的内存泄漏,是针对且仅针对堆内存的。它们存储的是引用指向的对象实体。3、那么第二个问题,为什么会内存泄漏呢?为了判断Java是否存在内存泄漏,首先要了解Java是如何管理(堆)内存的。Java的内存管理就是对象的分配和释放。在Java中,内存的分配是由程序完成的,内存的释放是由垃圾收集器(GarbageCollection,GC)完成的。程序员不需要调用函数来释放内存,但它只能回收无用且不再被其他对象引用的那些对象占用的空间。Java的内存垃圾回收机制是从程序的主要运行对象(如静态对象/寄存器/堆内存对象指向的栈上等)开始检查引用链,遍历后得到上述不可回收对象及其引用的对象对象链构成了不可回收对象的集合,而其他孤立对象(集合)则作为垃圾进行收集。为了能够正确的释放对象,GC必须监控每个对象的运行状态,包括对象的申请、引用、引用、赋值等,GC需要监控。监控对象状态的目的是为了更准确及时地释放对象,而释放对象的根本原则是对象不再被引用。在Java中,这些无用的对象被GC回收,所以程序员不需要考虑这部分内存泄漏。虽然我们有几个可以访问GC的函数,比如运行GC的函数System.gc(),但是根据Java语言规范的定义,这个函数并不能保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法来管理GC。通常GC线程具有较低的优先级。JVM调用GC的策略也有很多。当内存使用达到一定水平时,其中一些开始工作。但一般来说,我们不需要关心这些。至此,我们来看看Java中需要收集的垃圾:{Personp1=newPerson();...}引用句柄p1的范围是从定义到“}”,执行完所有代码在大括号中,生成的Person对象会变成垃圾,因为引用这个对象的句柄p1已经超出了它的范围,p1在栈中失效并被销毁,所以堆上的Person对象不再被任何句柄引用。所以人变成了垃圾,会被回收利用。从上面的例子和解释中,我们可以看出一个非常关键的词:参考。通俗地说,如果A可以调用访问B,则说明A持有对B的引用,或者说A是对B的引用,B的引用计数+1。(1)例如Personp1=newPerson();可以通过P1Person对象进行操作,所以P1是对Person的引用;(2)比如类O中有一个成员变量是类I的对象,那么我们可以用o.i来访问类I对象的成员,所以o持有对象i的引用。GC进程与对象的引用类型严重相关。我们来看看Java对引用的分类:StrongReference、SoftReference、WeakReference和PhatomReference。软引用/弱引用通常在这里做什么?在Android应用程序开发中,为了防止内存溢出,在处理一些占用内存大、生命周期长的对象时,可以尽量应用软引用和弱引用技术。软/弱引用可以与引用队列(ReferenceQueue)结合使用。如果软引用引用的对象被垃圾回收器回收,Java虚拟机会将软引用添加到与其关联的引用队列中。这个队列可以用来获知已经被回收的软/弱引用的对象列表,从而为缓冲区清除无效的软/弱引用。假设我们的应用会用到大量的默认图片,比如应用中的默认头像、默认游戏图标等,这些图片会在很多地方用到。如果每次都读取图片,因为读取文件需要硬件操作,速度慢,会导致性能低下。所以我们考虑把图片缓存起来,需要的时候直接从内存中读取。但是由于图片占用内存空间比较大,缓存很多图片需要占用大量内存,更容易出现OutOfMemory异常。这时候我们可以考虑使用软/弱引用技术来避免这个问题。下面是缓存的原型:首先定义一个HashMap来保存软引用对象。1.privateMap>imageCache=newHashMap>();定义一个方法将Bitmap的软引用保存到HashMap中。publicclassCacheBySoftRef{//首先定义一个HashMap来保存软引用对象。privateMap>imageCache=newHashMap>();//定义一个方法将Bitmap的软引用保存到HashMap中。publicvoidaddBitmapToCache(Stringpath){//强引用Bitmap对象Bitmapbitmap=BitmapFactory.decodeFile(path);//软引用Bitmap对象SoftReferencesoftBitmap=newSoftReference(bitmap);//将这个对象添加到Map中itcacheimageCache.put(path,softBitmap);}//获取时可以通过SoftReference的get()方法获取Bitmap对象。publicBitmapgetBitmapByPath(Stringpath){//从缓存中获取软引用的Bitmap对象SoftReferencesoftBitmap=imageCache.get(path);//判断是否有软引用if(softBitmap==null){returnnull;}//Pass软引用取出Bitmap对象。如果Bitmap因内存不足而被回收,则为空。如果没有回收,可以重复使用,提高速度。Bitmapbitmap=softBitmap.get();returnbitmap;}}使用软引用后,在OutOfMemory异常发生之前,可以释放这些缓存图片资源的内存空间,从而避免内存达到上限,避免Crash。如果你只是想避免OutOfMemory异常,你可以使用软引用。如果你比较关心应用程序的性能,希望尽快回收一些占用大量内存的对象,可以使用弱引用。另外可以根据对象是否被频繁使用来判断选择软引用还是弱引用。如果对象可能会被频繁使用,尽量使用软引用。如果更有可能不使用该对象,则可以使用弱引用。回到我们的问题,为什么会内存泄漏?堆内存中的长寿命对象持有对短寿命对象的强/软引用。尽管不再需要短寿命对象,但它们不能被回收,因为长寿命对象持有它们的引用。这是Java中内存泄漏的根本原因。腾讯Bugly,国内专业的崩溃监控统计平台,为开发者提供NDK错误支持、问题精准定位、ANR上报、自定义日志上报、智能合并分析、问题实时上报等多项服务,减少用户使用损失。