内存泄露 - CSDN
  • 什么是内存泄露? 简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它...

    来源:https://blog.csdn.net/u014796694/article/details/80795372

    什么是内存泄露?

    简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

    2、如何检测内存泄露

    第一:良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。

    第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

    第三:Boost 中的smart pointer。

    第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等。

    检测方法:

    无论是C还是C++程序,运行时候的变量主要有三种分配方式:堆分配、栈分配、全局和静态内存分配。内存泄漏主要是发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”,若缺乏语言这样的垃圾回收机制,这样的内存片就无法归还系统。因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。下面将介绍几种常用的内存检测方法,每种方法均以现有的内存检测工具为分析范例,并对各种方法进行比较。

    静态分析技术

    静态分析技术就是直接分析程序的源代码或机器代码,获得一些有用的信息,而并不运行程序本身。目前有许多静态分析的工具,编译器就属于这一类,它读入源程序代码,对源程序进行词法和语法分析,进行数据类型的检查以及一些优化的分析等,以此来提高程序的质量与运行效率。这类静态的分析工具仅仅是读入程序代码进行相关的分析,而并不进行其它额外的操作,如修改源程序代码等。

    LCLink是一种通过对源代码及添加到源代码中特定格式的注释说明进行静态分析的程序理解和检错工具,的检查对象是源程序,能检查出的内存错误有内存分配释放故障、空指针的错误使用、使用未定义或己被释放的内存等程序错误。

    LCLink重点分析两类内存释放错误:

    • 试图释放某内存块,该内存块有两个或两个以上的有效指针指向它。

    • 试图释放某内存块,该内存块没有任何有效指针指向它。

    解决此类内存错误的方法是规定分配某块内存时返回的指针必须释放该内存。使用注释表示某指针是唯一指向某内存块的指针,使用注释表示被调用函数可能释放函数参数指向的内存块或创建新的指针指向该内存块。

    源代码插装技术

    为了获得被测程序的动态执行信息,需要对其进行跟踪,一般使用插装方法。所谓插装就是在保持被测程序的逻辑完整性的基础上,在被测程序的特定部位插入一段检测程序又称探针函数,通过探针的执行抛出程序的运行特征数据。基于这些特征数据分析,可以获得程序的控制流及数据流信息,进而获得逻辑覆盖等动态信息,这样就可以在被测程序执行的过程中动态地同步执行程序的检测工作。插装方法又分为源代码级程序插装和目标代码级程序插装。

    源代码插装测试必须在静态测试部分获得的被测程序的结构信息、静态数据信息、控制流信息等基础上,应用插装技术向被测程序中的适当位置植入相应类型的探针,通过运行带有探针的被测程序而获得程序运行的动态数据。源代码插装要通过运行被测程序来测定程序的各种指标,如覆盖率、时间性能、内存使用等等,实现源代码插装的关键技术是借助于插入到源程序中的监控语句来收集执行信息,以达到揭示程序内部行为和特性的目的,如图所示。

    基于源代码插装的动态测试框架分为个主要的阶段:

    • 插装交互与动态测试信息分析;

    • 插装阶段;

    • 插装库制作阶段;

    • 测试实施阶段。

    插装交互与动态测试信息分析是软件测试工具与用户交互的界面。用户通过该界面选择要进行动态测试的程序模块,拓扑产生相应的插装选择记录文件。用户还可以通过该交互界而浏览动态测试结果信息,在软件测试工具的实现上,采用可视化的方式显示这些动态信息。插装阶段实现了在被测程序中植入探针,并生成带有插装信息的源文件。在此过程中,首先将被测程序经过预处理展开为不包含宏、条件编译和头文件的文件格式。然后,按照一定的插装策略,根据前面生成的插装选择记录文件,将探针函数加载到该文件中,最后生成插装后的程序。插装库制作阶段的目的是生成插装库中的探针函数,它含有插装语句调用的函数及其函数的定义。显然,插装过程中生成的目标文件中含有探针函数的桩,而探针函数的实现恰恰在本过程完成。需要指出的是,插装库的制作过程是独立于动态测试过程之外的,可以与软件测试工具开发同步。测试实施阶段将插装过程生成的文件与插装库制作过程生成的插装静态库连接生成带有插装信息的可执行文件,选取测试用例,运行该程序,可以获得被测程序的动态跟踪信息。

    在以上四个阶段中,其中的插装交互与动态测试信息分析与测试实施阶段是测试人员的可视部分,通过这两部分,用户与系统交互,完成测试工作。而插装阶段与插装库制作阶段对测试人员是不可见的,在后台完成,对于用户而言,这两部分是完全透明的。在性能方面,采用插装方法应尽量减少插装开销。为了达到不同的统计目的如语句覆盖、分支覆盖等,应尽量减少插装次数。若能仅仅插装一次就能完成多种类型的统计,则可使插装代码得到优化。此外,应尽量减少插装代码的数量,减少插装代码的运行次数,从而达到减小插装代码运行开销的目的。特别是对于一些实时系统的测试,在这方面的要求尤为苛刻。一个运行时错误检测工具,能够自动检测一应用中大量的编程和运行时错误。通过使用源码插装和运行时指针跟踪的专利技术,在编译时,附十插入测试和分析代码,它建立一个有关程序中各种对象的数据库。然后在运行时通过检查数据值和内存引用验证对象的一致性和正确性。使用这些技术,包括变异测试技术等,一能够检查和测试用户的代码,精确定位错误的准确位置并给出详细的诊断信息。十十能够可视化实时内存操作,优化内存算法。还能执行覆盖性分析,清楚地指示那些代码己经测试过。将集成到开发环境中,能够极大地减少调试时间并有效地防止错误。检验每一次内存操作的有效性,包括静态全局和堆栈以及动态分配内存的操作。叶有两种运行模式。监护模式下用户可以快速检测代码中的错误,不需要对代码作任何插装和处理源码插装模式则进行彻底地代码检测。

    目标代码插装技术

    目标代码插装实现主要分为预处理、测试执行和结果汇总个阶段,工作流程如图所示,系统主要工作是围绕断点而进行的。在预处理阶段,首先静态分析被测程序的目标代码,查找待测程序中源代码各语句、函数入口点在目标代码中的对`应位置,然后在相应位置插入断点在测试执行阶段,启动调试进程,当被测程序执行到断点处时,响应断点信息,在相应的断点处完成相应的统计操作在结果汇总阶段,根据各断点处的统计结果,按不同的统计角度进行归并、综合得到最终的统计数据。

    被测代码预处理

    在测试预处理阶段对被测程序的目标代码进行分析,可以获得目标代码与源代码中语句、函数的对应关系。在目标代码中为相对应的源代码的每条语句及每个函数的入口点插入断点。对于第三方代码,只要其目标代码格式与下生成的目标代码格式一致,我们就可以用与分析用户代码同样的方法获取信息。获取断点的信息后,为所有的断点建立断点链表,同时建立语句及函数的信息链表,供随后的测试执行阶段存储信息。预处理流程如图所示。

    测试执行阶段

    利用OCI技术,我们把测试执行看作是一个在被测进程和检测进程间不断切换的过程。每当被测进程遇到断点,就会将自身挂起,同时发送消息唤醒检测进程,检测进程根据当前断点的地址在断点链表中查找相应节点,并查找对应的语句或函数信息,记录该语句或函数的执行次数、到达或离开的时刻,供以后统计之用。然后,将插入的断点信息去除,恢复原来的指令,转入被测进程继续执行。在转入被测进程之前,必须将上一个断点处的断点恢复上一个断点处的断点在指令运行时被去除了。具体流程如图所示。

    数据统计与结果汇总

    根据各断点处的统计结果,按不同的统计角度进行归并、综合,进行覆盖率及各种时间的计算,得到最终的统计数据。是公司出品的一种软件测试和质量保证工具,它能检测程序内存泄漏和内存访问冲突等错误。使用目标码插装技术,在编译器生成的目标码中直接插入特殊的检查指令实现对内存错误的检测。在程序的所有代码中插入这些检查逻辑,包括第三方目标码库,并且验证系统调用的接口。目标码插装技术分为链接前插装和链接后插装两种插装方法。

    使用如图所示的链接前插装法。检查插装后程序的每个内存读写动作,跟踪内存使用情况,使用类似垃圾收集器的技术来检查内存泄漏。垃圾收集机制分为两阶段垃圾检测和垃圾回收。为了不影响程序的执行速度,提供了一个可调用的垃圾检测器,使用类似于保守式垃圾收集算法,即标记一清除算法。在标记阶段,递归地从数据段、堆栈段到数据堆跟踪分析指针,并使用标准保守式方法为所有被引用的内存块做标记。在清除阶段,逐步访问数据堆,并报告已分配但程序不再引用的内存块,即程序的内存泄漏。

    检测工具

    编辑

    部分工具

    1.ccmalloc-Linux和Solaris下对C和C++程序的简单的使用内存泄漏和malloc调试库。

    2.Dmalloc-Debug Malloc Library.

    3.Electric Fence-Linux分发版中由Bruce Perens编写的malloc()调试库。

    4.Leaky-Linux下检测内存泄漏的程序。

    5.LeakTracer-Linux、Solaris和HP-UX下跟踪和分析C++程序中的内存泄漏。

    6.MEMWATCH-由Johan Lindh编写,是一个开放源代码C语言内存错误检测工具,主要是通过gcc的precessor来进行。

    7.Valgrind-Debugging and profiling Linux programs, aiming at programs written in C and C++.

    8.KCachegrind-A visualization tool for the profiling data generated by Cachegrind and Calltree.

    9.IBM Rational PurifyPlus-帮助开发人员查明C/C++、托管.NET、Java和VB6代码中的性能和可靠性错误。PurifyPlus 将内存错误和泄漏检测、应用程序性能描述、代码覆盖分析等功能组合在一个单一、完整的工具包中。

    10.ParasoftInsure++-针对C/C++应用的运行时错误自动检测工具,它能够自动监测C/C++程序,发现其中存在着的内存破坏、内存泄漏、指针错误和I/O等错误。并通过使用一系列独特的技术(SCI技术和变异测试等),彻底的检查和测试我们的代码,精确定位错误的准确位置并给出详细的诊断信息。能作为MicrosoftVisual C++的一个插件运行。

    11.Compuware DevPartner for Visual C++ BoundsChecker Suite-为C++开发者设计的运行错误检测和调试工具软件。作为Microsoft Visual Studio和C++ 6.0的一个插件运行。

    12.Electric Software GlowCode-包括内存泄漏检查,code profiler,函数调用跟踪等功能。给C++和.Net开发者提供完整的错误诊断,和运行时性能分析工具包。

    13.Compuware DevPartner Java Edition-包含Java内存检测,代码覆盖率测试,代码性能测试,线程死锁,分布式应用等几大功能模块。

    14.Quest JProbe-分析Java的内存泄漏。

    15.ej-technologies JProfiler-一个全功能的Java剖析工具,专用于分析J2SE和J2EE应用程序。它把CPU、执行绪和内存的剖析组合在一个强大的应用中。

    16.BEAJRockit-用来诊断Java内存泄漏并指出根本原因,专门针对Intel平台并得到优化,能在Intel硬件上获得最高的性能。

    展开全文
  • 一、内存泄露如何产生?...具体主要有如下几大类:1、静态集合类引起内存泄漏:像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Ob...

    一、内存泄露如何产生?

    Java内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。

    具体主要有如下几大类:

    1、静态集合类引起内存泄漏:

    像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

    Static Vector v = new Vector(10);
    for (int i = 1; i<100; i++)
    {
    Object o = new Object();
    v.add(o);
    o = null;
    }

    循环申请Object 对象,并将所申请的对象放入静态的Vector v 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。

    2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用,此时造成内存泄露

    3、监听器

    在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

    4、各种连接

    比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

    5、内部类和外部模块的引用

    内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,内部类是否提供相应的操作去除外部引用。

    6、单例模式

    由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么外部对象将不能被JVM正常回收,导致内存泄漏。

    二、常见的内存泄露的处理方式

    1、集合类泄露

    集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。因此,在编写代码的时候,集合类需要有成对出现添加和删除或者清空的操作。

    2、单例造成的泄露

    public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
    this.context = context;
    }
    public static AppManager getInstance(Context context) {
    if (instance == null) {
    instance = new AppManager(context);
    }
    return instance;
    }
    }
    
    这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
    
    1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
    
    2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

    还是利用静态变量的生命周期与应用的生命周期一致的特性,进行单例修改,从而避免内存泄露 方法如下

        //方式一
        public class AppManager {
            private static AppManager instance;
            private Context context;
    
            private AppManager(Context context) {
                this.context = context.getApplicationContext();// 使用Application 的context
            }
    
            public static AppManager getInstance(Context context) {
                if (instance == null) {
                    instance = new AppManager(context);
                }
                return instance;
            }
        }
         
        //方式二 在你的 Application 中添加一个静态方法,getContext() 返回Application 的 context
    
        public class AppManager {
            private static AppManager instance;
            private Context context;
    
            private AppManager() {
                this.context = MyApplication.getContext();// 使用Application 的context
            }
    
            public static AppManager getInstance() {
                if (instance == null) {
                    instance = new AppManager();
                }
                return instance;
            }
        }

    3、非静态内部类创建静态实例造成的内存泄漏

        public class MainActivity extends AppCompatActivity {
            private static TestResource mResource = null;
    
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                if (mManager == null) {
                    mManager = new TestResource();
                }
                //...
            }
    
            class TestResource {
                //...
            }
        }

    在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

    正确的做法为:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,

    4、匿名内部类/异步线程


        public class MainActivity extends AppCompatActivity {
    
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
    
                new Thread(new MyRunnable()).start();
                new MyAsyncTask(this).execute();
            }
    
            class MyAsyncTask extends AsyncTask<Void, Void, Void> {
    
                // ...
    
                public MyAsyncTask(Context context) {
                    // ...
                }
    
                @Override
                protected Void doInBackground(Void... params) {
                    // ...
                    return null;
                }
    
                @Override
                protected void onPostExecute(Void aVoid) {
                    // ...
                }
            }
    
            class MyRunnable implements Runnable {
                @Override
                public void run() {
                    // ...
                }
            }
        }
    AsyncTask和Runnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。
    解决方法:将AsyncTask和Runnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏

    5、Handle造成的内存泄露
    由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放

        public class SampleActivity extends Activity {
    
            private final Handler mLeakyHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    // ...
                }
            }
    
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
    
                // Post a message and delay its execution for 10 minutes.
                mLeakyHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() { /* ... */ }
                }, 1000 * 60 * 10);
    
                // Go back to the previous Activity.
                finish();
            }
        }
    从Android的角度
    当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。当主线程中实例化一个Handler对象后,它就会自动与主线程Looper的MessageQueue关联起来。所有发送到MessageQueue的Messag都会持有Handler的引用,所以Looper会据此回调Handle的handleMessage()方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。另外,主线程的Looper对象会伴随该应用程序的整个生命周期。
     从Java角度
    在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。

    当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

    修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:
    public class SampleActivity extends Activity {
    
      /**
       * Instances of static inner classes do not hold an implicit
       * reference to their outer class.
       */
      private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;
    
        public MyHandler(SampleActivity activity) {
          mActivity = new WeakReference<SampleActivity>(activity);
        }
    
        @Override
        public void handleMessage(Message msg) {
          SampleActivity activity = mActivity.get();
          if (activity != null) {
            // ...
          }
        }
      }
    
      private final MyHandler mHandler = new MyHandler(this);
    
      /**
       * Instances of anonymous classes do not hold an implicit
       * reference to their outer class when they are "static".
       */
      private static final Runnable sRunnable = new Runnable() {
          @Override
          public void run() { /* ... */ }
      };
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
    
        // Go back to the previous Activity.
        finish();
      }
    }
    即推荐使用静态内部类 + WeakReference 这种方式

    6、避免使用static成员变量
    7、资源未关闭造成的内存泄露

    对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

    1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。
    2)资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
    3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
    4)Bitmap对象不在使用时调用recycle()释放内存。2.3以后的bitmap应该是不需要手动recycle了,内存已经在java层了。

    8、listview没用复用contentview造成的泄露

    初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象(初始化时缓存中没有View对象则convertView是null)。

    构造Adapter时,没有使用缓存的convertView。
    解决方法:在构造Adapter时,使用缓存的convertView。

    9、webview 造成的内存泄露

    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。
    解决方法:为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

    三、避免内存泄露
    1、在涉及使用Context时,对于生命周期比Activity长的对象应该使用Application的Context。但要注意context的应用场景
    2、对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。
    3、对于不再需要使用的对象,将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。
    4、保持对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
    5、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,如下方法可以避免内存泄漏:
    1)将内部类改为静态内部类
    2)静态内部类中使用弱引用来引用外部类的成员变量
    展开全文
  • (Memory Leak,内存泄漏) 为什么会产生内存泄漏? 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就...

    (Memory Leak,内存泄漏)

    为什么会产生内存泄漏?

    当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

    内存泄漏对程序的影响?

    内存泄漏是造成应用程序OOM的主要原因之一。我们知道Android系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出从而导致应用Crash。

    如何检查和分析内存泄漏?

    因为内存泄漏是在堆内存中,所以对我们来说并不是可见的。通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
    1、MAT是一款强大的内存分析工具,功能繁多而复杂。
    2、LeakCanary则是由Square开源的一款轻量级的第三方内存泄漏检测工具,当检测到程序中产生内存泄漏时,它将以最直观的方式告诉我们哪里产生了内存泄漏和导致谁泄漏了而不能被回收。

    常见的内存泄漏及解决方法

    1、单例造成的内存泄漏

    由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
    示例:防止单例导致内存泄漏的实例

    // 使用了单例模式
    public class AppManager {
        private static AppManager instance;
        private Context context;
        private AppManager(Context context) {
            this.context = context;
        }
        public static AppManager getInstance(Context context) {
            if (instance != null) {
                instance = new AppManager(context);
            }
            return instance;
        }
    }
    

    这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。???

    2、非静态内部类创建静态实例造成的内存泄漏

    例如,有时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现如下写法:

    public class MainActivity extends AppCompatActivity {
    
        private static TestResource mResource = null;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if(mResource == null){
                mResource = new TestResource();
            }
            //...
        }
    
        class TestResource {
        //...
        }
    }
    

    这样在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据。虽然这样避免了资源的重复创建,但是这种写法却会造成内存泄漏。因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。
    解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。

    3、Handler造成的内存泄漏

    示例:创建匿名内部类的静态对象

    public class MainActivity extends AppCompatActivity {
    
        private final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                // ...
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // ...
                    handler.sendEmptyMessage(0x123);
                }
            });
        }
    }
    

    1、从Android的角度
    当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。当主线程中实例化一个Handler对象后,它就会自动与主线程Looper的MessageQueue关联起来。所有发送到MessageQueue的Messag都会持有Handler的引用,所以Looper会据此回调Handle的handleMessage()方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。另外,主线程的Looper对象会伴随该应用程序的整个生命周期。
    2、 Java角度
    在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。

    对上述的示例进行分析,当MainActivity结束时,未处理的消息持有handler的引用,而handler又持有它所属的外部类也就是MainActivity的引用。这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。
    解决方法:将Handler类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

    4、线程造成的内存泄漏

    示例:AsyncTask和Runnable

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Thread(new MyRunnable()).start();
            new MyAsyncTask(this).execute();
        }
    
        class MyAsyncTask extends AsyncTask<Void, Void, Void> {
    
            // ...
    
            public MyAsyncTask(Context context) {
                // ...
            }
    
            @Override
            protected Void doInBackground(Void... params) {
                // ...
                return null;
            }
    
            @Override
            protected void onPostExecute(Void aVoid) {
                // ...
            }
        }
    
        class MyRunnable implements Runnable {
            @Override
            public void run() {
                // ...
            }
        }
    }
    

    AsyncTask和Runnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。
    解决方法:将AsyncTask和Runnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

    5、资源未关闭造成的内存泄漏

    对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。

    1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。
    2)资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
    3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
    4)Bitmap对象不在使用时调用recycle()释放内存。2.3以后的bitmap应该是不需要手动recycle了,内存已经在java层了。

    6、使用ListView时造成的内存泄漏

    初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象(初始化时缓存中没有View对象则convertView是null)。

    构造Adapter时,没有使用缓存的convertView。
    解决方法:在构造Adapter时,使用缓存的convertView。

    7、集合容器中的内存泄露

    我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
    解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

    8、WebView造成的泄露

    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。
    解决方法:为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

    如何避免内存泄漏?

    1、在涉及使用Context时,对于生命周期比Activity长的对象应该使用Application的Context。凡是使用Context优先考虑Application的Context,当然它并不是万能的,对于有些地方则必须使用Activity的Context。对于Application,Service,Activity三者的Context的应用场景如下:



    其中,NO1表示Application和Service可以启动一个Activity,不过需要创建一个新的task任务队列。而对于Dialog而言,只有在Activity中才能创建。除此之外三者都可以使用。

    2、对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。
    3、对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。
    4、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
    5、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
    1)将内部类改为静态内部类
    2)静态内部类中使用弱引用来引用外部类的成员变量

    参考

    Android内存泄漏总结

    展开全文
  • 先考虑一种情况,对一个已知对象进行拷贝,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。 //main.cpp #include #include "student.h" ...

    先考虑一种情况,对一个已知对象进行拷贝,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数

    复制代码
    //main.cpp
    
    #include <iostream>
    #include "student.h"
    int main()
    {
             Student s1;
             Student s2(s1);//Student s2 = s1;//复制对象
    
             return 0;
    }
    复制代码
    复制代码
    //student.h
    
    #ifndef STUDENT_H
    #define STUDENT_H
    class Student
    {
             private:
                   int num;
                   char *name;
             public:
                    Student();
                    ~Student();
    };
    
    #endif
    复制代码
    复制代码
    //student.cpp
    
    #include "student.h"
    #include <iostream>
    using namespace std;
    
    Student::Student()
    {
           name = new char(20);
           cout << "Student" << endl;
    
    }
    Student::~Student()
    {
            cout << "~Student " << (int)name << endl;
            delete name;
            name = NULL;
    }
    复制代码

    执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致什么问题呢?

     

    name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会造成内存泄漏问题!

    这是由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间。

     

    所以,在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。

     自己定义拷贝构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //student.h
     
    #ifndef STUDENT_H
    #define STUDENT_H
    class Student
    {
           private:
                 int num;
                 char *name;
          public:
                  Student();//构造函数
                   ~Student();//析构函数
                   Student(const Student &s);//拷贝构造函数,const防止对象被改变
    };
     
    #endif

      

    复制代码
    //student.cpp
    
    #include "student.h"
    #include <iostream>
    #include <string.h>
    using namespace std;
    
    Student::Student()
    {
          name = new char(20);
          cout << "Student " << endl;
    }
    
    Student::~Student()
    {
             cout << "~Student " << (int)name << endl;
             delete name;
             name = NULL;
    }
    
    Student::Student(const Student &s)
    {
             name = new char(20);
             memcpy(name, s.name, strlen(s.name));
             cout << "copy Student " << endl;
    }
    复制代码

    执行结果:调用一次构造函数,一次自定义拷贝构造函数,两次析构函数。两个对象的指针成员所指内存不同。

     

    总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

    再说几句:

    当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:

    1.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;

    2.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。

     




    1. 在类的构造函数和析构函数中没有匹配的调用newdelete函数

    两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;二是在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存

    2. 没有正确地清除嵌套的对象指针

    3. 在释放对象数组时在delete中没有使用方括号

    方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值并调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄露。

    释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数

    4. 指向对象的指针数组不等同于对象数组

    对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间

    指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了。

    5. 缺少拷贝构造函数

    两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。

    按值传递会调用(拷贝)构造函数,引用传递不会调用

    C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。

    所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符

     C++中构造函数,拷贝构造函数和赋值函数的区别和实现参见:http://www.cnblogs.com/liushui-sky/p/7728902.html

    6. 缺少重载赋值运算符

    这种问题跟上述问题类似,也是逐个成员拷贝的方式复制对象,如果这个类的大小是可变的,那么结果就是造成内存泄露,如下图:

     

    7. 关于nonmodifying运算符重载的常见迷思

    a. 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针

    b. 返回内部静态对象的引用。

    c. 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收

    解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int

    8. 没有将基类的析构函数定义为虚函数

    当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露

     

    野指针:指向被释放的或者访问受限内存的指针。

    造成野指针的原因:

    1. 指针变量没有被初始化(如果值不定,可以初始化为NULL
    2. 指针被free或者delete后,没有置为NULL, freedelete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL.
    3. 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。
    展开全文
  • 具体可见
  • 1.简介在整个Android开发过程中,内存泄露是导致OOM的一个重点因素。大意思就是:GC无法回收原本应该被回收的对象,这个对象就引发了内存泄露。那有什么危害呢?手机的内存大小是有限的,如果不能释放的话,你就无法...
  • 内存泄露是每个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其他应用问题。 什么是内存泄露? 本质上,内存泄露可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存...
  • 深入内存泄露

    2016-11-08 15:34:14
    Android应用的内存泄露,其实就是Java虚拟机的堆内存泄漏. 1.知识储备 1.Java内存模型   相关内存对象模型,参照博客精讲Java内存模型 1) 寄存器(register)。这是最快的保存区域,这是主要由于它位于...
  • 由于java的JVM引入了垃圾...那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。 1、静态集合类,如HashMap、LinkedList等等。如果这些...
  •  Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的...理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同。   JAVA 中的内存管理  ...
  • 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏内存泄漏对程序的影响? 内存泄漏...
  • 内存泄露调试

    2015-08-01 17:33:08
    内存泄露调试(Memory Leak Debug) 一、概述内存泄露产生原因存在多种,但常见分类如下: 1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。 2.偶发性内存泄漏。...
  • 内存泄漏 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。 一 . 以发生的方式来分类,内存泄漏可以分为4类:  ...
  • 一、内存溢出和内存泄露一种通俗的说法。1、内存溢出:你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,出现溢出。2、内存泄漏:你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放...
  • 内存泄漏概念和种类

    2019-04-21 20:10:01
    内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,如果放任内存泄漏不做处理,无论多少内存,迟早会被占光。 内存溢出 (out of ...
  • 内存泄漏的场景

    2020-02-17 22:31:28
    一、什么是内存泄漏在说内存泄露之前,需要先了解JVM的内存回收机制。1.JVM的内存回收机制大家都知道,Java是有垃圾回收机制的,这使得java程序员比C++程序员轻松了许多,存储申请了,不用心心念念要加一句释放,...
  • 内存泄漏与内存溢出的区别Java中的内存泄漏概念分类常发性内存泄露偶发性内存泄露一次性内存泄露隐式内存泄露发生的情况Java中的内存溢出概念发生的情况解决方法二者之间的区别 Java中的内存泄漏 概念 内存泄漏...
  • 内存泄露 :是指程序在申请内存后,无法释放已申请的内存空间就造成了内存泄漏,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。 我们知道了内存泄漏的原因而内存溢出则有可能是因为我们...
  • 一、常见JS内存泄漏 1.1 全局变量引起的内存泄漏  全局变量使用完毕没有置为null导致内存就无法回收。平常应注意不要引入意外的全局变量,比如定义变量记得加 var声明。  全局变量引发泄露的实例: &lt;...
  • 内存溢出,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;...内存泄露,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,
1 2 3 4 5 ... 20
收藏数 297,826
精华内容 119,130
热门标签
关键字:

内存泄露