【小木箱成长营】内存优化系列文章:
内存优化 · 工具论 · 常见的 Android 内存优化工具和框架
内存优化 · 方法论 · 揭开内存优化神秘面纱
内存优化 · 实战论 · 内存优化实践与应用
Tips: 关注微信公众号小木箱成长营,回复"内存优化"可免费获得内存优化思维导图
一、序言
Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享内存优化 · 基础论 · 初识 Android 内存优化。
本次分享主要分为五个部分内容,第一部分内容是 5W2H 分析内存优化,第二部分内容是内存管理机制,第三部分内容是内存优化 SOP,第四部分内容是 内存优化指导原则, 最后一部分内容是总结与展望。
如果学完小木箱内存优化的基础论、工具论、方法论和实战论,那么任何人做内存优化都可以拿到结果。
二、5W2H 分析内存优化
首先我们说说我们的第一部分内容,5W2H 分析内存优化,5W2H 分析内存优化提出了 7 个高价值问题
-
What: 内存优化定义
-
Why: 内存优化原因
-
How: 内存优化归因
-
Who: 内存优化维度
-
When: 内存优化时机
-
How Much: 内存优化价值
-
Where: 内存痛点定位
What: 内存优化定义
Android 内存优化是指优化 Android 应用程序的内存使用,以减少可用内存的消耗,提高应用程序的性能和可靠性。Android 内存优化可以通过减少内存使用量,减少对资源的消耗,以及提高内存利用率来实现。
Why: 内存优化原因
安卓系统对每个应用程序都有一定的内存限制,当应用程序的内存超过了上限,就会出现 OOM (Out of Memory),也就是 App的异常退出。
因此,要改善系统的运行效率、改善用户体验、降低系统资源占用、延长电池寿命、降低系统故障的危险。
Android通过内存优化,可以减少系统内存使用,让系统更加流畅,运行更快,减少系统Crash,提升用户体验。
How: 内存优化归因
关于应用内存分析,需要重点关注四个阶段
-
应用停留在闪屏页面内存固定值
-
应用的MainActivity到HomeActivty内存波动值
-
应用运行十分钟后回归到HomeActivty内存波动值
-
应用内存使用量分配值汇总
Android 给每个应用进程分配的内存都是非常有限的,那么,为什么不能把图片下载下来都放到磁盘中呢?
因为放在内存中,展示会更“快”,快的原因两点:
-
硬件快:内存本身读取、存入速度快。
-
复用快:解码成果有效保存,复用时,直接使用解码后对象,而不是再做一次图像解码。
那么,问题来了,什么是解码呢?
Android 系统要在屏幕上展示图片的时候只默认“像素缓冲”,而这也是大多数操作系统的特征。jpg,png 等图片格式,是把“像素缓冲”使用不同的手段压缩后的结果。
不同格式的图片,在设备上展示,必须经过一次解码,执行速度会受图片压缩比、尺寸等因素影响。
Who: 内存优化维度
对于 Android 内存优化可以细分为 RAM 和 ROM 两个维度:
1.2.1 RAM 优化
主要是降低运行时内存,RAM 优化目的有以下三个:
-
防止应用发生 OOM。
-
降低应用由于内存过大被 LMK 机制杀死的概率。
-
避免不合理使用内存导致 GC 次数增多,从而导致应用发生卡顿。
1.2.2 ROM 优化
减少程序占用的 ROM,并进行 APK精简。其目标是减少应用程序的占用,防止由于 ROM空间限制而导致程序的安装失败。
When: 内存优化时机
手机不使用 PC 的 DDR 内存,采用的是 LP DDR RAM,也就是“低功率的两倍数据率存储器”。其计算规则如下所示:
LP DDR 系列的带宽=时钟频率 ✖️ 内存总线位数/8
LP DDR4=1600MHZ✖️64/8✖️ 双倍速率=26GB/s。
那么内存占用是否越少越好?
如果当系统内存充足的时候,那么小木箱建议你多用一些内存获得更好的性能。
如果系统内存不足的时候,那么小木箱建议你可以做到“用时分配,及时释放”。
How Much: 内存优化价值
做好内存优化将带来以下三点好处:
第一点好处是减少 OOM,提高应用稳定性。
第二点好处是减少卡顿,提高应用流畅度。
第三点好处是减少内存占用,提高应用后台运行时的存活率。
Where: 内存痛点定位
那么,内存痛点定位主要是有哪几类呢?内存痛点问题通常来说,可以细分为如下三类:
第一,内存抖动。
第二,内存泄漏。
第三,内存溢出。
下面,小木箱带大家来了解下内存抖动、内存泄漏和内存溢出。
1.3.1 内存抖动
1.3.1.4.1 内存抖动定义
内存波动图形呈锯齿状、GC 导致卡顿。内存抖动在 Dalvik 虚拟机上更明显,因为 ART 虚拟机内存管理、回收策略做了优化,所以内存分配、GC 效率提升了 5~10 倍,内存抖动发生概率小。
当内存频繁分配和回收导致内存不稳定,出现内存抖动,内存抖动通常表现为频繁 GC、内存曲线呈锯齿状。
并且,内存抖动的危害严重,会导致页面卡顿,甚至 OOM。
1.3.1.4.2 OOM 原因
那么,为什么内存抖动会导致 OOM?
主要原因有如下两点:
第一,频繁创建对象,导致内存不足及不连续碎片;
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 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。
第二,不连续的内存片无法被分配,导致 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 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。
1.3.1.4.3 内存抖动解决
这里假设有这样一个场景:点击按钮使用 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);
}
};
}
}
请注意,这个代码中的消息循环可能会导致内存泄漏,因此您需要在适当的时候删除消息。
1.3.1.4.4 内存抖动常见案例
下面列举一些导致内存抖动的常见案例,如下所示:
1.3.1.4.1 字符串使用加号拼接
-
实际开发中我们不应该使用字符串使用加号进行拼接,而应该使用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
1.3.1.4.2 资源复用
使用全局缓存池,避免频繁申请和释放的对象。
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)。
1.3.1.4.3 减少不合理的对象创建
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
对象不会被立即回收,因此内存不断增加。
1.3.1.4.4 使用合理的数据结构
使用 SparseArray 类族、ArrayMap 来替代 HashMap。
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");
}
}
1.3.4 内存泄漏
Android 系统虚拟机的垃圾回收是通过虚拟机 GC 机制来实现的。GC 会选择一些还存活的对象作为内存遍历的根节点 GC Roots,通过对 GC Roots 的可达性来判断是否需要回收。
内存泄漏是在当前应用周期内不再使用的对象被 GC Roots 引用,导致不能回收,使实际可使用内存变小。
对象被持有导致无法释放或不能按照对象正常的生命周期进行释放,内存泄漏导致可用内存减少和频繁 GC,从而导致内存溢出,App 卡顿。
public class MainActivity extends AppCompatActivity {
private List<Bitmap> bitmaps = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 不断加载图片并加入到List中
while (true) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image);
bitmaps.add(bitmap);
}
}
}
在上面的代码中,每次加载图片并加入到List
中都不会释放内存,因为List
引用了这些图片,导致图片无法释放,最终造成内存溢出。为了避免内存溢出,你可以考虑使用低内存占用的图片格式,或者在不需要使用图片时主动调用recycle
方法释放图片的内存。
1.3.4 内存溢出
OOM,OOM 时会导致程序异常。Android 设备出厂以后,java 虚拟机对单个应用的最大内存分配就确定下来了,超出值就会 OOM。
单个应用可用的最大内存对应于 /system/build.prop 文件中的 dalvik.vm.heap growth limit。
此外,除了因内存泄漏累积到一定程度导致 OOM 的情况以外,也有一次性申请很多内存,比如说一次创建大的数组或者是载入大的文件如图片的时候会导致 OOM。而且,实际情况下很多 OOM 就是因图片处理不当而产生的。
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.image_view);
// 试图创建大的数组
int[] largeArray = new int[Integer.MAX_VALUE];
// 或者试图载入大的图片
Bitmap largeBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image);
imageView.setImageBitmap(largeBitmap);
}
}
三、内存管理机制
3.1 ART&Dalvik 虚拟机
ART 和 Dalvik 虚拟机使用分页和内存映射来管理内存。ART 和 Dalvik 虚拟机有什么区别呢?
Dalvik 是 Android 系统首次推出的虚拟机,它是一个字节码解释器,把 Java 字节码转换为机器码执行。由于它的设计历史和硬件限制,它的性能较差,但是可以很好地支持多个 Android 设备。
而 ART 则是 Android 4.4(KitKat)发布后推出的一种新的 Java 虚拟机,它把 Java 字节码编译成机器码,在安装应用时一次性编译,因此不需要在运行时解释字节码,提高了性能。ART 的编译技术带来了更快的应用启动速度和更低的内存消耗。
因此,ART 相比 Dalvik,在性能和稳定性方面有了很大的提升,但是由于 ART 把字节码编译成机器码,因此空间占用更大,对于一些低内存的设备来说可能不太适用。
说到这两种虚拟机我们不得不提到 LMK(Low Memory killer)
3.2 LMK 内存管理机制
LMK(Low Memory Killer)是 Android 系统内存管理机制中的一部分,LMK 是用来在内存不足时释放系统中不必要的进程,以保证系统的正常运行。
LMK 机制的底层原理是利用内核 OOM(Out-of-Memory)机制来管理内存。当系统内存不足时,内核会根据各进程的优先级将内存分配给重要的进程,同时会结束一些不重要的进程,以避免系统崩溃。
LMK 机制的使用场景包括:
-
系统内存不足:当系统内存不足时,LMK 机制会帮助系统管理内存,以保证系统正常运行。
-
内存泄漏:当应用存在内存泄漏时,LMK 机制会将泄漏的内存释放掉,以保证系统正常运行。
-
进程优化:LMK 机制可以帮助系统管理进程,以确保系统资源的合理利用。
在系统内存紧张的情况下,LMK 机制可以通过结束不重要的进程来释放内存,以保证系统的正常运行。但是,如果不当使用,它也可能导致应用程序的不稳定。因此,开发者需要合理设计应用程序,避免内存泄露。
下面先从 Java 的内存分配开始说起。
3.3 Java 内存分配
Java 的内存分配区域分为如下五部分:
3.4 Java 内存回收算法
3.4.1 标记清除算法
标记清除算法是最早的内存回收算法,其工作原理是标记出不再使用的对象并将其回收。
标记清除算法步骤
-
标记所有存活的对象。
-
统一回收所有未被标记的对象。
标记清除算法优点
实现比较简单。
标记清除算法缺点
-
标记、清除效率不高。
-
产生大量内存碎片。
3.4.2 复制算法
复制算法是一种将内存分为两个区域的算法,其中一个区域用于存储活动对象,另一个区域用于存储不再使用的对象。
复制算法步骤
-
将内存划分为大小相等的两块。
-
一块内存用完之后复制存活对象到另一块。
-
清理另一块内存。
复制算法优点
实现简单,运行高效,每次仅需遍历标记一半的内存区域。
复制算法缺点
会浪费一半的空间,代价大。
3.4.3 标记整理算法
标记整理算法是标记清除算法和复制算法的结合,其工作原理是先标记出不再使用的对象,再整理内存使得活动对象的内存分配连续
标记整理算法步骤
-
标记过程与 标记-清除算法 一样。
-
存活对象往一端进行移动。
-
清理其余内存。
标记整理算法优点
-
避免标记清除导致的内存碎片。
-
避免复制算法的空间浪费。
标记整理算法缺点
-
时间开销:标记整理算法需要进行两次扫描,一次标记活动对象,一次整理内存,这增加了时间开销。
-
空间开销:由于标记整理算法需要为活动对象留出足够的空间,因此必须移动内存中的一些对象,这会增加空间开销。
-
内存碎片:标记整理算法在整理内存时可能会产生内存碎片,使得未使用的内存碎片不能被有效利用。
-
速度慢:相对于其他垃圾回收算法,标记整理算法的速度较慢,因此不适合需要高效内存管理的场景。
-
效率不稳定:标记整理算法效率受到内存使用情况的影响,如果内存使用情况不均衡,效率会不稳定。
3.4.4 分代收集算法
分代回收算法是一种将内存分为几个代的算法,并对每个代进行不同的回收策略
分代收集算法步骤
-
分配新的对象:新创建的对象分配在新生代中,因为大多数新创建的对象都很快失效,并且删除它们的成本很低。
-
垃圾回收:新生代中的垃圾对象被回收,并且回收算法只涉及到新生代的一小部分。如果一个对象存活到一定时间,它将被移动到老年代。
-
老年代回收:在老年代中,回收算法进行全面的垃圾回收,以确保可以回收所有垃圾对象。
-
整理内存:回收后,内存被整理,以确保连续的内存空间可以分配给新对象。
主流的虚拟机一般用的比较多的是分代收集算法。
分代收集算法优点
-
减少垃圾回收的时间:通过将新生代和老年代分开,分代收集算法可以减少垃圾回收的时间,因为新生代中的垃圾对象被回收的频率较高。
-
减少内存碎片:因为新生代的垃圾回收频率较高,分代收集算法可以防止内存碎片的产生。
-
提高内存利用率:分代收集算法可以有效地回收垃圾对象,提高内存的利用率。
-
减少内存消耗:分代收集算法可以减少对内存的消耗,因为它仅需要涉及小的内存区域,而不是整个 Java 堆。
-
提高系统性能:分代收集算法可以提高系统性能,因为它可以缩短垃圾回收的时间,提高内存利用率,减少内存消耗。
分代收集算法缺点
-
复杂性:分代收集算法相对于其他垃圾回收算法来说更复杂,需要更多的内存空间来管理垃圾回收。
-
内存分配不均衡:分代收集算法可能导致内存分配不均衡,这可能导致新生代内存不足,老年代内存过多。
-
垃圾对象转移次数:分代收集算法需要移动垃圾对象,这可能导致更多的计算开销。
-
时间开销:分代收集算法需要更长的时间来管理垃圾回收,这可能导致系统性能下降。
-
停顿时间:分代收集算法可能导致长时间的停顿,这可能影响系统的实时性。
3.4.5 内存回算法使用推荐
在Java中,两种常用的内存回收算法分别是新生代回收算法和老年代回收算法。
新生代回收算法推荐场景:
-
对象生命周期短:适用于那些生命周期短的对象,因为它们在很短的时间内就会被回收。
-
大量生成对象:对于大量生成对象的场景,新生代回收算法可以有效地减少回收时间。
老年代回收算法推荐场景:
-
对象生命周期长:适用于生命周期长的对象,因为它们不会很快被回收。
-
内存数据稳定:对于内存数据稳定的场景,老年代回收算法可以提高内存效率。
请注意,这是基于Java的默认内存回收算法(即垃圾回收器)的推荐使用场景。您可以通过配置JVM参数来更改这些默认设置,以适应您的特定需求。
3.5 Java 内存管理
Android 中的内存是弹性分配的,分配值与最大值受具体设备影响。
对于 OOM 场景其实可以细分为如下两种:
-
可用(被分配的)内存不足:指系统已经分配了足够的内存,但是由于程序或者其他应用程序的需求,系统中的可用(被分配的)内存不足以支持当前的运行。
-
内存真正不足:指系统中内存总量不足以支持程序的运行,即系统总内存实际上不够用。
因此,在解决内存不足的问题时,需要首先判断是可用(被分配的)内存不足还是内存真正不足,并根据相应情况采取适当的措施。
如果是可用(被分配的)内存不足,可以通过调整程序的内存配置或者关闭其他应用程序来解决问题。
如果是内存真正不足,则需要通过升级内存或者更换计算机等方式来解决问题。
3.6 Java 引用类型
JVM 场景的引用类型有四种,分别是强引用、软引用、软引用和虚引用
强引用、软引用、软引用和虚引用的本质区别可以参考如下表:
引用类型 | GC 回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 永不 | 对象的一般状态 | JVM 停止运行时 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | GC | 对象缓存 | GC 后终止 |
虚引用 | 未知 | 未知 | 未知 |
强引用
强引用概念
强引用是 Java 中最常见的引用类型,当对象具有强引用时,它永远不会被垃圾回收。只有在程序结束或者手动将对象设置为 null
时,才会释放强引用。
强引用案例
public class StrongReferenceExample {
public static void main(String[] args) {
ArrayList<String> data = new ArrayList<>();
data.add("Hello");
data.add("World");
// 创建强引用
ArrayList<String> strongReference = data;
System.out.println("Data before garbage collection: " + strongReference);
// 断开 data 引用,使其可以被回收
data = null;
System.gc();
System.out.println("Data after garbage collection: " + strongReference);
}
}
输出结果:
Data before garbage collection: [Hello, World]
Data after garbage collection: [Hello, World]
在代码中,我们创建了一个 ArrayList 对象 data
,并通过赋值语句将它的引用赋给了变量 strongReference
,此时,strongReference
和 data
将指向同一个对象。
在之后的代码中,我们断开了 data
的引用,让其变成可回收对象,但因为 strongReference
仍然保持着对该对象的强引用,所以该对象在 GC 后仍然不会被回收。
弱引用
弱引用概念
一种用于追踪对象的引用,不会对对象的生命周期造成影响。在内存管理方面,弱引用不被认为是对象的“有效引用”。
因此,如果一个对象只被弱引用指向,那么在垃圾回收的时候,这个对象可能会被回收掉。
弱引用常被用来在内存敏感的应用中实现对象缓存。在这种情况下,弱引用可以让缓存的对象在内存不足时被回收,从而避免内存泄漏。
弱引用案例
public class WeakReferenceExample {
public static void main(String[] args) {
String data = new String("Hello");
// 创建弱引用
WeakReference<String> weakReference = new WeakReference<>(data);
System.out.println("Data before garbage collection: " + weakReference.get());
// 断开 data 引用,使其可以被回收
data = null;
System.gc();
System.out.println("Data after garbage collection: " + weakReference.get());
}
}
输出结果:
Data before garbage collection: Hello
Data after garbage collection: null
在代码中,我们创建了一个字符串对象 data
,并通过创建 WeakReference
对象并将 data
作为参数来创建弱引用。
在之后的代码中,我们断开了 data
的引用,让其变成可回收对象,但因为 weakReference
仅持有对该对象的弱引用,所以当 JVM 进行 GC 时该对象可能会被回收。
可以通过 weakReference.get
方法来检查对象是否被回收。
如果对象已被回收,则 weakReference.get()
返回 null
。
软引用
软引用概念
软引用是比强引用更容易被回收的引用类型。当 Java 堆内存不足时,软引用可能会被回收,以腾出内存空间。如果内存充足,则软引用可以继续存在。
软引用案例
public class SoftReferenceExample {
public static void main(String[] args) {
Object referent = new Object();
SoftReference<Object> softReference = new SoftReference<>(referent);
referent = null;
System.gc();
// 软引用可以在内存不足时被回收
System.out.println(softReference.get());
}
}
输出结果:
java.lang.Object@2f92e0f4
这段代码创建了一个 Object
的实例,并使用它作为 SoftReference
的引用对象。
然后,它将该实例设置为 null
,并试图强制进行垃圾回收。如果内存不足,软引用会被回收,并且可以从 softReference
获取的对象将为 null
。
虚引用
虚引用概念
虚引用是 Java 中最弱的引用类型,对于虚引用,对象只存在于垃圾回收的最后阶段,在这个阶段,对象将被回收,而无论内存是否充足。虚引用主要用于监测对象被回收的状态,而不是用于缓存对象。
虚引用案例
public class PhantomReferenceExample {
public static void main(String[] args) {
Object referent = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(referent, referenceQueue);
referent = null;
System.gc();
// 虚引用在回收前不会被加入引用队列,但在回收时会被加入引用队列
System.out.println(referenceQueue.poll() == phantomReference);
}
}
输出结果:
false
这段代码创建了一个 Object
的实例,并使用它作为 PhantomReference
的引用对象。
然后,它将该实例设置为 null
,并试图强制进行垃圾回收。如果垃圾回收发生,虚引用会被加入引用队列,从而可以从引用队列中获取。
四、内存优化 SOP
分析现状
如果发现 APP 在内存方面可能存在很大的问题,第一方面的原因是线上的 OOM 率比较高。
第二方面的原因是经常会看到在 Android Studio 的 Profiler 工具中内存的抖动比较频繁。
确认问题
这是一个初步的现状,然后在知道了初步的现状之后,进行了问题的确认,经过一系列的调研以及深入研究,最终发现项目中存在以下几点大问题,比如说:内存抖动、内存溢出、内存泄漏,还有 Bitmap 粗犷使用。
问题优化
如果想解决内存抖动,Memory Profiler 会呈现了锯齿张图形,然后我们分析到具体代码存在的问题(频繁被调用的方法中出现了日志字符串的拼接),就能解决内存泄漏或内存溢出。
体验提升
为了不增加业务工作量,使用一些工具类或 ARTHook 大图检测方案,没有任何的侵入性。同时,将技术进行团队分享,团队的工作效率上会有本质提升。
对内存优化工具如 Profiler Memory、MAT 的使用,可以针对一系列不同问题的情况,写一系列解决方案文档,整个团队成员的内存优化意识会更强。
五、内存优化指导原则
万事俱备水滴石穿
做内存优化首先应该学习 Google 内存方面的文档,如 Memory Profiler、MAT 等工具的使用,当在工程遇到内存问题,才能对问题进行排查定位。而不是一开始并没有分析项目代码导致内存高占用问题,就依据自己看的几篇企业博客,不管业务背景,瞎猫碰耗子做内存优化。
结合业务优化内存
如果不结合业务背景,直接对APP运行阶段进行内存上报然后内存消耗进行内存监控,那么内存监控一旦不到位,比如存在使用多个图片库,因为图片库内存缓存不公用的,应用内存占用效率不会有质的飞跃。因此技术优化必须结合业务。
解决方案系统科学
在做内存优化的过程中,Android业务端除了要做优化工作,Android业务端还得负责数据采集上报,数据上报到 APM后台后,无论是Bug追踪人员或者Crash追踪人员,对问题"回码定位"都提供好的依据。
内存劣化Hook魔改
大图片检测方案,大家可能想到去是继承ImageView,然后重写ImageView的onDraw方法实现。但是,在推广的过程中,因为耦合度过高,业务同学很难认可,ImageView之前写一次,为什么要重复造轮子呢? 替换成本非常高。所以我们可以考虑使用类似ARTHook这样的Hook方案。
六、总结与展望
内存优化、启动优化、卡顿优化、包体积优化是 Android 性能优化四驾马车,而内存优化又是四驾马车最难驾驭的一驾,如果你掌握了这项基础技能,那么你将超过绝对多数的 Android 开发
内存优化 · 基础论 · 初识 Android 内存优化我们讲解了五部分内容,第一部分内容是 5W2H 分析内存优化,第二部分内容是内存管理机制,第三部分内容是内存优化 SOP,第四部分内容是内存优化指导原则,最后一部分内容是总结与展望。
下一节,小木箱将带大家深入学习内存优化 · 工具论 · 常见的内存优化工具和框架。
我是小木箱,如果大家对我的文章感兴趣,那么欢迎关注小木箱的公众号小木箱成长营。小木箱成长营,一个专注移动端分享的互联网成长社区。
参考资料
-
抖音 Android 性能优化系列: Java 内存优化篇
-
抖音 Android 性能优化系列:Java OOM 优化之 NativeBitmap 方案
-
拯救 OOM!字节自研 Android 虚拟机内存管理优化黑科技 mSponge
-
腾讯游戏学院专家:UE 手游研发中,如何做好 Android 内存优化?
-
深入探索 Android 内存优化(炼狱级别-上)
-
深入探索 Android 内存优化(炼狱级别-下)
-
微信 Android 终端内存优化实践
-
Android 内存泄露自动化链路分析组件
-
内存优化-4GB 内存时代,再谈内存优化
本文由 mdnice 多平台发布