一、内存溢出和内存泄漏的概念理解
**内存溢出:**是指程序代码片段被执行申请内存时,没有足够的内存空间,导致OOM异常。
OOM:即内存溢出,out of momery。
Android系统为每一个应用程序申请的内存是有限的(为什么如此设计,在dalvik虚拟机章节已经说明),一般为64M或者128M等,国内手机产商修改rom后,也会有所不同。
我们可以在清单文件中配置android:largeheap=“true”,从而可给app申请更大的手机内存空间。
**内存泄漏:**内存泄漏是指程序在申请内存后,被某个对象一直持有而无法释放已申请的内存空间。内存泄漏不断堆积,应用程序可申请的内存空间就会越来越少,最终可能就出现,当程序片段被执行申请新的内存空间而不得,最终导致内存溢出。
内存泄漏是因,内存溢出是果。针对于内存溢出,除了手机内存小,应用程序本身申请的大对象内存多(比如没有合理的处理bitmap),内存泄漏是导致内存溢出的一个重要的原因。
故,在做我们的应用程序的内存优化的时候,内存溢出排查也是其中的一个重要方面。
二、内存溢出分类
1、栈内存溢出
栈内存溢出:StackOverflowError,方法被运行在虚拟栈中,在虚拟栈中的执行的命令递归执行,如果递归的深度过大,则可能会导致栈溢出。通过一下case,可以模拟栈内存溢出。
public class StackOverflowCase {
public void stackOverflowMethod(){
stackOverflowMethod();
}
public static void main(String[] args) {
StackOverflowCase stackOverflowCase = new StackOverflowCase();
stackOverflowCase.stackOverflowMethod();
}
}
2、堆内存溢出
堆内存溢出:OutOfMemoeryError,Java中被创建的对象实例,所占用的内存空间过大,超出了当前应用进程能获取的最大内存空间。
堆内存溢出,是非常常见的,日常开发当中,有很多案例可能导致堆内存溢出,在第三节中会对堆内存泄漏的案例做详细分析。内存泄漏终将导致内存溢出。除了内存泄漏之外,常见的内存溢出场景有:
1)类结构没有声明好,导致的内存溢出,比如fastJson解析json串的时候,出现了对象之间的相互引用。
2)对象所申请的内存过大,比如在android应用开发中,经常会用到的大图加载,bitmap的对象占用大量内存,所以,我们有各种三方图片加载库,大多通过缓存算法,解决图像占用过大的内存。
3)内存泄漏,并不会理解导致我们的应用程序瘫痪,但是溢出的持续积累,终将导致内存溢出。
3、持久代内存溢出
持久代中包含方法区,方法区中包含常量池,持久代内存溢出包括:
1)运行时的常量池溢出 2)方法区中保存的class对象没有及时被回收掉或者class信息占用的内存过大导致溢出。用String.intern()可以触发常量池溢出。
三、内存泄漏的场景案例
1、单例模式下的内存溢出
代码案例:
class AppSetting {
private static AppSetting ourInstance = null;
public static Context mContext;
static AppSetting getInstance(Context context) {
if(ourInstance!=null){
ourInstance = new AppSetting(context);
}
return ourInstance;
}
private AppSetting(Context context) {
this.mContext=context;
}
}
public class PageActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_page);
AppSetting.getInstance(this);
}
}
AppSetting是一个单例,由于单例的静态特性,使得其生命周期和应用的生命周期一样长。PageActivity是一个普通页面Activity,当页面关闭的时候,PageActivity理当可以被Gc进行回收,但是由于上述的代码调用,使得AppSetting实例持有了PageActivity的引用,导致无法正常被Gc回收,形成此块已申请内存的泄漏。
泄漏,可以这么理解,就是这块内存不能jvm管理了,居然被普通对象长期占据。
上述问题的解决方法也比较简单,如果确实要使用上下文对象,换成ApplicationContext。
2、静态变量导致内存泄漏
public class StaticVarMLeakActivity extends AppCompatActivity {
private static Info sInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_static_var_mleak);
if (sInfo == null) {
sInfo = new Info(this);
}
}
}
class Info {
Activity mActivity;
public Info(Activity activity) {
mActivity = activity;
}
}
静态变量存储在方法区,静态变量的生命周期,从类加载开始直到进程结束。一旦静态变量初始化后,它所持有的引用待进程结束才会被释放,上述代码中,页面关闭后,此Activity的对象实例,也没法被回收形成内存泄漏。
上述代码的解决方案是在onDestroy方法中,将静态变量置于null,当页面关闭后,Activity页面对象就可以被回收。
3、非静态内部类导致内存泄漏
Handler的case:
public class NoStaticInnerClassMLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_no_static_inner_class_mleak);
start();
}
private void start() {
Message message = Message.obtain();
message.what = 1;
mHandler.sendMessageDelayed(message,100);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
}
};
}
上述代码呢,也会造成内存泄漏,内存泄漏有主要是有两个原因:
1、非静态的内部类会有持有外部类的引用,这点可以通过查看编译后的class文件得到印证。Handler的引用链大概是这样的:
主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity。
2、上述handler的引用链的生命周期要比Activity生命周期长,运行中的主线程是不会被回收的。
所以Handler泄漏的本质是长生命周期的主线程持有了短生命周期对象Activity的引用。
上述案例的解决方案同样也是在onDestroy中移除message,并将mHandler置为null。
其它case:
同样,比如我们在使用网络框架的时候,网络框架内部通过线程池的方式创建了区别于主线程的子线程,当我们发起网络请求的同时,关闭了页面,其实也就造成了当前页面Activity的泄漏,这种泄漏的本质和上面的case是一样的,运行中的子线程的生命周期比Activity的生命周期要长,造成内存泄漏。
这种情况,我们一般可以通过将强引用关系,变成弱引用关系来解决类似的问题。
4、未取消的广播接收器导致的内存泄漏
public class NoUnRegisterMLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_no_un_register_mleak);
this.registerReceiver(mBroadcastReceiver,new IntentFilter());
}
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
}
};
@Override
protected void onDestroy() {
super.onDestroy();
this.unregisterReceiver(mBroadcastReceiver);
}
}
上述代码中已经添加了很详细的注释,未取消的广播接收器导致的内存泄漏的本质原因是系统服务对象的生命周期比Activity的生命周期长,系统服务对象通过持有广播接收器的引用,间接持有了Activity的引用,有兴趣的同学可以查看ContextImpl.registerReceiverAsUser(…)源码进行详细查看。
5、WebView导致的内存泄漏
下面这段代码摘自网络:https://www.jianshu.com/p/3e8f7dbb0dc7
@Override
protected void onDestroy() {
if( mWebView!=null) {
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
}
super.on Destroy();
}
如果没有调用此句((ViewGroup) parent).removeView(mWebView);这里webView的引用关系:
mComponentCallbacks->AwContents->BaseWebView->Activity;这里面如果直接调用destroy之前,未调用父容器中移除webView,会导致UnRegister的相关逻辑,不被调用,资源得不到释放,导致内存溢出。详情可以参见上述网络资源链接。
总之,我们分析了多种案例过后,其实内存泄漏的本质原因也非常清晰了,这些案例其实也无需记住。
内存泄漏的本质问题在于长生命周期的对象持有了短生命周期的引用。
当短生命周期对象本该到达生命周期终点之时,比如Activity页面关闭,本该交有Gc回收机制,允许其回收当前Activity实例,却由于被长生命周期对象持有,而无法被Gc系统进行回收,从而导致了内存泄漏。
及时获得更多更新,关注gongzhonghao:Hym4Android
|