大纲
- 内存抖动和内存泄漏
- 内存大户,Bitmap内存优化
- Profile内存检测工具
- Mat大对象与泄漏检测
【内存抖动和内存泄漏】
Out Of Memory(内存溢出)
翻译中文就是“内存用完了”,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error,此时称之为溢出。(注:非exception,因为这个问题已经严重到不足以被应用处理)。
为什么会OOM?
- 分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
- 应用用的太多:并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
在Java语言中,由于存在了垃圾自动回收机制,所以,我们一般不用去主动释放不用的对象所占的内存,也就是理论上来说,是不会存在“内存泄露”的。但是,如果编码不当,比如,将某个对象的引用放到了全局的Map中,虽然方法结束了,但是由于垃圾回收器会根据对象的引用情况来回收内存,导致该对象不能被及时的回收。如果该种情况出现次数多了,就会导致内存溢出,比如系统中经常使用的缓存机制。Java中的内存泄露,不同于C++中的忘了delete,往往是逻辑上的原因泄露。
OOM的类型?
JVM内存模型?
按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:
- 程序计数器:当前线程执行的字节码的行号指示器,线程私有
- JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。
- 本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。
- JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。
- 方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。
- 运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。
- 直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。
按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。
最常见的OOM情况有以下三种:
- java.lang.OutOfMemoryError: Java heap space
java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。 - java.lang.OutOfMemoryError: PermGen space
java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。 - java.lang.StackOverflowError
不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
内存抖动
短时间内大量的对象被创建,导致可用内存不足,从而引起频繁gc回收对象,这种已用内存忽高忽低的现象就叫内存抖动。由于gc的过程会 “stop the world” 停止其他的一切工作,gc太频繁无疑会造成界面卡顿,而且gc回收后可能会产生内存碎片,如果这时其他线程需要申请大块内存还有可能发生OOM,所以内存抖动的情况必须要避免。
什么情况会出现内存抖动呢?
这是很常见的字符串拼接操作,通过查看字节码文件可以看出+的拼接字符串实际上是创建StringBuilder对象进行拼接,这样就会出现大量创建对象频繁GC,导致内存抖动。
内存抖动一定是锯齿状吗?
看下面一个实例,实现IOS小菊花的功能
public class IOSStyleLoadingView extends View {
private Context mContext;
private float mStrokeWidth;
private float northwestXStart = 264.57f;
private float northwestYStart = 264.71f;
private float northwestXEnd = 193.72f;
private float northwestYEnd = 194.14f;
private float northXStart = 300;
private float northYStart = 250;
private float northXEnd = 300;
private float northYEnd = 150;
private float notheastXStart = 335.25f;
private float notheastYStart = 264.54f;
private float notheastXEnd = 405.76f;
private float notheastYEnd = 193.63f;
private float eastXStart = 350;
private float eastYStart = 300f;
private float eastXEnd = 450;
private float eastYEnd = 300;
private float southeastXStart = 335.36f;
private float southeastYStart = 335.34f;
private float southeastXEnd = 406.10f;
private float southeastYEnd = 406.02f;
private float southXStart = 300.03f;
private float southYStart = 345f;
private float southXEnd = 300;
private float southYEnd = 450;
private float southwestXStart = 264.68f;
private float southwestYStart = 335.39f;
private float southwestXEnd = 194.06f;
private float southwestYEnd = 406.19f;
private float westXStart = 250;
private float westYStart = 300;
private float westXEnd = 150;
private float westYEnd = 300;
String colorStr[] = new String[]{
"#ffff00",
"#ff3300",
"#ccff00",
"#ff00cc",
"#ccffff",
"#cc99ff",
"#99ff66",
"#993300"
};
private ValueAnimator valueAnimator;
private int currentColor = 0;
public IOSStyleLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
this.mStrokeWidth = UIUtils.dp2px(this.mContext, 5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Sample test1=new Sample("测试1");
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(this.mStrokeWidth);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
Path p0 = new Path();
paint.setColor(Color.parseColor(colorStr[0]));
p0.moveTo(northwestXStart, northwestYStart);
p0.lineTo(northwestXEnd, northwestYEnd);
canvas.drawPath(p0, paint);
Path p1 = new Path();
paint.setColor(Color.parseColor(colorStr[1]));
p1.moveTo(northXStart, northYStart);
p1.lineTo(northXEnd, northYEnd);
canvas.drawPath(p1, paint);
Path p2 = new Path();
paint.setColor(Color.parseColor(colorStr[2]));
p2.moveTo(notheastXStart, notheastYStart);
p2.lineTo(notheastXEnd, notheastYEnd);
canvas.drawPath(p2, paint);
Path p3 = new Path();
paint.setColor(Color.parseColor(colorStr[3]));
p3.moveTo(eastXStart, eastYStart);
p3.lineTo(eastXEnd, eastYEnd);
canvas.drawPath(p3, paint);
Path p4 = new Path();
paint.setColor(Color.parseColor(colorStr[4]));
p4.moveTo(southeastXStart, southeastYStart);
p4.lineTo(southeastXEnd, southeastYEnd);
canvas.drawPath(p4, paint);
Path p5 = new Path();
paint.setColor(Color.parseColor(colorStr[5]));
p5.moveTo(southXStart, southYStart);
p5.lineTo(southXEnd, southYEnd);
canvas.drawPath(p5, paint);
Path p6 = new Path();
paint.setColor(Color.parseColor(colorStr[6]));
p6.moveTo(southwestXStart, southwestYStart);
p6.lineTo(southwestXEnd, southwestYEnd);
canvas.drawPath(p6, paint);
Path p7 = new Path();
paint.setColor(Color.parseColor(colorStr[7]));
p7.moveTo(westXStart, westYStart);
p7.lineTo(westXEnd, westYEnd);
canvas.drawPath(p7, paint);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
}
public void startAnimation() {
valueAnimator = ValueAnimator.ofInt(7, 0);
valueAnimator.setDuration(400);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if ((int) animation.getAnimatedValue() != currentColor){
String b[] = new String[colorStr.length];
for (int c = 0, size = colorStr.length - 1; c < size; c++) {
b[c + 1] = colorStr[c];
}
b[0] = colorStr[colorStr.length - 1];
colorStr = b;
invalidate();
currentColor = (int) animation.getAnimatedValue();
}
}
});
valueAnimator.start();
}
}
这是一个看起来相对平滑的内存走势图,但是也能看出内存一直在增加,不知道为什么我的内存分配Allocations一直为0,希望看到的小伙伴留言解答一下。
上面的代码有三个问题:
1、不断创建String对象,如下代码
paint.setColor(Color.parseColor(color[0]));
点击进去看源码:
public static int parseColor(@Size(min=1) String colorString) {
if (colorString.charAt(0) == '#') {
// Use a long to avoid rollovers on #ffXXXXXX
// 这里调用了String类的substring方法
long color = Long.parseLong(colorString.substring(1), 16);
if (colorString.length() == 7) {
// Set the alpha value
color |= 0x00000000ff000000;
} else if (colorString.length() != 9) {
throw new IllegalArgumentException("Unknown color");
}
return (int)color;
} else {
Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
if (color != null) {
return color;
}
}
throw new IllegalArgumentException("Unknown color");
}
原来这里调用了String类的substring方法,我们都知道String类是不可变类,每一个字符串对应一个String对象,每次调用substring方法截取字符串就要新建一个String对象来接收截取后的值,所以我们选中的String对象就来自这里。
代码修改如下:
int[] colorInt = new int[colorStr.length];
// 构造方法中初始化
for (int i = 0; i < colorStr.length; i++) {
colorInt[i] = Color.parseColor(colorStr[i]);
}
2、不能再onDraw中创建对象
Path mPath = new Path();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 把Paint、Path的创建移出去
mPath.reset();
paint.setColor(colorInt[(currentColor++)%8]);
mPath.moveTo(northwestXStart, northwestYStart);
mPath.lineTo(northwestXEnd, northwestYEnd);
canvas.drawPath(mPath, paint);
}
3、属性动画的addUpdateListener监听中不断创建数组
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if ((int) animation.getAnimatedValue() != currentColor){
// 记录每次回调的int值,去改变颜色的角标
currentColor = (int) animation.getAnimatedValue();
invalidate();
}
}
});
通过修改,内存非常稳定,没有了大量对象的创建。那我们来看一下是否有内存泄漏的问题呢?
使用Profiler工具dump一下,查看内存分配的具体信息,在使用MAT工具查看明细。
可以看到我们刚刚的自定义菊花Activity退出后一直存在强引用,造成内存泄漏。Activity的Context一直被自定义控件持有,再往上看
IOSStyleLoadingView
又IOSStyleLoadingView$1
内部类持有。
注意:
动画一直要记得停止
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (valueAnimator != null){
valueAnimator.removeAllUpdateListeners();
valueAnimator.cancel();
}
}
总结:
软引用与弱引用区别?
/**
* 软引用
*/
Object softObject = new Object();
SoftReference