【转自】https://yq.aliyun.com/articles/225751?utm_content=m_32842
内存问题一直是大型App的开发人员比较头痛的问题,特别是像手淘这种超级的App,App中到处都是带有图片和视频的界面,而且这些功能都是由不同的团队甚至不同的事业部开发的,要整体上去管控和排查内存的问题变得相当的复杂。之前,我们多个线上版本都存在着严重的Activity等内存泄漏和不合理内存使用。这不是偶然,一个很重要的原因就是我们很多的开发测试人员侧重业务开发,忽略内存和性能,而且没有站在全局性的角度去考虑资源的使用。认为我自己的模块多缓存一些就会加快速度,以空间换时间看似正确,但是在手淘这样的超级App中是不可取的,需要严格限制,否则不要说里面几百个模块,有几十个模块这样来做,其结果都会是灾难性的,不但没有加快速度,反而会拖慢速度以及带来很多稳定性问题。
经过一年多的更新,现在的Android Studio所带的工具已经相当的成熟。以前我们还停留在使用MAT来分析内存,但是现在Android Studio的内存分析工具已经相当的强大,已经完全可以抛开MAT来实现更为直观的内存检测。我想,作为一个大型App的开发人员和测试人员,掌握这些技能都是必不可少的,也是提高整个App质量的关键所在。
在使用工具分析内存之前,我们需要了解一下内存回收上的一些策略,否则很多时候排查到的可能都不是真正的问题。 我们知道,Java的内存回收如果有强引用存在,那么这个对象的内存不会回收。那么这个对象的引用如果不存在,是不是这块内存就会回收呢?答案是否定的,VM有自己的回收策略,不会因为一个对象的引用为空了就立马对它进行回收。也就是说,回收策略需要达到一定的触发阈值。我们可以看一个Demo,写如下的分配对象的方法:
void onButtonClick(){
for (int i = 0; i < 1000; i++) {
View view = new ImageView(this);
}
}
在内存充足的情况下,我们点击按钮4次,执行了4遍该函数,这个时候可以看到堆内存呈现了4次增长。如下图所示:
静置在那半个小时,内存仍然会维持现状,VM并不会来执行实际的GC。我们可以Dump内存看看内存中的对象:
通过手动触发GC,我们可以来主动回收这块内存,点击如下图所示的按钮,触发GC。
当然,这里还有一个问题存在,因为刚才创建的属于Finalizer对象,该对象前面的文章已经分析过,需要至少两次GC才能真正回收其内存。所以,第一次触发GC的时候,我们可以看一下内存的变化。
我们将测试代码后面加上主动调用GC的代码,如下:
void onButtonClick(){
for (int i = 0; i < 1000; i++) {
View view = new ImageView(this);
}
System.gc();
}
在不同的Android版本上看下执行效果,点击按钮执行多次。在Android 4.4.4版本的设备上,内存基本已经回收,如果将1000次修改为10次,偶尔可以看到有ImageView已经没有引用存在,但是仍然没有回收。如下图所示:
从这里可以看出来,在该版本下,大部分的对象在没有强引用后,调用System.gc()就会被回收。我们再看下ART上的情况,在ART下,却发生了不一致的表现。在调用后没有进行GC。查看堆内存,我们也可以看到,4000个ImageView仍然存在,并未执行GC。
看来在ART后,这部分的GC策略做了调整,System.gc()没有内存回收。我们可以看下源码,在Android 4.4.4下的源码:
/**
* Indicates to the VM that it would be a good time to run the
* garbage collector. Note that this is a hint only. There is no guarantee
* that the garbage collector will actually be run.
*/
public static void gc() {
Runtime.getRuntime().gc();
}
我们可以看到,在调用gc函数后,直接调用了运行时的gc。再来看下5.0上的代码:
/**
* Indicates to the VM that it would be a good time to run the
* garbage collector. Note that this is a hint only. There is no guarantee
* that the garbage collector will actually be run.
*/
public static void gc() {
boolean shouldRunGC;
synchronized(lock) {
shouldRunGC = justRanFinalization;
if (shouldRunGC) {
justRanFinalization = false;
} else {
runGC = true;
}
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
}
/**
* Whether or not we need to do a GC before running the finalizers.
*/
private static boolean runGC;
/**
* If we just ran finalization, we might want to do a GC to free the finalized objects.
* This lets us do gc/runFinlization/gc sequences but prevents back to back System.gc().
*/
private static boolean justRanFinalization;
/**
* Provides a hint to the VM that it would be useful to attempt
* to perform any outstanding object finalization.
*/
public static void runFinalization() {
boolean shouldRunGC;
synchronized(lock) {
shouldRunGC = runGC;
runGC = false;
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
Runtime.getRuntime().runFinalization();
synchronized(lock) {
justRanFinalization = true;
}
}
从源码我们可以发现,System.gc()函数,已经有了变化,从5.0开始,多了一些判断条件。是否执行gc,是依赖于justRanFinalization变量,而变量justRanFinalization在runFinalization后才会变为true。也就是说,直接调用System.gc()方法并没有调用Runtime.getRuntime().gc(),只是做了一个标记将runGC变量设置为true。
void onButtonClick(){
for (int i = 0; i < 2000; i++) {
View view = new ImageView(this);
}
Runtime.getRuntime().gc();
}
这里直接调用了Runtime.getRuntime().gc()。这个时候内存回收确实会有变化。但是Finalizer对象仍然可能存在,在Android 5.0的时候会回收一部分,但是从6.0开始,单次调用gc,Finalizer对象却不一定回收。如下图所示,虽然所有引用链路已经不复存在,但是内存仍然没有回收:
void onButtonClick(){
for (int i = 0; i < 2000; i++) {
View view = new ImageView(this);
}
Runtime.getRuntime().gc();
Runtime.getRuntime().gc();
}
public class MyThread extends Thread{
@Override
public void run() {
View view = new View(MainActivity.this);
view = null;
}
}
void onButtonClick(){
new MyThread().start();
}
这里只是创建了一个该对象,但是也很容易可以跟踪到该对象的内存情况。通过Memory Monitor的【Dump Java Heap】按钮可以把当前堆内存显示出来,如下图所示:
这里是默认查看方式,我们可以切换到以包名的形式查看。这样就可以很容易的找到我们自己的代码了。如下所示:
通过上图我们可以清晰的看到,在ju包名下的内存分布情况。
所以,在内存占用的检查上,Android Studio已经给我们提供了强大的工具,各个开发和测试人员已经可以很简单的查看到自己开发的模块占据的内存大小,然后对内存的使用做一些改进等措施。这里也可以通过右侧的Instance窗口检查各个实例的引用以及排查不合理的内存强制占用。对于Android Studio提供的内存分析工具,使用起来非常简单,会比Mat工具要快捷,排查问题也更加容易。所以Android的开发和测试人员,应该尽可能的都迁移到该工具上来,并能够熟练的掌握内存分析工具,这样才能让自己开发的模块质量更加的优秀。
以上主要是针对Android Studio 2中的使用方式,在今年的Android Studio 3 Preview版本中,内存这块的分析工具更加强大,可以在面板上直接看到更细粒度的内存占用,不仅仅是Java的对内存了。
对于需要更细粒度和更全面的分析一些内存的细节,本文所涉及的内存知识还是不够的,还需要了解Linux下的内存机制以及Android下的一些内存机制,例如按页分配,共享内存,GPU内存等。