什么是内存泄露

内存管理一直是Java 所鼓吹的强大优点。开发者只需要简单地创建对象,而Java的垃圾收集器将会自动管理内存空间的分配和释放。
但在很多情况下,事情并不那么简单,在 Java程序中总是会频繁地发生内存泄露(Memory Leaks)。
内存泄漏就是:当某些对象不再被应用程序所使用,但是由于仍然被引用而导致垃圾收集器不能释放他们。
或者说是:我们对某一内存空间使用完成后没有释放。
用白话来说就是:该回收的内存没被回收。
要理解这个定义,我们需要理解内存中的对象状态。下图展示了什么是不使用的部分,以及未被引用的部分。
从图中可以看出,内存中存在着"有引用的对象"和"无引用的对象"。无引用的对象将被垃圾收集器所回收,而有引用的对象则不会被当做垃圾收集。 
因为没有任何其他对象所引用,所以无引用对象一定是不再使用的,但是有一部分无用对象仍然被(无意中)引用着。这就是发生内存泄露的根源。

为什么会发生内存泄露

为什么内存会泄露?
让我们看下面的实例来了解为什么内存会泄露。 
在下面的情境中,对象A引用了对象B。 A的生命周期(t1 - t4) 比 B的(t2 - t3)要长得多。 当对象B在应用程序逻辑中不会再被使用以后, 对象 A 仍然持有着 B的引用。 (根据虚拟机规范)在这种情况下垃圾收集器不能将 B 从内存中释放,这种情况很可能会引起内存问题。甚至有可能 B 也持有一大堆其他对象的引用,这些对象由于被 B 所引用,也不会被垃圾收集器所回收。所有这些无用的对象将消耗大量宝贵的内存空间。
导致内存泄漏最主要的原因就是:某些长存对象持有了一些其它应该被回收的对象的引用,导致垃圾回收器无法去回收掉这些对象。

发生内存泄漏的常见场景

静态集合类的使用

像HashMap、ArrayList等的使用最容易出现内存泄露,由于静态变量的生命周期和应用程序一致,所以他们所引用的所有的对象也不能被释放。

public class Test {static ArrayList<Person> list = new ArrayList<Person>();//list中引用的对象在应用整个生命周期中都不会被释放public static void main(String[] args) {for (int i = 1; i < 10; i++) {Person person = new Person("" + i);list.add(person);person = null;//这行代码的作用仅仅是将【变量person】指向null(之前是指向堆内存中的Person对象),但是这时堆内存中的【Person对象】并不会被回收,因为它被list强引用了}for (Person person : list) {System.out.println(person);//所有Person对象都没有被回收!!!}}
}class Person {public String name;public Person(String name) {this.name = name;}@Overridepublic String toString() {return name;}
}
1
public class Test {

2
    static ArrayList<Person> list = new ArrayList<Person>();//list中引用的对象在应用整个生命周期中都不会被释放

3
    public static void main(String[] args) {

4
        for (int i = 1; i < 10; i++) {

5
            Person person = new Person("" + i);

6
            list.add(person);

7
            person = null;//这行代码的作用仅仅是将【变量person】指向null(之前是指向堆内存中的Person对象),但是这时堆内存中的【Person对象】并不会被回收,因为它被list强引用了

8
        }

9
        for (Person person : list) {

10
            System.out.println(person);//所有Person对象都没有被回收!!!

11
        }

12
    }

13
}

14
15
class Person {

16
    public String name;

17
    public Person(String name) {

18
        this.name = name;

19
    }

20
    @Override

21
    public String toString() {

22
        return name;

23
    }

24
}

在这个例子中,如果仅仅释放引用本身(person = null),那么集合仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到集合后,还必须从集合中删除(移除),最简单的方法就是将集合对象设置为null。

Listener、Receiver等监听器的引用

当注册的监听器不再使用后,如果没有被注销,那么很可能会发生内存泄露。
public class Test {public static void main(String[] args) {B b = new B();//被监听者,如ActivityA a = new A();//监听者,如BroadcastReceiverb.register(a);//在b中注册【a监听器】,通常register内部的操作是:将a的引用传给b的成员变量,进而长期持有a的引用a = null;//虽然将监听器a设为null,但是监听器对象A并没有被回收,因为它被B引用了b.show();//监听器工作了b.unregister();//用完要注销掉}
}interface Listener {public void listen();
}class A implements Listener {//监听者,如BroadcastReceiver@Overridepublic void listen() {System.out.println("监听器工作了");}
}class B {//被监听者,如ActivityListener listener;public void register(Listener listener) {//B持有了Listener的引用,除非在B内部将其设为null,否则listener将不会被回收this.listener = listener;}public void unregister() {listener = null;}public void show() {if (listener != null) listener.listen();}
}
34
34

1
public class Test {

2
    public static void main(String[] args) {

3
        B b = new B();//被监听者,如Activity

4
        A a = new A();//监听者,如BroadcastReceiver

5
        b.register(a);//在b中注册【a监听器】,通常register内部的操作是:将a的引用传给b的成员变量,进而长期持有a的引用

6
        a = null;//虽然将监听器a设为null,但是监听器对象A并没有被回收,因为它被B引用了

7
        b.show();//监听器工作了

8
        b.unregister();//用完要注销掉

9
    }

10
}

11
12
interface Listener {

13
    public void listen();

14
}

15
16
class A implements Listener {//监听者,如BroadcastReceiver

17
    @Override

18
    public void listen() {

19
        System.out.println("监听器工作了");

20
    }

21
}

22
23
class B {//被监听者,如Activity

24
    Listener listener;

25
    public void register(Listener listener) {//B持有了Listener的引用,除非在B内部将其设为null,否则listener将不会被回收

26
        this.listener = listener;

27
    }

28
    public void unregister() {

29
        listener = null;

30
    }

31
    public void show() {

32
        if (listener != null) listener.listen();

33
    }

34
}

上述只是逻辑十分清楚、简单的监听,对于关系复杂的监听,会导致更多更严重的问题。比如当我们在Activity中使用了registerReceiver()方法注册了一个BroadcastReceiver,如果没在Activity的生命周期内调用unregisterReceiver()方法取消注册此BroadcastReceiver,由于BroadcastReceiver不止被Activity引用,还可能会被AMS等系统服务、管理器等引用,导致BroadcastReceiver无法被回收,而BroadcastReceiver中又持有着Activity的引用(即:onReceive方法中的参数Context),会导致Activity也无法被回收(虽然Activity回调了onDestroy方法,但并不意味着Activity呗回收了),从而导致严重的内存泄漏。

非静态内部类对外部类的引用

(非静态)内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。
内部类的实现其实是通过编译器的语法糖(Syntactic sugar)实现的,通过生成相应的子类即以OutClassName$InteriorClassName命名的Class文件。并添加构造函数,在构造函数中【传入】外部类,这也是为什么内部类能使用外部类的方法与字段的原因。所以,当外部类与内部类生命周期不一致的时候很有可能发生内存泄漏。
例如在一个Activity启动一个Thread执行一个任务,因为Thread是内部类持有了Activity的引用,当Activity销毁的时候如果Thread的任务没有执行完成,造成Activity的引用不能被释放从而引起内存泄漏。
这种情况下可以通过声明一个静态内部类来解决问题,从反编译中可以看出,声明为static的内部类不会持有外部类的引用。此时,如果你想在静态内部类中使用外部类的话,可以通过软引用的方式保存外部类的引用。
/*** 测试非静态内部类导致内存泄漏的问题*/
public class MemoryLeaksActivity extends Activity {TextView textView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);textView = new TextView(this);setContentView(textView);String startTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());textView.setText("开始休息 " + startTime);//匿名内部类,如果在Activity销毁前线程的任务还未完成,将导致Activity的内存资源无法回收,造成内存泄漏new Thread(() -> {SystemClock.sleep(1000 * 5);String endTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());Log.i("bqt", "【结束休息】" + endTime);//即使Activity【onDestroy被回调了】,这条日志仍会打出来//runOnUiThread(() -> textView.append("\n结束休息 " + endTime));}).start();}@Overrideprotected void onDestroy() {super.onDestroy();Log.i("bqt", "【onDestroy被回调了】");}
}
1
/**

2
 * 测试非静态内部类导致内存泄漏的问题

3
 */

4
public class MemoryLeaksActivity extends Activity {

5
    TextView textView;

6
    

7
    @Override

8
    protected void onCreate(Bundle savedInstanceState) {

9
        super.onCreate(savedInstanceState);

10
        textView = new TextView(this);

11
        setContentView(textView);

12
        String startTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());

13
        textView.setText("开始休息 " + startTime);

14
        

15
        //匿名内部类,如果在Activity销毁前线程的任务还未完成,将导致Activity的内存资源无法回收,造成内存泄漏

16
        new Thread(() -> {

17
            SystemClock.sleep(1000 * 5);

18
            String endTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());

19
            Log.i("bqt", "【结束休息】" + endTime);//即使Activity【onDestroy被回调了】,这条日志仍会打出来

20
            //runOnUiThread(() -> textView.append("\n结束休息 " + endTime));

21
        }).start();

22
    }

23
    

24
    @Override

25
    protected void onDestroy() {

26
        super.onDestroy();

27
        Log.i("bqt", "【onDestroy被回调了】");

28
    }

29
}

解决办法就是使用静态内部类,如下:
/*** 测试使用静态内部类避免导致内存泄漏*/
public class MemoryLeaksActivity extends Activity {TextView textView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);textView = new TextView(this);setContentView(textView);String startTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());textView.setText("开始休息 " + startTime);new MyThread(this).start();}@Overrideprotected void onDestroy() {super.onDestroy();Log.i("bqt", "【onDestroy被回调了】");}//静态内部类private static class MyThread extends Thread {SoftReference<Activity> context;MyThread(Activity activity) {context = new SoftReference<>(activity);}@Overridepublic void run() {SystemClock.sleep(1000 * 15);String endTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());Log.i("bqt", "【结束休息】" + endTime);//即使Activity【onDestroy被回调了】,这条日志仍会打出来if (context.get() != null) {context.get().runOnUiThread(() -> Toast.makeText(context.get(), "结束休息", Toast.LENGTH_SHORT).show());}}}
}
1
/**

2
 * 测试使用静态内部类避免导致内存泄漏

3
 */

4
public class MemoryLeaksActivity extends Activity {

5
    TextView textView;

6
    

7
    @Override

8
    protected void onCreate(Bundle savedInstanceState) {

9
        super.onCreate(savedInstanceState);

10
        textView = new TextView(this);

11
        setContentView(textView);

12
        String startTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());

13
        textView.setText("开始休息 " + startTime);

14
        

15
        new MyThread(this).start();

16
    }

17
    

18
    @Override

19
    protected void onDestroy() {

20
        super.onDestroy();

21
        Log.i("bqt", "【onDestroy被回调了】");

22
    }

23
    

24
    //静态内部类

25
    private static class MyThread extends Thread {

26
        SoftReference<Activity> context;

27
        

28
        MyThread(Activity activity) {

29
            context = new SoftReference<>(activity);

30
        }

31
        

32
        @Override

33
        public void run() {

34
            SystemClock.sleep(1000 * 15);

35
            String endTime = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date());

36
            Log.i("bqt", "【结束休息】" + endTime);//即使Activity【onDestroy被回调了】,这条日志仍会打出来

37
            if (context.get() != null) {

38
                context.get().runOnUiThread(() -> Toast.makeText(context.get(), "结束休息", Toast.LENGTH_SHORT).show());

39
            }

40
        }

41
    }

42
}

单例模式初始化后的对象

单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。

数据库、网络、io流等连接

各种连接,比如数据库连接(cursor 游标)、网络连接(socket)和io流的连接,除非显式的调用了其close()方法将其连接关闭,否则是不会自动被GC回收的。

优化内存的方法

1、减少不必要的全局变量,尽量避免static成员变量引用资源耗费过多的实例
比如Context。因为Context的引用超过它本身的生命周期,会导致Context泄漏,所以尽量使用Application这种Context类型。 
2、Cursor(游标)回收
Cursor是Android查询数据库后得到的一个管理数据集合的类,在使用结束以后,应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。
Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。
3、Receiver(接收器)回收
当我们Activity中使用了registerReceiver()方法注册了BroadcastReceiver,一定要在Activity的生命周期内调用unregisterReceiver()方法取消注册 。
通常我们可以重写Activity的onDestory()方法,在onDestory里进行unregisterReceiver操作

4、Stream/File(流/文件)回收
主要针对各种流,文件资源等等,如:InputStream/OutputStream,SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O,Bitmap图片等操作等都应该记得显示关闭。
5、避免创建不必要的对象
最常见的例子就是当你要频繁操作一个字符串时,使用StringBuffer代替String。
还比如:使用int数组而不是Integer数组。
尽量避免创建短命的临时对象,因为减少对象的创建就能减少垃圾收集。
6、避免内部Getters/Setters
在Android中,虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用Getters和Setters是有道理的,但在一个字段经常被访问的类中宜采用直接访问。

来自为知笔记(Wiz)

【内存泄露】Memory Leaks 内存优化相关推荐

  1. VC++6.0 内存泄露(Memory Leaks)征兆1

    我在调试[用VC++6.0的MFC框架写的eWSAuditSys_VC6应用程序]时,突然出现了一下莫名其妙的东西.我将output中的内容分为上半部分和下半部分进行展示,其中下半部分的内容是关键 o ...

  2. SQL Server 内存泄露(memory leak)——游标导致的内存问题

    原文:SQL Server 内存泄露(memory leak)--游标导致的内存问题 转自:http://blogs.msdn.com/b/apgcdsd/archive/2011/07/01/sql ...

  3. 内存溢出(Memory Overflow)和内存泄露(Memory Leak)的区别

    内存泄漏指你用malloc或new申请了一块内存,但是没有通过free或delete将内存释放,导致这块内存一直处于占用状态 内存溢出指你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数 ...

  4. Detected memory leaks!内存泄漏,溢出,内存越界问题分析

    应用程序发生 Detected memory leaks!内存泄漏 一直程序员面对的是一个很痛苦的问题,要查出泄漏的地方有时候需要大半天甚至更长时间.这里讲讲我的一些查找内存泄漏以及避免内存泄漏的一些 ...

  5. 转载浅谈MFC内存泄露检测及内存越界访问保护机制

    2019独角兽企业重金招聘Python工程师标准>>> 本文所有代码均在VC2008下编译.调试.如果您使用的编译器不同,结果可能会有差别,但本文讲述的原理对于大部分编译器应该是相似 ...

  6. java 多线程 内存泄露_关于内存泄露的总结

    大致先分为五个小模块: 1.什么是内存泄漏 2.有哪些情况会导致内存泄漏切如何解决 3.如何检测内存泄漏 4.Java得基本数据类型和占用字节 5.什么是内存溢出和解决办法 一.什么是内存泄漏(Mem ...

  7. JavaScript内存泄露,闭包内存泄露如何解决

    转载于:JavaScript内存泄露,闭包内存泄露如何解决 - 一粒一世界 - 博客园 JavaScript 内存泄露的4种方式及如何避免 简介 什么是内存泄露? JavaScript 内存管理 Ja ...

  8. drools规则引擎因为内存泄露导致的内存溢出

    进入这个问题之前,先了解一下drools: 在很多行业应用中比如银行.保险领域,业务规则往往非常复杂,并且规则处于不断更新变化中,而现有很多系统做法基本上都是将业务规则绑定在程序代码中. 主要存在的问 ...

  9. 性能优化之内存泄露(Memory Leak)常用分析工具(另3种)

    1 LeakCanary(最常用,能监控整个App内存泄漏情况) 1.1 使用LeakCanary // 仅在debug包启用LeakCanary debugImplementation 'com.s ...

最新文章

  1. LeetCode 200. Number of Islands--c++ dfs解法
  2. 提高页面显示速度的秘技
  3. VMware演示手机虚拟化
  4. 数据库基础知识——DDL语言
  5. linux mint 19界面美化,安装完 LinuxMint 19.3 后必做的10件事
  6. 漫步数理统计三十三——采样与统计量
  7. 4.4 一个完整的Google Maps应用
  8. Android开发之修改Chrome书签
  9. 《战争论》第四篇《战斗》的主要内容
  10. pytorch tensor操作:tensor与numpy转换
  11. selenium-webdriver层级定位元素
  12. JS逆向---获取某知名外卖平台数据(_token)
  13. 应用程序正常初始化(0xc0150002)失败
  14. 深入理解Attention及变种(二)
  15. 网页版在线公式编辑器
  16. pycharm和webstorm下载安装流程
  17. Robot Framework自动化测试教程-通过RIDE创建工程、测试套、测试用例、测试资源、变量文件,引入测试库
  18. 基于CAN总线步进电机驱动器设计
  19. 第十四章:Element-ui组件库
  20. 解决pxe网络批量安装部署linux遇到的问题和解决方法

热门文章

  1. 【Linux】Linux基本命令扫盲【转】
  2. Apache常用配置
  3. android graphic:canvas
  4. thinkphp链接mssql以及查询中文乱码问题
  5. 使用.NET中的Action及Func泛型委托
  6. Django中使用UpdateView修改数据后,返回列表页
  7. MVC中执行成功弹出对话框
  8. oracle 10g安装
  9. 删除windows上的oracle产品
  10. 部署Laravel项目到centos服务器上