1.内存优化(一)内存泄漏

目录

  1. 内存泄漏
  1. 如何找到项目中存在的内存泄露
  1. 如何在应用里面避免内存泄露

  2. 内存泄漏经常出现的例子

1.内存泄漏

C/C++ 自己去分配内存和释放内存——手动管理malloc和free

1.1.什么是内存泄露

内存不在GC掌控之内了。

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏

了解java的GC内存回收机制:某对象不再有任何的引用的时候才会进行回收。

ArrayList<String> list = new Arraylist<String>();
复制代码

(回到顶部)

1.2.了解内存分配的几种策略

1.静态的

静态的存储区:内存在程序编译的时候就已经分配好,这块的内存在程序整个运行期间都一直存在。 它主要存放静态数据、全局的static数据和一些常量。

2.栈式的

在执行函数(方法)时,函数一些内部变量的存储都可以放在栈上面创建,函数执行结束的时候这些存储单元就会自动被释放掉。 栈内存包括分配的运算速度很快,因为内置在处理器的里面的。当然容量有限。

3.堆式的

也叫做动态内存分配。有时候可以用malloc或者new来申请分配一个内存。在C/C++可能需要自己负责释放(java里面直接依赖GC机制)。

在C/C++这里是可以自己掌控内存的,需要有很高的素养来解决内存的问题。java在这一块貌似程序员没有很好的方法自己去解决垃圾内存,需要的是编程的时候就要注意自己良好的编程习惯。

区别:

1.空间和大小: 堆是不连续的内存区域,堆空间比较灵活也特别大。 栈式一块连续的内存区域,大小是有操作系统觉决定的。

2.效率: 堆管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致效率低下。 对于栈的话,他先进后出,进出完全不会产生碎片,运行效率高且稳定。

public class Main{
	int a = 1;
	Student s = new Student();
	public void XXX(){
		int b = 1;//栈里面
		Student s2 = new Student();
	}
}
复制代码

1.成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)---因为他们属于类,类对象最终还是要被new出来的。

2.局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。-----因为他们属于方法当中的变量,生命周期会随着方法一起结束。

我们所讨论内存泄露,主要讨论堆内存,他存放的就是引用指向的对象实体。

(回到顶部)

1.3.防止内存溢出

有时候确实会有一种情况:当需要的时候可以访问,当不需要的时候可以被回收也可以被暂时保存以备重复使用。比如:ListView或者GridView、RecyclerView.

加载大量数据或者图片的时候,图片非常占用内存,一定要管理好内存,不然很容易内存溢出。滑出去的图片就回收,节省内存。看ListView的源码——回收对象,还会重用ConvertView。如果用户反复滑动或者下面还有同样的图片,就会造成多次重复IO(很耗时),那么需要缓存---平衡好内存大小和IO,算法和一些特殊的java类。

算法:lrucache(最近最少使用先回收)

特殊的java类:利于回收,StrongReference,SoftReference,WeakReference,PhatomReference

类型 回收时机 使用 生命周期
StrongReference 强引用 从不回收 对象的一般保存 JVM停止的时候才会终止
SoftReference 软引用 当内存不足的时候 SoftReference结合
ReferenceQueue构造有效期短
内存不足时终止
WeakReference 弱引用 在垃圾回收的时候 同软引用 GC后终止
PhatomReference 虚引用 在垃圾回收的时候 合ReferenceQueue来跟踪对象被
垃圾回收期回收的活动
GC后终止

开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用和弱引用。 软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。

比如使用场景:默认头像、默认图标。 ListView或者GridView、RecyclerView要使用内存缓存+外部缓存(SD卡)

(回到顶部)

1.4.内存泄露例子

单例模式导致内存对象无法释放而导致内存泄露

MainActivity在内存当中泄露了。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CommUtil commUtil = CommUtil.getInstance(this);

    }
}


public class CommUtil {
    private static CommUtil instance;
    private Context context;
    private CommUtil(Context context){
        this.context = context;
    }

    public static CommUtil getInstance(Context context){
        if(instance == null){
            instance = new CommUtil(context);
        }
        return instance;
    }

}
复制代码

这个故事告诉我们能用Application的context就用Application的CommonUtil生命周期跟MainActivity不一致,而是跟Application进程同生同死。

旋转3次:会在内存里面开辟三个MainActivity 实际上3次以上都只会有2个MainActivity。当GC回收的时候会将除了第0个和最后这一个留着其他的都会被回收。

优化两个情况: 1.主动;平时 2.被动,很卡的时候 出现问题的时候。

如果我们不知道代码内存泄露的情况,如何判断我们的项目里面有哪些是有内存泄露情况的?

1.凭借工具结合自己的经验来判断。 往往我们的app在某个时候或者某个操作以后会出现很卡的现象。

1)判断就是查看内存抖动情况

Android Monitor MAT (对Eclipse插件使用的,也有独立分析工具)

查找引用了该对象的外部对象有哪些, 然后一个一个去猜,查找可能内存泄露的嫌疑犯,依据:看(读代码和猜)他们的生命周期是否一致(可以通过快照对比),如果生命周期一致了肯定不是元凶。

排除一些容易被回收的(软引用、虚引用、弱引用)

设置监听很容易出现内存泄露

handler.post(callback)
onDestroy(){
handler.removeCallback();
}
复制代码

(回到顶部)

2.如何找到项目中存在的内存泄露

往往做项目的时候情况非常复杂,或者项目做得差不多了想起来要性能优化检查下内存泄露。

如何找到项目中存在的内存泄露的这些地方呢?

2.1.确定是否存在内存泄露

1.Android Monitors的内存分析

最直观的看内存增长情况,知道该动作是否发生内存泄露。

动作发生之前:GC完后内存1.4M; 动作发生之后:GC完后内存1.6M

2.使用MAT内存分析工具

MAT分析heap的总内存占用大小来初步判断是否存在泄露。 Heap视图中有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。

我们反复执行某一个操作并同时执行GC排除可以回收掉的内存,注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。

反之如果代码中存在没有释放对象引用的情况,随着操作次数的增多Total Size的值会越来越大。 那么这里就已经初步判断这个操作导致了内存泄露的情况。

(回到顶部)

2.2.先找怀疑对象(哪些对象属于泄露的)

MAT对比操作前后的hprof来定位内存泄露是泄露了什么数据对象。(这样做可以排除一些对象,不用后面去查看所有被引用的对象是否是嫌疑)

快速定位到操作前后所持有的对象哪些是增加了(GC后还是比之前多出来的对象就可能是泄露对象嫌疑犯)

技巧:Histogram中还可以对对象进行Group,比如选择Group By Package更方便查看自己Package中的对象信息。

(回到顶部)

2.3. MAT分析hprof来定位内存泄露的原因所在。(哪个对象持有了上面怀疑出来的发生泄露的对象)

  • 1.Dump出内存泄露“当时”的内存镜像hprof,分析怀疑泄露的类;
  • 2.把上面得出的这些嫌疑犯一个一个排查个遍。步骤:

(1)进入Histogram,过滤出某一个嫌疑对象类

(2)然后分析持有此类对象引用的外部对象(在该类上面点击右键List Objects--->with incoming references)

(3)再过滤掉一些弱引用、软引用、虚引用,因为它们迟早可以被GC干掉不属于内存泄露(在类上面点击右键Merge Shortest Paths to GC Roots--->exclude all phantom/weak/soft etc.references)

(4)逐个分析每个对象的GC路径是否正常,此时就要进入代码分析此时这个对象的引用持有是否合理,这就要考经验和体力了!

(比如上面的例子中:旋转屏幕后MainActivity有两个,肯定MainActivity发生泄露了,那谁导致他泄露的呢?原来是我们的CommonUtils类持有了旋转之前的那个MainActivity,那是否合理?结合逻辑判断当然不合理,由此找到内存泄露根源是CommonUtils类持有了该MainActivity实例造成的。怎么解决?罪魁祸首找到了,怎么解决应该不难了,不同情况解决办法不一样,要靠你的智慧了。)

context.getapplictioncontext()可以吗? 可以!!只要让CommonUtils类不直接只有MainActivity的实例就可以了。

一般我是最笨的方法解决
new出来对象,用完后把它 = null;这样算不算优化
假如:
    方法里面定义的对象,要去管吗?一般不需要管。
    自己=null,要自己去控制所有对象的生命周期 判断各种空指针,有点麻烦。
    但是在很多时候去想到主动将对象置为null是很好的习惯。
复制代码

(回到顶部)

3.如何在应用里面避免内存泄露

判断一个应用里面内存泄露避免得很好,怎么看?
当app退出的时候,这个进程里面所有的对象应该就都被回收了,尤其是很容易被泄露的(View,Activity)是否还内存当中。
可以让app退出以后,查看系统该进程里面的所有的View、Activity对象是否为0.

工具:

使用AndroidStudio--AndroidMonitor--System Information--Memory Usage查看Objects里面的views和Activity的数量是否为0.
命令行模式:

(回到顶部)

4.内存泄露经常出现的例子

内存泄露(Memory Leak):

进程中某些对象已经没有使用价值了,但是他们却还可以直接或者间接地被引用到GC Root导致无法回收。
当内存泄露过多的时候,再加上应用本身占用的内存,日积月累最终就会导致内存溢出OOM.

内存溢出(OOM):

当应用占用的heap资源超过了Dalvik虚拟机分配的内存就会内存溢出。比如:加载大图片。

4.1.静态变量引起的内存泄露

当调用getInstance时,如果传入的context是Activity的context。只要这个单利没有被释放,那么这个 Activity也不会被释放一直到进程退出才会释放。

public class CommUtil {
	    private static CommUtil instance;
	    private Context context;
	    private CommUtil(Context context){
		this.context = context;
	    }

	    public static CommUtil getInstance(Context mcontext){
		if(instance == null){
		    instance = new CommUtil(mcontext);
		}
	//        else{
	//            instance.setContext(mcontext);
	//        }
		return instance;
	    }
复制代码

(回到顶部)

4.2.非静态内部类引起内存泄露(包括匿名内部类)

错误的示范:

public void loadData(){//隐式持有MainActivity实例。MainActivity.this.a
	new Thread(new Runnable() {
	    @Override
	    public void run() {
		while(true){
		    try {
			//int b=a;
			Thread.sleep(1000);
		    } catch (InterruptedException e) {
			e.printStackTrace();
		    }
		}
	    }
	}).start();
}
复制代码

解决方案: 将非静态内部类修改为静态内部类。(静态内部类不会隐式持有外部类)

当使用软引用或者弱引用的时候,MainActivity难道很容易或者可以被GC回收吗?
GC回收的机制是什么?
当MainActivity不被任何的对象引用。 虽然Handler里面用的是软引用/弱引用,但是并不意味着不存在其他的对象引用该MainActivity。
我连MainActivity都被回收了,那他里面的Handler还玩个屁。

(回到顶部)

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

例子1:

// tv.setOnClickListener();//监听执行完回收对象
//add监听,放到集合里面
tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
	@Override
	public void onWindowFocusChanged(boolean b) {
		//监听view的加载,view加载出来的时候,计算他的宽高等。

		//计算完后,一定要移除这个监听
		tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
	}
});
复制代码

例子2:

SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
//不需要用的时候记得移除监听
sensorManager.unregisterListener(listener);
复制代码

(回到顶部)

4.4.资源未关闭引起的内存泄露情况

比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute attr.recycle()回收。
当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。

4.5.无限循环动画

没有在onDestroy中停止动画,否则Activity就会变成泄露对象。 比如:轮播图效果。

(回到顶部)

猜你喜欢

转载自juejin.im/post/5c6138e651882562260d1346