JAVA中内存的分配策略

程序运行时的内存分配有三种策略,分别是静态的、栈式的、和堆式的,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、堆区和栈区。他们的功能不同,对他们使用方式也就不同。

  • 静态存储区(方法区):内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在。它主要存放静态数据、全局static数据和常量。
  • 栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存(Java则依赖垃圾回收器)。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉。

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。——因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)——因为它们属于类,类对象终究是要被new出来使用的。

我们这里说的内存泄露,是针对,也只针对堆内存,他们存放的就是引用指向的对象实体。

为什么会内存泄漏

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

防止内存溢出的方法

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:

public class CacheBySoftRef {
    // 首先定义一个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异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。

另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。

常见原因

  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;
     }
}

正确的方式

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,

context = getApplicationContext();
...
/**
* 获取全局的context
* @return 返回全局context对象
*/
public static Context getContext(){
     return 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;
     }
}

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

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的内存资源不能正常回收。

正确的做法有两种,一种是将内部类testResource改成静态内部类,还有就是将testResource抽取出来,封装成一个单例,但是需要context时单例要切记注意Context的泄漏,使用applicationContext。

3.异步线程
异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。

private void initData{
        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(20000);
            }
        }).start();
}

线程Runnable执行耗时操作,注意在页面返回时及时取消或者把Runnable写成静态类。a) 如果线程类是内部类,改为静态内部类。

private void initData{
        MyRunnable runnable=new MyRunnable();
        new Thread(runnable).start();
}
private static class MyRunnable implements Runnable{

        @Override
        public void run() {
            SystemClock.sleep(20000);
        }
    }

4.Handler

只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。如果你使用的是sendMessageDelayed(Message msg, long delayMillis)或postDelayed(Runnable r, long delayMillis)等发送延迟消息的时候,那基本内存泄漏发生的概率已经在90%以上了。

private Handler mHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what==0){
                Log.i(TAG,"delay message");
            }
        }
    };

    private void loadData(){
        mHandler.sendEmptyMessageDelayed(0,20000);
    }

解决方法一: 使用静态内部类

private MyHandler mHandler =new MyHandler();
//定义一个静态内部类
private static class MyHandler extends Handler{
 @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what==0){
                Log.i(TAG,"delay message");
            }
        }
}

解决方法二:
在使用Handler的时候,在外面的Activity或者Fragment中的关闭方法中,如onDestroy中调用一下handler.removeCallbacksAndMessages(null),就是移除所有的消息和回调,简单一句话就是清空了消息队列。

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

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

Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。

在Java的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋空,如清空对图片等资源有直接引用或者间接引用的数组(使用array.clear();array = null),最好遵循谁创建谁释放的原则。

6.不需要用的监听未移除会发生内存泄露

v.setOnClickListener();//监听执行完回收对象,不用考虑内存泄漏
tv.getViewTreeObserver().addOnWindowFocusChangeListene()
//监听放到集合里面,需要考虑内存泄漏,一定要remove掉