Java中内存简介
Java内存分配会涉及到一下几个区域:
1.寄存器:程序中无法控制(补充:c里是是可以通过register关键字将数据分配在寄存器上的)
2.栈:存放基本类型的数据和对象的引用,在函数中定义的一些基本类型变量和一些对象引用变量(局部变量,函数参数等),但是对象本身不放在栈里,而放在堆里。
它是先进后出的队列,进出一一对应,不产生碎片,运行效率高,当超过变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。由编译器自动分配释放。
3.堆:存放由new创建的对象或数组,一般由程序员分配释放, 若程序员不释放,由java虚拟机的自动垃圾回收器来管理。
在堆中产生一个对象或者数组之后,可以在栈中定义一个变量,让栈中这个变量的取值等于数组或者对象在堆内存中的首地址,栈中这个变量就成了这个数组或者对象的引用变量。引用变量就相当于为数组或者对象起的一个名字,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,栈中的变量指向堆内存中的对象这就是java的指针。
引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域以外后就会被释放,而数组和对象本身就还在堆中,即使程序运行到对象创建所在代码块以外后,数组和对象在堆中所占内存也不会被释放,仍占据内存空间不放,在之后不确定的一个时间之后才会被释放。
堆内存用于存放对象实例。在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆内存中频繁的 new / delete 会造成大量内存碎片,使程序效率降低。
4.静态域:存放对象中使用static定义的静态成员。编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量。
四种引用类型的介绍:
1.强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
Object obj = new Object();
2.软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存,软引用曾经常被用来作图片缓存(image-loader开源框架图片缓存就是使用的软引用)。
一般用于实现内存敏感的高速缓存。
示例:实现学生信息查询操作时有两套数据操作的方案
一、将得到的信息存放在内存中,后续查询则直接读取内存信息;(优点:读取速度快;缺点:内存空间一直被占,若资源访问量不高,则浪费内存空间)
二、每次查询均从数据库读取,然后填充到TO返回。(优点:内存空间将被GC回收,不会一直被占用;缺点:在GC发生之前已有的TO依然存在,但还是执行了一次数据库查询,浪费IO)
通过软引用解决:
3.弱引用:发生GC时必定回收弱引用指向的内存空间。
4.虚引用:"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
总结:堆内存中的长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因。
Android中常见的内存泄露及解决方案
1.集合类引起的内存泄漏:
集合类使用时如果只有添加元素的操作,该集合类是全局性变量(比如用static修饰),那么随着集合size的增大,而没有相应的删除机制,会导致内存占用不断上升,而在Activity销毁时,集合中这些对象无法被回收,导致内存泄漏。比如我们喜欢用静态的HashMap做一些缓存之类的事,这种情况要小心,集合内对象建议采用弱引用方式存储,并且要在不需要时手动释放。
2.单例造成的内存泄漏:
单例的静态特性导致其生命周期和应用一样长。
有时在创建单例时如果我们需要context对象,如果传入的是Application的Context那么不会有问题。但是如果传入的是Activity的Context对象的话,那么当该Activity生命周期结束时,该Activity的引用依然被单例持有,所以不会被回收,而单例的生命周期又会和应用生命周期一样长所以这样就造就了内存泄漏。
解决办法一:通过该Activity的Context获取Application的Context。代码如下:
、、、
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;
}
}
、、、
第二种解决方案:在构建单例时不需要传入Context,直接在Application中写一个静态方法,方法内通过getApplicationContext返回Context,在单例中直接调用这个方法获取Context。
非静态内部类造成的内存泄漏:
在 Java 中,非静态内部类(包括匿名内部类,比如 Handler, Runnable匿名内部类最容易导致内存泄露)会持有外部类对象的强引用(如 Activity),而静态的内部类则不会引用外部类对象。
非静态内部类或匿名类因为持有外部类的引用,所以可以访问外部类的资源属性成员变量等;静态内部类不行。
因为普通内部类或匿名类依赖外部类,所以必须先创建外部类,再创建普通内部类或匿名类;而静态内部类随时都可以在其他外部类中创建。
Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。
由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。
举个例子:
在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。
修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。
尽量避免使用 static 成员变量
如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。
这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,yi’wei如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。
这里修复的方法是:
不要在类初始时初始化静态成员。可以考虑lazy初始化。
架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。
资源未关闭造成的内存泄露:
例如:
BroadcastReceiver,Content Provider之类的没有解除注册啊;
Cursor,Stream 之类的没有 close 啊;
无限循环的动画在 Activity 退出前没有停止啊;
一些其他的该 release 的没有 release,该 recycle 的没有 recycle…等等。
一些不良代码造成的内存压力
有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
比如:
Bitmap 没调用 recycle()方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。
构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。
总结
对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。
尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。
对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
将内部类改为静态内部类
静态内部类中使用弱引用来引用外部类的成员变量
Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable。
在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。
正确关闭资源,对于使用了BraodcastReceiver,Content Provider,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。
保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。