什么是垃圾回收
- 对比C/C++这种需要自己管理内存的语言
- java可以实现自动内存管理和回收
- 垃圾回收器负责回收程序中已经不使用,但是仍然被各种对象占用的内存。
优点:将程序员从繁重、危险的内存管理工作中解放出来
缺点:可能会占用大量资源
垃圾回收机制
Android系统里面有一个Generational Heap Memory的模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。
执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。
通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。
年轻代(young generation)
年轻代是所有新对象产生的地方。当年轻代内存空间被用完时,就会触发垃圾回收。这个垃圾回收叫做Minor GC。年轻代被分为3个部分——Enden区和两个Survivor区。
- 大多数新建的对象都位于Eden区。
- 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。
- Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
- 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
老年代(Old Generation)
年老代内存里包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象。通常会在老年代内存被占满时进行垃圾回收。老年代的垃圾收集叫做Major GC。Major GC会花费更多的时间。
永久代(Permanent Generation)
存放方法区,方法区中有要加载的类信息、静态变量、final类型的常量、属性和方法信息。
导致GC频繁执行有两个原因:
1.内存抖动
2.瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
Android内存检测工具
1. Memory Monitor
android monitor能做什么:
a. 实时查看应用的内存分配情况
b. 判断应用是否由于GC操作造成卡顿
c. 判断应用崩溃是否是因为超出了内存
如何使用 Memory Monitor
进入项目后,可以看到Android Studio的主面板左下角有一个Android Monitor标签:
点击Android Monitor标签,然后点击Monitor标签,当项目运行的时候即可查看内存实时数据
在Android Monitor中我们可以手动触发GC,下图中的小车子就是触发GC的按钮,一旦按下就会回收那些没有被引用的对象
利用 Memory Monitor可以发现的问题
1.发现Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。
2.发现大内存对象分配的场景
3.发现内存不断增长的场景
4.确定卡顿问题是否因为执行了GC操作
2. Allocation Tracker
Allocation Tracker是android studio的一个内存分配跟踪器,使用之后能够了解在一定时间内的内存分配情况。
如何使用Allocation Tracker
点击下图中画红圈的按钮可以启动标记
再次点击停止追踪
随后自动生成一个alloc结尾的文件,这个文件就记录了这次追踪到的所有数据,然后会打开一个数据面板,面板左上角是所有历史数据文件列表,后面是详细信息
查看方式
- Group by Method:用方法来分类我们的内存分配
- Group by Allocator:用内存分配器来分类我们的内存分配
默认会以Group by Method来组织。首先以线程对象分类,默认以分配顺序来排序。Count表示分配了多少次内存,size表示内存大小
以Group by Allocator来查看内存分配的情况如下图。这种方式显示的好处,是我们很好的定位我们自己的代码的分析信息
Jump To Source按钮
如果我们想看内存分配的实际在源码中发生的地方,可以选择需要跳转的对象,点击该下图中红圈的按钮就能发现我们的源码
统计图标按钮
Jump To Source按钮右边的按钮为统计按钮,点击该按钮,会弹出一个新窗口,里面是一个酷炫的统计图标,有柱状图和轮胎图两种图形可供选择,默认是轮胎图,其中分配比例可以选择分配次数和占用内存大小,默认是大小Size
圆心是我们的起点处,如果把鼠标放到我图中标注的区域,会在右边显示当前指示的是什么线程以及具体信息。
默认打开的是全局信息,我们如果想看其中某个线程,详细信息,可以顺着某个扇面向外围滑动,当然如果你觉得不还是不清晰,可以双击该扇面全面展现该扇面的信息。如果想回到默认显示的圆,双击圆心空白处就可以。
3.Heap Viewer
Heap Viewer能实时查看App分配的内存大小和空闲内存大小。还可以用于发现发现Memory Leaks。但这个功能只限于Android5.0以上。
如何使用Heap Viewer
在Android studio工具栏中直接点击小机器人:
选中要检测的app,然后点击cause GC:
总览
列名 | 含义 |
---|---|
Heap Size | 堆栈分配给App的内存大小 |
Allocated | 已分配使用的内存大小 |
Free | 空闲的内存大小 |
%Used | Allocated/Heap Size,使用率 |
Objects | 对象数量 |
详情
类型 | 意义 |
---|---|
free | 空闲的对象 |
data object | 数据对象,类类型对象,最主要的观察对象 |
class object | 类类型的引用对象 |
1-byte array(byte[],boolean[]) | 一个字节的数组对象 |
2-byte array(short[],char[]) | 两个字节的数组对象 |
4-byte array(long[],double[]) | 4个字节的数组对象 |
non-Java object | 非Java对象 |
当我们点击某一行时,可以看到如下的柱状图:
横坐标是对象的内存大小,这些值随着不同对象是不同的,纵坐标是在某个内存大小上的对象的数量
那么如何用heap viewer来检测内存泄露呢?在需要检测内存泄漏的用例执行过后,手动GC下,然后观察总览中的allocted(也可以观察Allocated/Heap Size内存的情况),看看内存是不是会回到一个稳定值,多次操作后,只要内存是稳定在某个值,那么说明没有内存溢出的,如果发现内存在每次GC后,都在增长,不管是慢增长还是快速增长,都说明有内存泄漏的可能性。
4. LeakCanary
这是一个是一个开源的库。github地址为LeakCanary。使用之后能够直接在手机或者虚拟机发生明显的内存泄漏之后弹出通知栏,告知在哪个类中出现问题。
下图的例子是因为一个static静态的TextView在所在的Activity被销毁时没有被回收而引起的,只要把TextView前的static去掉就可以了。
避免内存泄露的方法
- 尽量不要让静态变量引用activity
- 使用WeakReference
- 使用静态内部类来代替内部类
- 静态内部类使用弱引用引用外部类
- 在声明周期结束的时候释放资源
减少内存使用的方法
- 使用更轻量的数据结构(比如SpareArray代替HashMap)
- 避免在onDraw方法中创建对象
- 对象池(Message.obtain())
- LRUCache
- Bitmap内存复用,压缩(inSampleSize, inBitmap)
- StringBuilder
实例分析(MemoryBugs)
首先打开应用,点击startActivityB按钮,等待一段时间后弹出通知,点击通知显示如下图
从图中可以看出内存泄露是因为当MainActivity关闭之后,sTextView仍然持有MainActivity引用,导致无法回收MainActivity的引用。
我们再点击dump java heap
弹出heap viewer显示如下,从图中可以看到MainActivity仍然存在于内存中。具体的解决办法就是取消将sTextView声明为static。
将sTextView声明为static后,将代码中的延迟从5000改为20000
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
System.out.println("post delayed may leak");
}
}, 20000);
发现当点击跳转到ActivityB之后,通知提示如下图
这是因为Handler仍然持有MainActivity的引用
于是习惯性的将Handler声明为静态内部类
public static class MyHandler extends Handler {
private WeakReference<MainActivity> mWeakReference;
public MyHandler(MainActivity activity) {
mWeakReference = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
private MyHandler mHandler;
protected void onCreate(Bundle savedInstanceState) {
mHandler = new MyHandler(this);
}
但是仍然出现了内存泄露,于是看了下内存泄露的具体原因,似乎是匿名接口Runnable持有了对MainActivity的引用,于是创建一个Runnable静态内部类
public static class MyRunnable implements Runnable{
public final WeakReference<MainActivity> mMainActivityWeakReference;
public MyRunnable(MainActivity activity) {
mMainActivityWeakReference = new WeakReference<>(activity);
}
@Override
public void run() {
System.out.println("post delayed may leak");
}
}
private Runnable mRunnable;
protected void onCreate(Bundle savedInstanceState) {
mRunnable = new MyRunnable(this);
}
mHandler.postDelayed(mRunnable, 20000);
这是后再利用Heap Viewer验证,发现MainActivity的引用已经不存在了
接下来点击start allocation按钮,为了方便查看循环是否完成,在startAllocationLargeNumbersOfObjects方法最后添加
Toast.makeText(this, "完成循环", Toast.LENGTH_SHORT).show();
点击后,内存图如下所示,
如果连续点击,则会出现内存抖动
点击前start allocation tracking,当出现提示完成循环后结束tracking,结果如下图,发现有10000个Rect对象和10000个StringBuilder对象
具体优化方法是,只在onCreate()方法中新建一个Rect对象,并且将要输出的String事先定义
private Rect mRect;
private String printString;
protected void onCreate(Bundle savedInstanceState) {
mRect=new Rect(0, 0, 100, 100);
printString= "-------: " + mRect.width();
}
private void startAllocationLargeNumbersOfObjects() {
Toast.makeText(this, R.string.memorymonitor, Toast.LENGTH_SHORT).show();
for (int i = 0; i < 10000; i++) {
System.out.println(printString);
}
}
最后将MyView类中的onDraw()方法中新建对象的操作放到声明变量的时候进行
private RectF rect = new RectF(0, 0, 100, 100);
private Paint paint = new Paint();