前情提要
java中四种引用类型
StrongReference强引用
如 Object o = new Object()
- 回收时机:从不回收
- 使用:对象的一般保存
- 生命周期:JVM停止的时候才会终止
SoftReference软引用
- 回收时机:当内存不足的时候;
- 使用:SoftReference结合- ReferenceQueue构造有效期短;
- 生命周期:内存不足时终止
WeakReference,弱引用
- 回收时机:在垃圾回收的时候;
- 使用:同软引用;
- 生命周期:GC后终止
PhatomReference 虚引用
- 回收时机:在垃圾回收的时候;
- 使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动;
- 生命周期:GC后终止
Java 程序运行时的内存分配
Java 程序运行时的内存分配策略有三种:静态分配、栈式分配和堆式分配。
对应的存储区域如下:
静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区 :方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
栈和堆的区别
栈内存:在方法体内定义的局部变量(一些基本类型的变量和对象的引用变量)都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。
堆内存:用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。
栈内存:基本类型变量、对象引用变量 、方法内局部变量
堆内存: new 出来的对象
看下面代码
public class A {
int a = 0; // 栈内
B b = new B(); // new B()堆内 b在栈内
public void test(){
int a1 = 1; //栈内
B b1 = new B(); // b1在栈内 new B() 在堆内
}
}
A object = new A(); //object栈内 new A() 堆内
A类内的局部变量都存在于栈中,包括基本数据类型a1和引用变量b1,b1指向的B对象实体存在于堆中
引用变量object存在于栈中,而object指向的对象实体存在于堆中。new A 对象的所有成员变量a和b在栈内(句柄),而引用变量b指向的B类对象实体存在于堆中。
主线程的Looper对象的生命周期 = 该应用程序的生命周期
在Java中,非静态内部类 & 匿名内部类都默认持有 外部类的引用
举例handler内部msg —— handler实例 ——Activity实例
造成内存泄漏情景
非静态内部类导致的内存泄露,比如Handler,解决方法是将内部类写成静态内部类,在静态内部类中使用软引用/弱引用持有外部类的实例
IO操作后,没有关闭文件导致的内存泄露,比如Cursor、FileInputStream、FileOutputStream使用完后没有关闭
自定义View中使用TypedArray后,没有recycle
Context 造成的内存泄漏 如单例模式中的内存泄漏。解决方法:使用Application的Context
注册监听器的泄漏 没有在destory时 unregisterxxx()
集合中对象没清理造成的内存泄漏 解决方法 :在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序
WebView造成的泄露
当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。
adb dumpsys meminfo packageName 查找内存泄漏
adb shell dumpsys meminfo packagename -d命令,反复进入、退出同一界面,并对比两次的Activity和View的数量变化。如果有差异,则说明存在内存泄露(在使用命令查看Activity和View的数量之前,记得手动触发GC)。
要继续观察dumpsys meminfo 包名, 输出的结果信息,关注点放在 UnKnown那一行 和 Native Heap 那一行,关注Heap Alloc 或者 Pss Total, 如果你的总TOTAL一直再增加,但是是由于这两行的增加,那么这个问题你不需要再继续在MAT上花时间了,因为这种内存泄露问题,出在Native层(C)那么你需要去找你程序中使用到JNI的地方,so库或者其他一些特殊调用上,分析它们是否可能造成内存泄露问题。
adb shell showmap -a PID
然兴许你依旧没有头绪,那么没关系,另一个命令就是为了你而存在的,(首先某个应用的PID号, 用dumpsys meminfo 包名,那边已经可以查到)
譬如我上面那个mms, PID号为2786, 接着adb shell showmap -a PID号 (adb shell showmap -a 2786)
然后根据结果[....]这的信息,在去google上面找关键字, 譬如:[ anon ] bash的堆
(4)当你最终还是不知道是由哪边的.so库引起的话,你可以查看下Native Heap的内存分配情况,这时候你依旧需要借助DDMS,
需要先执行以下命令:
adb shell setprop libc.debug.malloc 1
adb shell stop
adb shell start
然后你还需要改一下eclipse中的配置参数值【因为如果你不配置的话,你的DDMS打开默认是看不到Native Heap那个Tab项的】
在ddms.cfg文件(实在找不到的话,就用Everything搜索下吧)最后增加一行native=true并save。ddms.cfg位于c:\Users\xxx.android目录下。
在Device中选择好你要的应用的包名项,然后按下Snapshot按钮, 就可以观察到Native Heap的使用情况了,然后反复执行脚本,再观察观察,你会找到你需要的东西的。
1.单例造成的内存泄漏
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
//this.context = context.getApplicationContext(); 解决方式
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
2.Handler造成的内存泄漏
当 Android 应用程序启动时,framework 会为该应用程序的主线程创建一个 Looper 对象。Looper 对象包含一个简单的消息队列 Message Queue,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序 framework 事件,例如 Activity 生命周期方法调用、button 点击等,这些消息都会被添加到消息队列中并被逐个处理。主线程的 Looper 对象会伴随该应用程序的整个生命周期。
当我们在主线程中实例化一个 Handler 对象后,会自动与主线程 Looper 的消息队列关联起来。所有发送到消息队列的消息 Message 都会拥有一个对 Handler 的引用,而此时当前 Activity 如果已经结束/销毁,而 Handler 由于是非静态内部类就会持有外部类的对象,抓住当前 Activity 对象不放,此时就极有可能导致内存泄漏。
public class SampleActivity extends AppCompatActivity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 1);
// Go back to the previous Activity.
finish();
}
}
静态内部类不会持有外部类的引用,其跟外部类的关系,可以看成平级。
解决办法就是使用静态内部类加 WeakRefrence,如下所示:
private static class MyHandler extends Handler {
private final WeakReference mActivity;
public MyHandler(Sample2Activity activity) {
mActivity = new WeakReference(activity);
}
@Override
public void handleMessage (Message msg) {
Sample2Activity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
或者也可以在Activity的onDestory()中 removeCallbackandMessag(null)
3.非静态内部类持有外部类的实例
public class Sample4Activity extends AppCompatActivity {
private static LeakSample mLeakSample = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mLeakSample == null){
mLeakSample = new LeakSample();
}
//...
}
class LeakSample {
//...
}
}
上述代码在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据(避免了资源的重复创建),这种写法却会造成内存泄漏,同样因为非静态内部类持有外部类对象的原因。正确的做法为: 将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext。
属性动画导致内存泄漏
属性动画中有一类无线循环的动画,如果在当前 Activity 中播放此类动画,并且没有在结束的时候(onDestory)去停止该动画,那么动画会一直播放下去,尽管在界面上无法看见动画的运转,但是在此时 Activity 的 View 会被动画所持有,而 View 又持有当前 Activity,最终导致 Activity 无法被释放。动画的特征代码如下:
animator.setRepeatCount(ValueAnimator.INFINITE);
解决办法自然很简单,在 OnDestory() 中去取消动画即可。
Dialog 导致的内存泄漏
在当前 Dialog 所依附的 Activity 销毁之前,我们没有去将当前的 Dialgo 销毁(dismiss) 话也是很容易导致内存泄漏的。
匿名内部类
android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:
使用 Memory Profiler 查看 Java 堆和内存分配
深入理解 Android 之内存泄漏
Android 内存泄漏总结
Android 内存泄漏分析心得