一、内存抖动定义
内存波动图形呈锯齿状、GC 导致卡顿。内存抖动在 Dalvik 虚拟机上更明显,因为 ART 虚拟机内存管理、回收策略做了优化,所以内存分配、GC 效率提升了 5~10 倍,内存抖动发生概率小。
当内存频繁分配和回收导致内存不稳定,出现内存抖动,内存抖动通常表现为频繁 GC、内存曲线呈锯齿状。
并且,内存抖动的危害严重,会导致页面卡顿,甚至 OOM。
OOM 原因
主要原因有如下两点:
1. 频繁创建对象,导致内存不足及不连续碎片:
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
for (int i = 0; i < 100000; i++) {
// 频繁创建大量的对象
byte[] data = new byte[1024 * 1024];
}
}
});
}
}
在这段代码中,每次点击按钮时都会创建 100,000 个大约为 1MB 的数组,如果内存不够用,则可能导致 OOM 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。
2. 不连续的内存片无法被分配,导致 OOM:
public class MainActivity extends AppCompatActivity {
private Button mButton;
private ArrayList<byte[]> mDataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mDataList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
// 频繁创建大量的对象
byte[] data = new byte[1024 * 1024];
mDataList.add(data);
}
}
});
}
}
在这段代码中,每次点击按钮时都会创建大量的 1MB 大小的数组,并将它们添加到 mDataList
中。由于内存是不连续的,因此在较大的数组中分配这些不连续的内存片可能导致 OOM 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。
内存抖动解决
这里假设有这样一个场景:点击按钮使用 Handler 发送空消息,Handler 的 handleMessage 方法接收到消息后会导致内存抖动
for 循环创建 100 个容量为 10w+的 string[]数组在 30ms 后继续发送空消息。使用 MemoryProfiler 结合代码可找到内存抖动出现的地方。查看循环或频繁调用的地方即可。
public class MainActivity extends AppCompatActivity {
private Button mButton;
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mHandler.sendEmptyMessage(0);
}
});
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
for (int i = 0; i < 100; i++) {
String[] arr = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
}
}
请注意,这个代码中的消息循环可能会导致内存泄漏,因此您需要在适当的时候删除消息。
二、内存抖动常见案例
下面列举一些导致内存抖动的常见案例,如下所示:
字符串使用加号拼接
-
实际开发中我们不应该使用字符串使用加号进行拼接,而应该使用 StringBuilder 来替代。
-
初始化时设置容量,减少 StringBuilder 的扩容。
public class Main {
public static void main(String[] args) {
// 使用加号拼接字符串
String str = "";
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
str = str + "hello";
}
System.out.println("使用加号拼接字符串的内存使用量:" + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024) + " MB");
System.out.println("使用加号拼接字符串的时间:" + (System.currentTimeMillis() - startTime) + " ms");
// 使用StringBuilder
StringBuilder sb = new StringBuilder(5);
startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
sb.append("hello");
}
System.out.println("使用StringBuilder的内存使用量:" + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024) + " MB");
System.out.println("使用StringBuilder的时间:" + (System.currentTimeMillis() - startTime) + " ms");
}
}
输出结果:
使用加号拼接字符串的内存使用量:75 MB
使用加号拼接字符串的时间:4561 ms
使用 StringBuilder 的内存使用量:77 MB
使用 StringBuilder 的时间:4 ms
资源复用
使用全局缓存池,避免频繁申请和释放的对象。
public class ObjectPool {
private static ObjectPool instance = null;
private HashMap<String, Object> pool = new HashMap<>();
private ObjectPool() {}
public static ObjectPool getInstance() {
if (instance == null) {
instance = new ObjectPool();
}
return instance;
}
public void addObject(String key, Object object) {
pool.put(key, object);
}
public Object getObject(String key) {
return pool.get(key);
}
public void removeObject(String key) {
pool.remove(key);
}
}
该代码使用单例模式创建了一个 ObjectPool 类,并实现了添加、获取和删除对象的方法。
当应用程序需要使用某个对象时,可以通过调用 ObjectPool.getInstance().getObject(key) 方法从缓存池中获取该对象。
当不再需要该对象时,可以调用 removeObject(key) 方法将其从缓存池中删除。
但使用后,手动释放对象池中的对象(removeObject 这个 key)。
不合理的对象创建
onDraw 中创建的对象尽量进行复用
public class CustomView extends View {
private Paint paint;
private Rect rect;
public CustomView(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 重复创建对象,导致内存抖动
paint = new Paint();
rect = new Rect();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
rect.set(0, 0, getWidth(), getHeight());
canvas.drawRect(rect, paint);
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 重复创建对象,导致内存抖动
setContentView(new CustomView(this));
}
}
上面的代码中,在CustomView
的onDraw
方法和MainActivity
的onCreate
方法中,每次都重新创建了Paint
和Rect
对象,这会导致内存波动,因为系统并不能回收之前创建的对象。
为了避免这种情况,我们可以将Paint
和Rect
对象声明为类变量,并在构造方法中初始化,以保证只创建一次:
public class CustomView extends View {
private Paint paint;
private Rect rect;
public CustomView(Context context) {
super(context);
// 初始化对象
paint = new Paint();
rect = new Rect();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
rect.set(0, 0, getWidth(), getHeight());
canvas.drawRect(rect, paint);
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CustomView(this));
}
}
每次创建局部变量时,内存都会分配给它,但在循环结束后,它们不会被立即回收。这将导致内存的不断增加,最终导致内存抖动。
在循环中不断创建局部变量
//----------------------------错误示例---------------------------
for(int i=0;i< 100000;i++){
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);
}
//----------------------------正确示例---------------------------
Bitmap bitmap;
for(int i=0;i< 100000;i++){
bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);
bitmap.recycle();
}
在这个例子中,每次循环都会创建一个 Bitmap
对象,并将其赋值给局部变量 bitmap
。但是,循环结束后, Bitmap
对象不会被立即回收,因此内存不断增加。
使用不合理的数据结构
建议使用 SparseArray 类族、ArrayMap 来替代 HashMap。
SparseArray用int[]数组存放key,避免了HashMap中基本数据类型需要装箱的步骤,其次不使用额外的结构体(Entry),单个元素的存储成本下降。
public class Main {
public static void main(String[] args) {
int N = 100000;
// Create a SparseArray
SparseArray<Integer> sparseArray = new SparseArray<>();
for (int i = 0; i < N; i++) {
sparseArray.put(i, i);
}
System.out.println("SparseArray size: " + sparseArray.size());
System.gc();
long memorySparseArray = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
// Create an ArrayMap
ArrayMap<Integer, Integer> arrayMap = new ArrayMap<>();
for (int i = 0; i < N; i++) {
arrayMap.put(i, i);
}
System.out.println("ArrayMap size: " + arrayMap.size());
System.gc();
long memoryArrayMap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
// Create a HashMap
HashMap<Integer, Integer> hashMap = new HashMap<>();
for (int i = 0; i < N; i++) {
hashMap.put(i, i);
}
System.out.println("HashMap size: " + hashMap.size());
System.gc();
long memoryHashMap = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("Memory usage:");
System.out.println("SparseArray: " + memorySparseArray / 1024.0 + " KB");
System.out.println("ArrayMap: " + memoryArrayMap / 1024.0 + " KB");
System.out.println("HashMap: " + memoryHashMap / 1024.0 + " KB");
}
}