Android内存分析
在进行Android开发时,OOM是一类非常难处理的问题,要处理OOM问题,除了在编程时多注意对内存的使用,还要会对内存的使用情况进行分析。
现在Android手机的内存已经非常大,但这不能成为应用程序开发者,不注意内存使用的理由,一方面,内存使用过高,会对整个app性能产生影响,另一方面对每个应用程序来说,系统是不会把所有内存分配给一个应用程序的,每个应用程序都有内存的使用上限,被称为堆大小。不同的手机,堆大小也不同。可以通过以下方法,得到当前手机的堆大小。
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();
该方法返回的结果以MB为单位,如果应用程序使用内存超过该值,将出现OOM。
- 分析Log信息
- 使用工具进行具体分析
分析Log信息
进行Android内存分析最简单的开始就是分析运行时输出的Log。当发生GC时,会有一条相关的logcat信息输出。
Dalvik Log信息
在Dalvik中,每一次GC打印的logcat信息格式如下:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
真实输出的示例如下:
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
参数字段讲解:
GC Reason
触发本次GC的原因,包含的值如下:
- GC_CONCURRENT:当应用程序的堆内存快满时,系统会触发该种GC以释放内存。
- GC_FOR_MALLOC:当应用程序需要更多内存,但堆内存不足时,系统触发该种GC以释放内存。
- GC_HPROF_DUMP_HEAP:当为了分析堆内存去请求创建一个HPROF文件时,系统触发该种GC。
- GC_EXPLICIT:主动通知系统执行GC操作时,触发该种GC,比如
System.gc()
方法。 - GC_EXTERNAL_ALLOC:只在API Level 10或更低时,才会触发该种GC。为外部的内存分配而发生的GC(例如,存储在本地内存或NIO二进制缓存中的像素数据)。
Amount freed
本次GC回收的内存总量。
Heap stats
本次释放掉内存的百分比,和(存活对象占用内存)/(总堆大小)
External memory stats
只在API Level 10或更低时,才有该字段。表示外部分配的内存状态,(分配的数量)/(发生GC的限制)。
Pause time
更大的堆会有更长的停滞时间。该字段同时出现两个暂停时间:一个是收集的开始,另一个接近结束。
当打印大量这种Log信息,对其进行分析,发现存活对象占用内存的值是持续上升的,可能就发生了内存泄漏。
ART Log信息
不像Dalvik,ART在没有明确请求打印log时,不会打印信息。GC信息仅在被认为GC缓慢时才被打印。更准确的说,如果GC暂停超过5ms或GC持续超过100ms。如果应用不在可察觉的进程停滞状态,则没有GC被认为是缓慢的。主动的请求GC每次都会被打印。
在ART中,打印出的GC log的格式如下:
I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
真实输出的示例如下:
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
参数字段讲解:
GC Reason
触发本次GC的原因,包含的值如下:
- Concurrent:并发的GC不会停滞所有线程,这种GC运行在后台,不会阻止内存的分配。
- Alloc:这种GC,在应用需要更多内存,但堆内存不足时被触发。在这种情况,垃圾收集发生在分配线程中。
- Explicit:当应用主动请求GC时,该种GC被触发,比如
System.gc()
方法。同Dalvik一样,在ART中也推荐信任系统主动的垃圾收集而避免进行主动的GC请求。 - NativeAlloc:当本地内存分配对本地内存产生压力时,该种GC被触发,例如位图(Bitmaps)或渲染脚本(RenderScript)分配对象。
- CollectorTransition:该种GC由“堆转换”(在运行时切换GC)引起。收集器转换包括将“空闲列表”空间的所有对象复制“碰撞指针”空间。当前的收集器转换仅发生在低内存设备上的应用将进程状态从可察觉的停滞状态转变为不可察觉的停滞状态。
- HomogeneousSpaceCompact(齐次空间紧致):内存的齐次空间压缩是一个“空闲列表”空间到一个压缩的“空闲列表”空间的过程,它通常发生在应用被转变到可察觉进程停滞状态时。这种GC的主要目的是减少内存的使用和整理内存碎片。
- DisableMovingGc:这不是一种真正的GC原因。而是由于GetPrimitiveArrayCritical的使用,导致垃圾收集阻塞的一个提示。
- HeapTrim(堆裁剪):这也不是一种真正的GC原因。而是垃圾收集会被阻塞到堆整理完成的一个提示。
GC Name
ART有多种不同的垃圾收集器可被运行。
- Concurrent mark sweep (CMS)(并发标记清除):一个完整的堆收集器,释放除了图片空间之外的所有空间
- Concurrent partial mark sweep(并发部分标记清除):一个部分完整的堆收集器,收集除了图片和zygote之外的空间
- Concurrent sticky mark sweep(并发粘性标记清除):一个代际收集器,只能释放从最近一次GC以来分配的对象。这个垃圾收集器比CMS和CPMS运行的更普遍,因为它的速度更快,阻塞更低
- Marksweep + semispace:一个非并发的、备份GC,用于堆转换和同类空间压缩(相对堆碎片整理而言)
Objects freed
本次GC从不是“大对象”的空间释放的对象数。
Size freed
本次GC从不是“大对象”的空间释放的字节数。
Large objects freed
本次GC在“大对象”空间释放的的对象数。
Large object size freed
本次GC在“大对象”空间释放的的字节数。
Heap stats
本次释放掉内存的百分比,和(存活对象占用内存)/(总堆大小)
Pause times
通常,GC运行时的停滞时间和引用变换的对象的数量是成正比的。目前,ART CMS收集仅在接近GC结束的时候有一次停滞。移动收集则有一个长的停滞时间,它占了GC期间的绝大多数时间。
如果你看见了大量的GC Log,则主要查看Heap stats的增长部分(例子中的25MB/38MB)。如果这部分持续增长,则你的应用程序存在内存泄漏的可能。与此同时,如果你看到的GC Reason是“Alloc”,则你的应用内存的使用已经接近堆的容量,很快就可能出现OOM异常。
使用工具进行具体分析
在使用工具进行具体内存使用情况分析时,一般先使用Android Studio的Memory Monitor工具观察应用内存的使用状况和GC表现。然后在感觉内存使用有问题的地方,获取hprof文件,并使用Eclipse Memory Analyzer(MAT)工具对该文件进行分析,从而完成对内存使用状况的分析,发现其中的内存使用问题,优化程序。
接下来通过对下面示例程序的内存使用情况进行分析,从而详细的介绍分析过程。
public class StaticReferenceActivity extends AppCompatActivity {
private static final String NAME = StaticReferenceActivity.class.getSimpleName();
private static final String TAG = "sxd";
public static StaticReferenceActivity sStaticReferenceActivity;
private static Bitmap sBitmap;
private ImageView mImageView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_static_reference);
sStaticReferenceActivity = this;
initView();
}
private void initView() {
mImageView = (ImageView) this.findViewById(R.id.image_view);
sBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.image);
mImageView.setImageBitmap(sBitmap);![Alt text](./观测.PNG)
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG, NAME + "--onDestroy++");
}
}
上面示例程序,因为使用了静态变量引用资源,所以在应用程序进入该Activity后,将发生内存泄漏。
使用Android Studio的Memory Monitor工具观测内存使用
Memory Monitor观测内存说明
上图中,动态图表的下面深色部分表示当前应用已分配的内存,上面的浅色部分表示当前时刻释放的内存。
如上图所示动态图表中的每一抖动都代表一次GC事件。
对示例程序内存使用情况进行监测
首先,进入示例应用程序,点击Memory Monitor左边的按钮,进行一次GC,然后进入StaticReferenceActivity界面,再退出该界面,最后再点击按钮执行一遍GC。执行完这个过程的观测结果如下:
从图中可以看出,示例程序在进入StaticReferenceActivity退出后,执行GC后,并没有完全回收掉进入时分配的内存。所以我们怀疑在StaticReferenceActivity界面中对内存的使用出了问题。但具体是什么问题?有时简单的情况下,可能查看代码就很容易发现,我们的示例程序的内存泄漏原因就很容易发现,但这只是为了演示工具的使用,但很多情况下,我们并不能很快通过查阅代码找出内存泄漏问题,这就需要使用工具进行进一步分析定位。
获取hprof文件
在观测到内存使用问题后,可以点击左边的按钮,来生成一个hprof文件,该文件记录着我们应用程序内部的所有数据。但刚生成的这种hprof文件还不能使用MAT工具直接打开,进行分析。我们需要使用下面的命令将hprof该文件从Dalvik格式转换成J2SE格式。
转换输出的hprof文件就可以被MAT工具打开,进行分析了。
使用Eclipse Memory Analyzer(MAT)工具,对hprof文件进行分析
打开hporf文件
上图最中央的那个饼状图展示了最大的几个对象所占内存的比例,这张图中提供的内容并不多,所以不对其进行说明。在这个饼状图的下方就有几个非常有用的工具了。其中最主要就是上图中标红的两个工具。
- Histogram:可以列出内存中每个对象的名字、数量以及大小
- Dominator Tree:会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构
Dominator Tree
下面开始对上图展示的信息进行解读。
Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存。Shallow Heap则表示当前对象自己所占内存的大小,不包含引用关系。
我们还发现,在每一行最左边的文件型图标的左下角,有的会带有一个红色的点。带有红点的对象就表示是可以被GC Roots(相关内容查看“Java内存分配与垃圾收集”一文)访问到的,根据上面的讲解,可以被GC Root访问到的对象都是无法被回收的。但不代表没有红点的对象类型的对象就一定不会被GC Roots访问到。我们也可以发现,每一行都可以被展开,展开后可以看到当前存在于程序中的该种类型的对象数。有的对象类型的后面还有System Class,其说明该种类型是一个系统类型,而不是由用户创建的。
在用Dominator Tree分析内存问题时,我们首先对Retained Heap中最大的进行分析,占用内存最大的部分,也是最容易出现内存泄漏的部分。要分析每一类型对象的内存使用情况,则在其上点击右键 -> Path to GC Roots -> exclude weak references(弱引用可在GC时释放)。下面是在Bitmap类型上执行上述操作的结果:
从图中可以很容易的发现,在StaticReferenceActivity中有一个名为sBitmap的Bitmap对象可以被GC Roots访问到,导致这部分内存不能被释放,我们查看示例程序发现,该对象在程序中被设置为静态,我们知道静态变量会被放入静态存储区中,它的生命周期同应用程序一样长,所以该部分发生内存泄漏。
Histogram
使用Histogram进行内存分析,也是先从占用内存大的对象开始分析。但更经常的是,使用Histogram对你认为可能会造成内存泄漏的对象进行精确分析,例如在我们的示例中,我们进入StaticReferenceActivity界面并退出之后,仍然有一部分内存不能被GC掉,所以我们怀疑StaticReferenceActivity这个对象出现了内存泄漏。我们在Histogram界面最上面的输入框,输入StaticReferenceActivity后,结果如下:
果然程序中还存在一个StaticReferenceActivity没有被回收掉。接下来我们分析具体原因,我们在StaticReferenceActivity上右键-> List objects -> with incoming references。查看具体StaticReferenceActivity实例。然后在实例上右键 -> Path to GC Roots -> exclude weak references。结果如下图:
从上图中可以很容易的发现,在StaticReferenceActivity中有一个名为sStaticReferenceActivity的StaticReferenceActivity对象可以被GC Roots访问到,导致该StaticReferenceActivity实例不能被消耗回收。我们查看示例程序发现,该对象在程序中被设置为静态,我们知道静态变量会被放入静态存储区中,它的生命周期同应用程序一样长,所以该部分发生内存泄漏。
上面简单的说明了查看GC Log和使用分析工具,对android应用内存使用情况进行分析的方法。