导读:
本篇文章是最近几天关于内存优化的个人学习总结,从基础到日常常见的内存泄漏的顺序慢慢介绍…本编全文本,可能有些单调,不过认真看下来,肯定收益良多!
如果急着解决,直接看 “常见的内存溢出处理”,”常见的内存泄漏”
java 内存分配策略
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
栈与堆的区别:
在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。
当在一段方法块中定义一个变量时,Java就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。
堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。
在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。
例子:
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。 mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。
结论:
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。
java中的四种引用类型:
- 强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
如:User u=new User();
User u 存在于栈里面,new User() 存在于堆里面的,栈通过 = 号,将堆对象引用起来,叫强引用(当前这种形式称为显式的强引用(强可及对象))
软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。
软引用和弱引用,这两个引用是可以随时被虚拟机回收的对象,我们将一些比较占内存但是又可能后面用的对象,比如Bitmap对象,可以声明为软引用或弱引用。但是注意一点,每次使用这个对象时候,需要显示判断一下是否为null,以免出错。
什么是垃圾回收机制?
JVM的垃圾回收机制中,判断一个对象是否死亡,并不是根据是否还有对象对其有引用,而是通过可达性分析。对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,从这些树根往下搜索,搜索走过的链称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明这个对象是不可用的,该对象会被判定为可回收的对象。
那么哪些对象可作为GC Roots(GC 会自动回收的对象)呢?主要有以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
- Thread —活着的线程
一般出问题时也是不巧当的调用以上对象造成的
内存泄漏与内存溢出的区别:
内存泄漏 memory leak
- 指程序申请了内存后(new),用完的内存没有释放(delete),一直被某个或某些实例所持有却不再被使用导致 GC 不能回收
- 生活例子 : 电热水器洗完澡不关水,其他人用就没热水的情况
- 内存泄漏是导致内存溢出的原因之一;内存泄漏累积起来就会造成内存溢出
- 内存泄漏可以通过完善代码来避免
内存溢出 out of memory
- 指程序申请内存时,没有足够的内存空间使用
- 生活例子 : 水杯满了还往里面加水
- 内存溢出可以通过调整配置来减少发生频率,无法彻底避免
常见内存溢出处理:
绝大部分的内存溢出原因是由于图片太大引起的
一般我们使用软引用/弱引用解决由于图片资源过大的内存溢出
但是API9以后,GC机制改变了,建议使用LruCache缓冲图片资源
注意临时Bitmap对象的及时回收,先recycle(),后致空
尽量避免Try catch某些大杯存分配的操作
加载Bitmap时:缩放比例、解码格式、局部加载(图片大于手机屏幕)
图片处理(防止内存溢出) 看这篇
内存泄漏的原因:
以发生的方式来分类,内存泄漏可以分为4类:
常发性内存泄漏 : 发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏 : 发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏 : 发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏 : 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。(重视)
小结:
一次性内存泄漏没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较于常发性和偶发性内存泄漏它更难被检测到
个人总结的内存泄漏:
单例模式造成的内存泄漏
由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
案例:
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;
}
}
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
1、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长 ;
2、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。(static > Activty 生命周期)
所以正确的单例应该修改为下面这种方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。
第二种方式:这样写,连Context都不用传进来了:
在你的 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;
}
}
非静态内部类创建静态实例造成的内存泄漏
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时都会使用该单例的数据
,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏
,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,
该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext 。
Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写不规范即有可能造成内存泄漏
如下示例:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}
private void loadData(){
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,
那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,
正确做法为:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView
//①将Handler改成静态内部类
private static class MyHandler extends Handler {
//②将需要引用Activity的地方,改成弱引用
private WeakReference<Context> reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText("");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
//③在onDestory()方法中移除handler发送的消息和任务
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
使用mHandler.removeCallbacksAndMessages(null);是移除消息队列中所有消息和所有的Runnable。
当然也可以使用 mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。
下面几个方法都可以移除 Message:
public final void removeCallbacks(Runnable r);
public final void removeCallbacks(Runnable r, Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public final void removeMessages(int what, Object object);
线程造成的内存泄漏
对于线程造成的内存泄漏,也是平时比较常见的,如下这两个示例可能每个人都这样写过:
//——————test1
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10000);
return null;
}
}.execute();
//——————test2
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(10000);
}
}).start();
上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。
正确的做法还是使用静态内部类的方式(或将AsyncTask和Runnable类独立出来),
如下:
static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
private WeakReference<Context> weakReference;
public MyAsyncTask(Context context) {
weakReference = new WeakReference<>(context);
}
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10000);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
MainActivity activity = (MainActivity) weakReference.get();
if (activity != null) {
//... }
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
SystemClock.sleep(10000);
}
}
//—————— new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();
这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask::cancel(),避免任务在后台执行浪费资源。
资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,以及一些开源项目的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
使用ListView时造成的内存泄漏(RecycleView同理)
初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象(初始化时缓存中没有View对象则convertView是null)。
说白了,即在构造Adapter时,没有使用缓存的convertView。
解决方法:在构造Adapter时,使用缓存的convertView。
集合容器中的内存泄露(静态的集合类同理)
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
如: 使用 array.clear() ; array = null
WebView造成的泄露
当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。
解决方法:为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
MVP框架的内存泄漏问题:
当Modle在获取数据时,不做处理,它就一直持有Presenter对象,而Presenter对象又持有Activity对象,这条GC链不剪断,Activity就无法被完整回收。
换句话说:Presenter不销毁,Activity就无法正常被回收。
解决:
Presenter在Activity的onDestroy方法回调时执行资源释放操作,或者在Presenter引用View对象时使用更加容易回收的软引用,弱应用。
比如示例代码:
Activity
@Override
public void onDestroy() {
super.onDestroy();
mPresenter.destroy();
mPresenter = null;
}
Presenter
public void destroy() {
view = null;
if(modle != null) {
modle.cancleTasks();
modle = null;
}
}
Modle
public void cancleTasks() {
// TODO 终止线程池ThreadPool.shutDown(),AsyncTask.cancle(),或者调用框架的取消任务api
}
View造成的内存泄漏
一般引用到布局文件的View,如自定义控件,系统的Widget组件,Fragment等,就等于关联上了当前的Activity,如果某个地方被static关键字修饰了,就成造成内存泄漏
解决办法:
在Fragment,或Activity的onDestory()将该View致空
注意:RecycleView 使用到Adapter,Adapter也会间接引用到Activity,因此也需要致空
@Override
public void onDestroy() {
super.onDestroy();
mFloatingActionButton=null;
mRecyclerView=null;
}
Listener监听器引起的内存泄漏:
不管是Android系统的Listener,还是自定义接口回调,定义Listener的类持有一个引用,调用Listener的类也持有了一个引用,系统GC不掉,就造成内泄漏了
解决:使用完listener完,将Listener致空
以百度定位为例:
/***
* 获取定位成功
*/
private void getLocationSuccess(BDLocation bdLocation){
if(mLocationGetListener != null){
mLocationGetListener.queryLocationSuccess(bdLocation);
//致空,防止内存泄漏
mLocationGetListener = null;
}
}
代码不规范造成的内存溢出:
比如:
Bitmap 没调用 recycle()方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。
构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。
图片放置在错误目录造成的内存溢出:
在Android 2.1以后,我们的工程目录出现了drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等目录,这几个目录里的图片资源会按一定比例压缩或扩充,如上比例为3:4:6:8:12
因此,如果我们将较大的图片放在高分辨率的文件目录里,那么真机或模拟器运行时,就会根据手机屏幕分辨率按一定比例压缩图片
反之,如果把较大的图片放在较小的目录,就会造成内存泄漏
内存抖动:
就是突然申请很多对象,变量或者内存空间,突然又不用了,过一阵子又申请很多,系统GC不过来,就会影响系统流畅性(说白了就是内存使用不稳定)
一般不正确的定义变量引起的:
如:
for(int = 0;i<10000;i++){
Person p=new Person(); //不断在循环里 new 对象
}
解决: 把Person p=new Person(); 抽到成员变量
频繁的findViewByID引起的内存泄漏:
findViewByID时获得的View,会占用部分内存,频繁的调用findViewById又不释放这部分内存,久而久之就会造成内存泄漏
解决:合理的使用单例,或使用viewHolder,写代码尽量避免重复使用findViewById的情况
实际开发中,如何避免内存泄漏:
- 在使用Context 时,优先考虑生命周期长的Application的Context
- 对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。
- 对于不再需要使用的对象,将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。
- 保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
- 对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
- 将内部类改为静态内部类
- 静态内部类中使用弱引用来引用外部类的成员变量
- 注意Handler对象和Thread的代码编写规范
- Context里如果有线程,一定要在onDestroy()里及时停掉
- 如果某个View 或 Adapter发生内存泄漏,那么我们可以在个Activty或Fragment 的onDestory() 把该View 设置为null,如recycleView=null;
- 部分系统造成的内存泄漏可以不予理会,如 InputManager
- static 关键字尽量避免使用,一般情况下GC不会回收静态变量
- 使用单例,要用到Context,建议这样写 context.getApplication();
- 将wdight或自定义控件的View,在onDestory() 致空
- 避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,还有类似MVP架构,Listener监听器,这样的设计谁都得不到释放。
- 频繁的字符串拼接用StringBuilder(字符串通过+的方式进行字符串拼接,会产生中间字符串内存块,这些都是没用的)
- 复用系统自带的资源,如不同的布局文件,控件ID名称一样
- 避免在onDraw()方法里面执行对象
- 避免在循环里反复创建对象
- 规范的将资源文件放在合适的工程目录
- 避免频繁的调用findViewById
获取App在当前手机系统能达到的最大内存(小扩展)
/**
* 获取应用最高可用内存
*
* @return 最大内存
*/
public static long getDeviceMaxMemory() {
return Runtime.getRuntime().maxMemory() / 1024;
}
/**
* 获取App在当前手机系统的最大内存
*
* @return 最大内存
*/
public static String getAppMemoryClass_Rom(Context context) {
StringBuilder sb = new StringBuilder();
ActivityManager activityManager = (ActivityManager) context
.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = activityManager.getMemoryClass();
int largeMemoryClass = activityManager.getLargeMemoryClass();
sb.append("memoryClass:"+memoryClass+"\n");
sb.append("largeMemoryClass:"+largeMemoryClass+"\n");
return sb.toString();
}
如果想进一步加深 Context 理解 可看:
不墙要加载略慢,能墙使用更佳
总结:
本人的这篇文章估计总结的非常详细了,在以后工作中如果发现有新的内存泄漏情况会继续更新
如果觉得文章对您有用,点击一个关注憋