前言

Java 没有指针;Java 自带内存回收机制,不用管理内存。
这大概是很多不负责任的大学老师教给同学们的错误概念。

虽然 Java 虚拟机有一套内存回收机制,但是我们写程序仍然需要注意内存管理,并不是传说中的只申请不释放,内存和性能是程序永恒的话题,Android 开发中卡顿往往都是由于内存的问题。

最近在着手公司项目的内存优化,做个总结,下文全是干货。

Java 对象的生命周期

上学的时候老师讲课为了让同学们容易理解把 Java 内存分为堆(Heap)和栈(Stack),在《深入 Java 虚拟机》书中讲到完整版应该是这样的,在 Java 世界中,内存分为:

  1. 虚拟机栈(java stacks)
  2. 堆(heap)
  3. 方法区(method area)
  4. PC 寄存器/程序计数器(pc registers)
  5. 本地方法栈(native method stacks)

这里就不过多介绍每一个内存区域都是做什么用的,详情查看《深入 Java 虚拟机》一书。

出生

死亡

  1. 引用计数

  2. 根集算法

Java 语言的特性

多态

1
List<String> strs = new ArrayList<String>();

父类引用指向子类对象,使其可以调用子类的方法。其原理是虚表,学过 C++的同学对这个应该很熟悉。

JIT 影响 for 循环的执行效率

JIT - just in time,即时编译技术。使用该技术,能够加速 java 程序的执行速度。

1
2
3
4
5
static class Foo {
int mSplat;
}

Foo[] mArray = ...
1
2
3
4
5
6
public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; i++) {
sum += mArray[i].mSplat;
}
}
1
2
3
4
5
6
7
8
public void one() {
int sum = 0;
final Foo[] localArray = mArray;
int length = localArray.length;
for (int i = 0; i < length; i++) {
sum += localArray[i].mSplat;
}
}
1
2
3
4
5
6
public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}

有上面三个zero() one() two() 三个 for 循环的函数,其中:

  • zero() 最慢,因为 JIT 不能去优化 mArray.length,无论有没有 JIT 都是最慢的;
  • one()zero() 快,无论有没有 JIT;
  • two() 在没有 JIT 的情况下最快,而如果有 JIT,其速度几乎和zero()一样慢。

一般情况下,应该使用two()这种增强型 for 循环,如果性能敏感的情况下,遍历 ArrayList 这样的线性结构的数据,应该采用one()

内部类

非静态内部类天生持有宿主类的一个引用,所以如果内部类泄露了会导致宿主类也跟着遭殃。

Android 平台的特性

onTrimMemory()

当内存不足时,系统会主动触发onTrimMemory(),这时你应该主动回收掉一些不用的内存。

数据结构选择

Bitmap —> LruCache

HashMap — > ArrayMap / SparseArray

List —> LinkedList / ArrayList

不推荐使用:
Vector
Enum

findView

这个是个比较耗时的操作,所以应该避免一个 View 多次 find。

Android 平台内存调优方法

通过 adb shell 查看内存泄露

1
adb shell dumpsys meminfo 包名

Dump HPROF file

MAT(Eclipse Memory Analysis Tool)

常见案例

单例

这个比较常见,就是声明了单例,如果里面有大内存数据,在程序退出的时候应该主动去回收掉。

内部类 泄露

经典例子为 Handler 作为内部类时发生的泄露。

静态引用

1
2
3
4
5
6
7
8
9
public class ProgressHelper {

private static TextView mTextView;

public static void update(String text) {
mTextView.setText(text);
}

}

有时候业务需要使用静态应用会得到一定的便利,但是请注意这是很危险的,如果一定要这样做,请使用 WeakReference 包一层。

1
2
3
public class ProgressHelper {
private static WeakReference<TextView> mWeakTextView;
}

Context 泄露

一般来说等同于 Activity 泄露

Bitmap 泄露

  • Bitmap 对象需要手动调用 recycle
  • 切记不要把一张巨大的图,不缩放就放入控件里了。

    如果一张图是2000*2000,比手机分辨率还大,但是你程序的 ImageView 只有300*300的大小,你需要先把2000*2000的图缩小成300*300再设置给 ImageView。这样一定不会出现 OOM 的情况。

飞线

一个类使用了另一个类的静态资源。

A.java
1
public static boolean isOk = false;
B.java
1
2
3
if (A.isOK) {

}

这种情况和静态引用一样,使用 WeakReference 包一层,即可。

内存抖动

内存的不停申请和释放会导致内存抖动,最可能出问题的就是频繁的使用 “” + “” 的方式生产新的字符串。

1
2
3
4
5
String newStr = "";

for (int i = 0; i < x.length; i++) {
newStr += x[i].str;
}

因为字符串相加得到的是一个新的字符串,所以如果遇到这种情况,应该使用StringBuilder

1
2
3
4
5
6
7
StringBuilder sb = new StringBuilder();

for (int i = 0; i < x.length; i++) {
sb.append(x[i].str);
}

String newStr = sb.toString();;

经验

开源库

相关资料