文章目录
内存优化方向以及工具简介
内存问题主要体现在以下三个方面
内存抖动:内存占用图呈现锯齿状,容易导致GC频繁,加大卡顿的隐患
内存泄露:可用内存减少、频繁GC
内存溢出:OOM、程序异常
工具选择
Memory Profiler
-
实时图表展示应用内存使用情况
-
识别内存泄露、抖动(呈现锯齿状)
-
提供捕获堆转储(堆栈信息转为文件)、强制GC以及跟踪内存分配的能力
-
方便直观(除了实时展示内存使用情况,甚至还能查看对象创建的地方)
-
线下平时使用
Memory Analyzer (MAT)
- 强大的Java Heap分析工具,查找内存泄露以及内存占用
- 生成整体报告、分析问题等
- 线下使用
Java内存管理机制
Java内存分配
Java内存回收算法
标记-清除算法
-
标记处所有需要回收的对象
-
统一回收所有被标记的对象
缺点:
- 标记和清除效率不高,因为需要对每个对象进行扫描,标记处要回收的对象
- 产生大量不连续的内存碎片,不利于后续对象的内存分配
复制算法
- 将内存划分为大小相等的两块
- 一块内存用完之后复制存活的对象到另一块内存
- 对复制前的那一块进行清除
优点:当存活的对象比较少时,比较高效(是相对标记清除算法的,因为只需要对内存的一般扫描标记)
缺点:需要一块内存作为交换空间来进行对象的移动,或者说是浪费了一半空间,每次只用了一半的内存,代价大。
标记-整理算法
- 标记过程与“标记-清除”算法一样
- 存活的对象往一端进行移动
- 清除其余内存
避免了标记-清除算法导致的内存碎片,避免了复制算法的空间浪费,但是有对象的移动(更新指针),成本也高。
分代收集算法
- 结合多种收集算法优势
- 新生代对象存活率低,就采用复制算法
- 老年代对象存活率高,标记-整理算法
在虚拟机中,是多种算法相结合使用的,并不只是使用单一的算法。
Android内存管理机制
内存弹性分配:对Android设备来说,每打开一个App,并不是每个应用都给固定的内存,用的多也就给的多,是弹性分配的,分配值以及最大值受具体设备的影响,有些高端机给应用分配的内存可以达到500M,可能有的手机就只有100多M。
OOM场景:
- 内存真正不足,一种是达到了应用使用内存的上限阀值就OOM了
- 没有达到应用内存阀值,但是系统可用内存不足,再申请内存的时候就OOM了。
Dalvik与Art区别
- Dalvik仅固定一种回收算法
- Art(从5.0开始默认)回收算法可运行期选择
- Art具备内存整理能力,减少内存空洞
Low Memory Killer
进程分类(优先级):Android系统将尽量长时间地保持应用进程,但为了新进程或运行更重要的进程,需要清除旧进程来回收内存。为了确定保留或终止哪些进程,系统会对进程进行分类,需要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,以此类推,以回收系统资源。下面按优先级从高到低,进行简单的介绍。
前台进程/foreground process
-
托管用户正在交互的Activity(已调用Activity的onResume()方法)
-
托管某个Service绑定到用户正在交互的Activity
-
托管正在“前台”运行的Service(服务已调用startForeground())
-
托管正在执行一个生命周期回调的Service(onCreate()、onStartCommand()、或onDestroy())
-
托管正执行其onReceiver()方法的BroadcastReceiver
可见进程/visible process
- Activity处于onPause, (还没有进入onStop())
- 绑定到前台Activity的Service(和前台进程的区别就是一个正在交互,一个仅仅可见)
服务进程/service process
- 通过 startService() 方法启动的进程
后台进程/background process
- 对用户没有直接影响的进程,Activity处于onStop的时候
空进程/empty process
- 不含任何活动应用组件的进程
- 保留这种进程的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间
- 为使总体系统资源咋进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程
当内存不足时,回收进程从优先级低的开始,同时回收收益,比如到底是回收30M还是300M的进程,也是一种参考因素。
内存抖动
基本介绍
- 定义:内存频繁分配和回收导致内存不稳定
- 表现:频繁GC、内存曲线呈锯齿状
- 危害:导致卡顿,严重时导致OOM
内存抖动导致OOM的原因分析
- 频繁创建对象,导致内存不足以及碎片(不连续)
- 不连续的内存片不易被分配,导致OOM
可通过Memory Profiler跟踪内存分配进行分析定位,内存呈现锯齿状,说明有内存抖动点击Record,随后stop,就可以查看堆栈信息,内存占用过多的对象,选中jump to source就可以定位到代码不规范的地方。此外还可以通过CPU Profiler结合代码进行排查,它可以记录定位哪一段代码消耗了时间,而我们内存抖动的代码一直在不断执行,结合起来是可以定位内存抖动的位置。内存抖动是由于频繁申请内存,频繁GC,所以出现内存抖动优先去找循环或者频繁调用的地方。
内存泄露
定义:垃圾对象无法被GC正常回收
表现:内存抖动、可用内存逐渐变少
危害:内存不足、GC频繁、OOM
关于内存泄露这一块想多说一点,到底什么是内存泄露呢?就是内存不在GC掌控之内了,原因就是GC垃圾回收机制对垃圾对象无法回收,从而导致垃圾对象持的内存持续占用,无法释放,造成可用内存减少,如果可用内存不断减少,就容易引发OOM。那么问题来了,GC判定一个对象是否可回收的依据是什么呢?如果一个对象被别的对象引用了,就不能被GC回收,对吗?答案是否定的,提一下强引用、软引用、弱引用、虚引用,你就知道了。在内存不足的时候,后面三种引用都可以被回收的。
常说的GC(Garbage Collector) Roots,特指的是垃圾收集器(Garbage Collector)的对象,GC Roots就是Java虚拟机中所有引用的根对象。我们都知道,垃圾回收器不会回收GC Roots以及那些被它们间接强引用的对象
一个对象可以属于多个root,GC Roots有以下几种:
- Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的Java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
- Thread - 活着的线程
- Stack Local - Java方法的local变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Monitor Used - 用于同步的监控对象
- Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。
Memory Analyzer
下载链接:https://www.eclipse.org/mat/downloads.php
转换:hprof-conv 原文件路径 转换后文件路径
下面举个例子,具体说说MAT的使用,在此之前还有一种方式可以简单的判断是否发生了内存泄露,就是通过adb shell dumpsys meminfo packageName -d 命令查看内存情况,会显示许多信息,但是只要查看Objects部分即可:
在自己测试demo中,在MainActivity跳转到TestActivity,并调用finish方法,结束掉了MainActivity,但是Activities为2,再看前面的AppContexts为4,盲猜是Context的使用出问题了。这个命令只是一个初步的判断,详细的分析还是通过MAT工具,下面回到MAT的具体使用
首先Dump Java heap信息,生成并导出导出hprof文件
此时的hprof还不能立即使用,还需要进行转换:
可通过hprof-conv -z 原文件名 转换后文件名
接下来就通过MAT的File—>Open Heap dump 打开文件,点击Histogram分析内存泄露问题。为了便于查看,可进行按包名分组查看
根据包名找到相关对象列表,选中本该回收的对象MainActivity,查看外部引用
选中结果,在Path To GC Root 中,选中除去软,弱,虚引用
高潮来了,我们看到是instance,位置在AppSettings类中,
再去排查AppSettings.java 代码,发现了问题所在,在MainActivity通过AppSettings.getInstance(this) 初始化AppSettings时,但是静态的instance对象间接的持有了MainActivity的引用,长生命周期的对象持有短生命周期的对象,导致MainActivity对象不能被回收,
传参时将context改为context.getApplicationContext()即可。
context.getApplicationCotext() 即可解决这个问题。
public class AppSettings {
private static volatile AppSettings instance;
private static Context mContext;
public AppSettings(Context context) {
mContext = context;
}
public static AppSettings getInstance(Context context) {
if (instance == null) {
synchronized (AppSettings.class) {
if (instance == null) {
//造成内存泄露
instance = new AppSettings(context);
// instance = new AppSettings(context.getApplicationContext());
}
}
}
return instance;
}
}
这只是通过一个问题,进行简单的分析,真实的情况是,项目代码繁多,可选择重点页面的进入和退出进行Dump Java heap,转换hprof,然后进行前后比对分析,就可以得到发生内存泄露的对象。MAT的使用就记录到这里了。
ARTHook优雅检测不合理图片
Bitmap内存模型
- API10之前Bitmap自身在Dalvik Heap中,像素信息保存在Native
- API10之后像素也被放在Dalvik Heap中
- API26之后像素在Native
2.3之前的像素存储需要的内存是在native上分配的,并且生命周期不太可控,可能需要用户自己回收。 2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上,当然,4.4之前的甚至能在匿名共享内存上分配(Fresco采用),而8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,当java层bitmap被回收后,能及时回收native层的像素数据。8.0之后图像资源的管理更加优秀,极大降低了OOM。
获取bitmap占用内存的方式
- getByteCount(): 运行时动态获取
- 宽x高x一个像素占用内存:需要注意的是图片放在的drawable目录,宽高会有一个压缩(拉伸)比例,此外一个像素占用内存大小与色彩存储格式有关如RGB_565一个像素占两个字节,ARGB_8888表示一个像素占4个字节。
不合理图片处理的常规方式
- 背景:图片对内存优化至关重要、图片宽高远大于控件宽高(如果直接load到内存,造成额外开销)
- 实现:继承ImageView,复写onDraw方法,绘制时计算图片大小,如果超出控件大小一定比例,警告提示
- 缺陷:侵入性强、不通用
ARTHook介绍
挂钩,将额外的代码钩住原有的方法,在执行原来的方法的时候,也会执行我们额外的代码,达到修改执行逻辑的目的。ARTHook就是这样一种方案,它有一些应用场景,如运行时插桩,做一些性能分析等等。
为了有一个更直观的认识,下面介绍一下Epic以及它的简单使用,Epic是一个虚拟机层面、以java Method为粒度的运行时Hook框架,添加依赖后,通过继承XC_MethodHook ,实现相应逻辑,通过DexposedBridge.findAndHookMethod注入Hook,让框架知道我们想要Hook哪个类的具体方法。
public class ImageHook extends XC_MethodHook {
/**
* Hook方法之后,添加比对宽高的逻辑
*/
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 实现我们的逻辑
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
}
/**
* 通过比比较宽高,判断是否合理
* @param thiz
* @param drawable
*/
private static void checkBitmap(Object thiz, Drawable drawable) {
if (drawable instanceof BitmapDrawable && thiz instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final View view = (View) thiz;
//控件的宽度
int width = view.getWidth();
//控件的高度
int height = view.getHeight();
if (width > 0 && height > 0) {
// Bitmap宽高都大于view宽高的2倍以上,则警告
if (bitmap.getWidth() >= (width << 1)
&& bitmap.getHeight() >= (height << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = new StringBuilder("Bitmap size too large: ")
.append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
.append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
.append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
.toString();
LogUtils.i(warnInfo);
}
}
具体的注入Hook代码
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
//传入需要Hook的方法名,参数类型等等
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
}
});
ARTHook小结
-
无侵入性
-
通用性强
-
兼容问题大,不能带到线上环境
避免可控的内存泄露
前面我们提到过内存泄露是因为垃圾对象无法被GC回收,本质是生命周期短的对象被生命周期长的对象引用,导致无法被正常回收。下面列举一些常见的内存泄露场景,以及如何避免。
非静态内部类导致的内存泄露
非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接的长期维持着外部类的引用,阻止被系统回收。
public class MemoryLeakActivity extends AppCompatActivity {
private static Object inner;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.bt_next);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
createInnerClass();
finish();
}
});
}
void createInnerClass() {
class InnerClass {
}
inner = new InnerClass();//1
}
}
当我们点击button的时候,通过注释1处的代码创建了非静态内部类InnerClass的实例,赋值给静态变量inner后,该实例的生命周期和应用程序生命周期一样长,并且持有MemoryLeakActivity的引用,导致MemoryLeakActivity无法被回收。将非静态内部类改为静态,即可避免这种方式造成的内存泄露。涉及到内部类的时候,一定要注意静态问题。
Handler造成的内存泄露
如下代码,就是一种常见的使用场景。Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,它们在MessageQueue中存在的时间不确定,有可能会很长,持有MHandler的引用,而mHandler又持有当前Activity的引用,所以当退出当前Activity的时候,如果MessageQueue中海油Message,Activity就不能被回收。
public class MemoryLeakActivity extends AppCompatActivity {
private static int FLAG = 100;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
mHandler.sendEmptyMessageDelayed(FLAG, 10000);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.sendEmptyMessage(FLAG);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeMessages(FLAG);//3
}
}
而我们通常采用的方是添加注释3处的代码,在退出Activity的时候,移除Message,就不再持有它的引用,避免了内存泄露。还有采用静态的Handler , 以及对当前Activity采用WeakReference弱引用。当采用如注释1处静态的Handler的时候,和静态内部类一样,就不再持有Activity的引用了,避免了内存泄露,但是频繁使用static,就会导致jvm加载类的时候消耗过多。另外就是弱引用了,如注释2处代码,GC回收是,就会自动被清除,避免了内存泄露,但是如果你的业务场景是Activity关闭后,延时或定时执行某项任务需要调用Activity的方法,这个时候就有问题了,因为弱引用的Activity可能已经被回收了。因为不确定延时时间,和GC回收哪一个先执行。所以说没有最优的方案,只有最适合的场景,一定要结合自身业务场景进行选择和取舍。
没有正确使用Context
对于不是必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们可以考虑使用Application Context来代替Activity的Context,这样可以避免Activity泄露,比如如下的单例模式:
public class Singleton {
private static volatile Singleton singleton;
private Context mContext;
private Singleton(Context context) {
mContext = context;
}
public static Singleton getInstance(Context context) {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton(context);
//singleton = new Single(context.getApplicationContext());
}
}
}
return singleton;
}
}
在Activity中调用了Singleton.getInstance(this); 作为静态的singleTon,它的生命周期长于Activity,当退出Activity的时候,由于singleTon持有Activity的引用,导致Activity不能被回收,将参数context改为context.getApplicationContext(),就避免了内存泄露。
注册监听未移除
很多服务需要register和unregister监听器,我们需要在合适的时候及时unregister那些监听器。自己手动add的Listener,要记得在合适的时候及时remove。
Bitmap未及时释放
临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。
避免静态变量持有比较大的bitmap对象或者其他大的数据对象,如果已经持有,要尽快置空该静态变量。
资源对象未关闭
在Activity中调用了Singleton.getInstance(this); 作为静态的singleTon,它的生命周期长于Activity,当退出Activity的时候,由于singleTon持有Activity的引用,导致Activity不能被回收,将参数context改为context.getApplicationContext(),就避免了内存泄露。