一、内存泄漏 & 内存溢出

​ 在软件开发与运行过程中,内存管理是一个至关重要的环节。然而,由于各种原因,如程序设计的缺陷或资源管理的不当,可能会导致两种常见的内存问题:内存泄漏(Memory Leak)和内存溢出(Memory Overflow)。各种语言/操作系统上预防和解决也有不同方式方案,以下是关于两者概念的详细介绍:

内存泄漏:

​ 内存泄漏指程序中已经不再需要使用的内存没有被释放,从而造成内存资源浪费和程序性能下降。其特征是程序使用内存总量持续增加,直到程序崩溃或者系统强制关闭。

原因:

  1. 资源未被正确释放:程序动态分配了内存资源,但没有及时释放,导致内存泄漏。
  2. 垃圾回收机制失效:在使用Java等高级语言编写的程序中,垃圾回收机制负责自动释放不再使用的内存资源。但如果程序员在编写代码时存在逻辑错误,就有可能导致垃圾回收机制失效,进而导致内存泄漏。
  3. 循环引用:在使用面向对象编程语言时,两个对象之间可能会发生相互引用的情况。如果这种引用形成了一个环路,就会导致这些对象永远无法被释放,从而产生内存泄漏。

影响

  • 系统崩溃:内存泄漏会导致系统可用内存不足,从而造成系统崩溃或者异常退出。
  • 程序性能下降:内存泄漏会让程序占用更多的内存,导致程序性能下降,响应速度变慢。
  • 安全问题:黑客可能会利用内存泄漏漏洞实现攻击,比如利用堆栈溢出漏洞进行缓冲区溢出攻击。

检测与避免方法

  • 使用内存监控工具:
    • Windows Task Manager、
    • Linux top命令、Valgrind
    • Android Liko KOOM Leakcanary
  • 代码审查:通过仔细阅读代码,找出可能导致内存泄漏的逻辑错误,发现和解决问题。
  • 显式地释放资源:在程序中动态分配了内存后,一定要在不再需要使用这些资源时显式地释放它们。例如,在C语言中使用free函数进行内存释放。
  • 使用智能指针:C++等语言中提供了智能指针的概念,可以自动管理内存,避免手动释放资源时遗漏。
  • 避免循环引用:在使用面向对象编程时,尽量避免对象之间相互引用形成环路的情况。如果无法避免,可以使用弱引用等技术来解决。

内存溢出:

​ 内存溢出是指程序在申请内存时,所需的内存空间超过了系统所分配的内存空间,使得程序无法正常运行.

原因:

  1. 分配的内存过于庞大,在内存池中没有足够的连续空间满足需求。
  2. 内存泄漏:应用程序持续向系统申请内存空间,但没有及时释放,导致内存池被消耗殆尽。
  3. 代码编写问题:如未正确释放已经申请的内存、指针操作错误等。
  4. 操作系统本身限制了进程所能申请的最大内存。

影响

​ 内存溢出会导致程序崩溃或者无法正常运行,因为它直接涉及到程序的运行空间不足。

解决办法

  • 优化代码:避免出现无效的内存分配和内存泄漏。
  • 使用内存池技术:尽可能地重复利用已经申请的内存空间。
  • 增大操作系统对程序可用的最大内存限制(前提是操作系统支持)。
  • 将需要处理的数据划分为更小的块,分批进行处理,这样可以使每个块需要分配的内存量变小。

二、关联/区别

关系:

​ 内存泄漏会造成内存溢出, 内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积起来后果很严重,无论多少内存,迟早会被占光,最终会导致内存溢出。

区别:

  • 发生时机:内存溢出通常发生在程序运行时,当数据结构的大小超过预设限制或者递归调用栈过深时,就会发生内存溢出。而内存泄漏则是在程序持续运行过程中逐渐累积的,当不再使用的内存没有及时释放时,就会产生泄漏。

  • 表现方式: 内存溢出会导致程序崩溃或者无法正常运行,因为它直接涉及到程序的运行空间不足。而内存泄漏在初期可能不会对程序产生明显影响,但随着时间的推移,未释放的内存不断累积,最终会导致系统资源耗尽,程序性能下降。

  • 解决方案: 对于内存溢出,解决方法通常涉及到优化数据结构和算法,减少内存消耗,或者增加系统可用内存。而解决内存泄漏则需要定位泄漏源头,修复代码中的内存管理问题,确保不再使用的内存能够被及时释放。

三、Android

内存泄漏场景:

1. 非静态内部类/匿名内部类

造成原因:非静态内部类默认会持有外部类的引用,如果内部类的生命周期超过了外部类,就会造成内存泄漏。

场景:当Activity销毁后,由于内部类中存在异步耗时任务还在执行,导致Activity实例一直被内部类持有无法被回收,造成内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... ...
Button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟耗时操作
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
});
}
}

解决办法:使用静态内部类,并通过弱引用的方式持有外部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);

Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Thread(new MyRunnable()).start();
}
});
}

static class MyRunnable implements Runnable {
@Override
public void run() {
try {
// 模拟耗时操作
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

2. 静态成员变量造成的内存泄漏

造成原因:静态成员变量的生命周期等于应用程序的生命周期,如果该静态成员引用的变量生命周期小于该静态变量,就会造成内存泄漏。

场景:静态成员变量持有了一个耗费资源过多的实例(如Activity、Fragment)。

1
2
3
4
5
6
7
public class Person {
private static Activity mActivity;

Person(Activity activity) {
mActivity = activity;
}
}

解决办法:尽量避免使用Static成员变量引用资源耗费过多的实例,如果必须使用,可以使用Application的Context。这样可以随着应用生命周期创建销毁.

3. 单例模式造成的内存泄漏

造成原因:单例模式由于其具有静态特性,导致其生命周期等于应用程序生命周期,如果单例中持有别的类的实例,就会造成内存泄漏。

场景:单例模式中持有一个耗费资源过多的实例(如Context)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SingleInstance {
private static SingleInstance instance;
private Context mContext;

private SingleInstance(Context context) {
this.mContext = context; // 传递的是Activity的context
}

public static SingleInstance getInstance(Context context) {
if (instance == null) {
instance = new SingleInstance(context);
}
return instance;
}
}

解决办法:使用Application的Context代替Activity的Context。

4. Handler造成的内存泄漏

造成原因:Handler会持有外部类的对象(如Activity),如果Handler中还有消息没执行完,此时创建Handler的Activity关闭就会造成内存泄漏。

场景:Activity中通过一个子线程异步请求网络数据,请求成功后更新当前页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
// 更新页面
}
};

// 成功获取到网络数据后更新页面
private void handleData(String data) {
Message message = Message.obtain();
message.obj = data;
mHandler.sendMessage(message);
}
}

解决办法

  1. 使用静态内部类+弱引用。
  2. 在Activity销毁时,及时清理消息队列中的消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MainActivity extends AppCompatActivity {
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> mActivity;

public MyHandler(MainActivity activity) {
mActivity = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivity.get();
if (activity != null) {
// 更新页面
}
}
}

private MyHandler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mHandler = new MyHandler(this);
}

@Override
protected void onDestroy() {
super.onDestroy();
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
}

5. 资源性对象未关闭造成的内存泄漏

造成原因:对于资源性对象(如BroadcastReceiver、EventBus等),在不再使用时,应该立即调用其close()函数将其关闭,然后再置为null。否则会造成内存泄漏。

场景:Activity中注册了BroadcastReceiver,但在onDestroy方法中没有注销该接收器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends AppCompatActivity {
private MyReceiver myReceiver;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

myReceiver = new MyReceiver();
registerReceiver(myReceiver, new IntentFilter("MY_ACTION"));
}

// 假设未重写onDestroy方法
}

class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 处理广播
}
}

解决办法:在onDestroy方法中注销广播接收器。

1
2
3
4
5
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(myReceiver); // 解除注册
}

6. 容器中的对象未清理造成的内存泄漏

造成原因:在使用集合类(如ArrayList、HashMap等)时,如果集合中的对象不再使用,但没有及时从集合中移除,就会导致这些对象无法被垃圾回收器回收,从而造成内存泄漏。

场景:在Activity中创建了一个ArrayList来存储一些对象,但在Activity销毁时,没有清空这个ArrayList。

解决办法:在Activity销毁时,清空集合中的对象,并将其置为null。

1
2
3
4
5
6
7
8
@Override
protected void onDestroy() {
super.onDestroy();
if (myList != null) {
myList.clear();
myList = null;
}
}

Liko

在 OOM 和内存触顶时通过用户无感知 dump 来获取 HPROF 文件,当 App 退出到后台且内存 充足的情况进行分析,裁剪 HPROF 回传进行分析

KOOM

利用系统内核COW(Copy-on-write,写时复制)机制,每次dump内存镜像前先暂停虚拟机,然后fork子进程来执行dump操作,父进程在fork成功后立刻恢复虚拟机运行,整个过程对于父进程来讲总耗时只有几毫秒。内存镜像于闲时进行独立进程单线程本地分析,分析完即删除。

LeakCanary

原理分析

分为以下几步:

  1. 监测Activity 的生命周期的 onDestroy() 的调用。
  2. 当某个 Activity 的 onDestroy() 调用后,便对这个 activity 创建一个带 ReferenceQueue 的弱引用,并且给这个弱引用创建了一个 key 保存在 Set集合 中。
  3. 如果这个 activity 可以被回收,那么弱引用就会被添加到 ReferenceQueue 中。
  4. 等待主线程进入 idle(即空闲)后,通过一次遍历,在 ReferenceQueue 中的弱引用所对应的 key 将从 retainedKeys 中移除,说明其没有内存泄漏。
  5. 如果 activity 没有被回收,先强制进行一次 gc,再来检查,如果 key 还存在 retainedKeys 中,说明 activity 不可回收,同时也说明了出现了内存泄漏。
  6. 发生内存泄露之后,dump内存快照,分析 hprof 文件,找到泄露路径(使用 haha 库分析),发送到通知栏