【小木箱成长营】内存优化系列文章:
内存优化 · 工具论 · 常见的 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,晋升用户体验。
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/10c05b0b0bc24064ba2cb543bc6101bf~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
How: 内存优化归因
对于利用内存剖析,须要重点关注四个阶段
- 利用停留在闪屏页面内存固定值
- 利用的 MainActivity 到 HomeActivty 内存稳定值
- 利用运行十分钟后回归到 HomeActivty 内存稳定值
- 利用内存使用量调配值汇总
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4badb1d4837a49a4904edfd72a44f0b3~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
Android 给每个利用过程调配的内存都是十分无限的,那么,为什么不能把图片下载下来都放到磁盘中呢?
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/97a50be070f0486f860606dee278b1cb~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
因为放在内存中,展示会更“快”,快的起因两点:
- 硬件快:内存自身读取、存入速度快。
- 复用快:解码成绩无效保留,复用时,间接应用解码后对象,而不是再做一次图像解码。
那么,问题来了,什么是解码呢?
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/324c6d09fe5645d59328774054ad600a~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
Android 零碎要在屏幕上展现图片的时候只默认“像素缓冲”,而这也是大多数操作系统的特色。jpg,png 等图片格式,是把“像素缓冲”应用不同的伎俩压缩后的后果。
不同格局的图片,在设施上展现,必须通过一次解码,执行速度会受图片压缩比、尺寸等因素影响。
Who: 内存优化维度
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e84f72255d3640d790f9e3883f75597e~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
对于 Android 内存优化能够细分为 RAM 和 ROM 两个维度:
1.2.1 RAM 优化
次要是升高运行时内存,RAM 优化目标有以下三个:
- 避免利用产生 OOM。
- 升高利用因为内存过大被 LMK 机制杀死的概率。
- 防止不合理应用内存导致 GC 次数增多,从而导致利用产生卡顿。
1.2.2 ROM 优化
缩小程序占用的 ROM,并进行 APK 精简。其指标是缩小应用程序的占用,避免因为 ROM 空间限度而导致程序的装置失败。
When: 内存优化机会
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/81d32b0e9ed348cba8231aef280fb0de~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
手机不应用 PC 的 DDR 内存,采纳的是 LP DDR RAM,也就是“低功率的两倍数据率存储器”。其计算规定如下所示:
LP DDR 系列的带宽 = 时钟频率 ✖️ 内存总线位数 /8
LP DDR4=1600MHZ✖️64/8✖️ 双倍速率 =26GB/s。
那么内存占用是否越少越好?
如果当零碎内存短缺的时候,那么小木箱倡议你多用一些内存取得更好的性能。
如果零碎内存不足的时候,那么小木箱倡议你能够做到“用时调配,及时开释”。
How Much: 内存优化价值
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/14029d4e68d740539522ecedc23c88e2~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
做好内存优化将带来以下三点益处:
第一点益处是缩小 OOM,进步利用稳定性。
第二点益处是缩小卡顿,进步利用晦涩度。
第三点益处是缩小内存占用,进步利用后盾运行时的存活率。
Where: 内存痛点定位
那么,内存痛点定位次要是有哪几类呢?内存痛点问题通常来说,能够细分为如下三类:
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b3701cea5d6542bdb3d9e0195bd0ea47~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
第一,内存抖动。
第二,内存透露。
第三,内存溢出。
上面,小木箱带大家来理解下内存抖动、内存透露和内存溢出。
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12dc4086a8394340978d34d5f419efb9~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
1.3.1 内存抖动
1.3.1.4.1 内存抖动定义
内存稳定图形呈锯齿状、GC 导致卡顿。内存抖动在 Dalvik 虚拟机上更显著,因为 ART 虚拟机内存治理、回收策略做了优化,所以内存调配、GC 效率晋升了 5~10 倍,内存抖动产生概率小。
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88c1653b438f49f4b87c8dcdb3c53eb5~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
当内存频繁调配和回收导致内存不稳固,呈现内存抖动,内存抖动通常体现为频繁 GC、内存曲线呈锯齿状。
并且,内存抖动的危害重大,会导致页面卡顿,甚至 OOM。
1.3.1.4.2 OOM 起因
那么,为什么内存抖动会导致 OOM?
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/24b28b98e2114fe784317a73049aae7f~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
次要起因有如下两点:
第一,频繁创建对象,导致内存不足及不间断碎片;
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 虚拟机有什么区别呢?
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ca8814a288d4370a7037873c623bbbf~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
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 的内存调配区域分为如下五局部:
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a32919052d3e4076a9667a963e5b3116~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/03dfca24d925497988a7c6ffff949e2b~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
3.4 Java 内存回收算法
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/655ab752be0340ff8fd2d2bb3f999956~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
3.4.1 标记革除算法
标记革除算法是最早的内存回收算法,其工作原理是标记出不再应用的对象并将其回收。
标记革除算法步骤
- 标记所有存活的对象。
- 对立回收所有未被标记的对象。
标记革除算法长处
实现比较简单。
标记革除算法毛病
- 标记、革除效率不高。
- 产生大量内存碎片。
3.4.2 复制算法
复制算法是一种将内存分为两个区域的算法,其中一个区域用于存储流动对象,另一个区域用于存储不再应用的对象。
复制算法步骤
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/777e7761b8e1447db5e0f52f2bc0c67b~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
- 将内存划分为大小相等的两块。
- 一块内存用完之后复制存活对象到另一块。
- 清理另一块内存。
复制算法长处
实现简略,运行高效,每次仅需遍历标记一半的内存区域。
复制算法毛病
会节约一半的空间,代价大。
3.4.3 标记整顿算法
标记整顿算法是标记革除算法和复制算法的联合,其工作原理是先标记出不再应用的对象,再整顿内存使得流动对象的内存调配间断
标记整顿算法步骤
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2586c4692ead4485b95cfa0ee3e2a5c2~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
- 标记过程与 标记 - 革除算法 一样。
- 存活对象往一端进行挪动。
- 清理其余内存。
标记整顿算法长处
- 防止标记革除导致的内存碎片。
- 防止复制算法的空间节约。
标记整顿算法毛病
- 工夫开销:标记整顿算法须要进行两次扫描,一次标记流动对象,一次整顿内存,这减少了工夫开销。
- 空间开销:因为标记整顿算法须要为流动对象留出足够的空间,因而必须挪动内存中的一些对象,这会减少空间开销。
- 内存碎片:标记整顿算法在整顿内存时可能会产生内存碎片,使得未应用的内存碎片不能被无效利用。
- 速度慢:绝对于其余垃圾回收算法,标记整顿算法的速度较慢,因而不适宜须要高效内存治理的场景。
- 效率不稳固:标记整顿算法效率受到内存应用状况的影响,如果内存应用状况不平衡,效率会不稳固。
3.4.4 分代收集算法
分代回收算法是一种将内存分为几个代的算法,并对每个代进行不同的回收策略
分代收集算法步骤
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b3878d74750b4abfa652aa7bda22e98e~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
- 调配新的对象:新创建的对象调配在新生代中,因为大多数新创建的对象都很快生效,并且删除它们的老本很低。
- 垃圾回收:新生代中的垃圾对象被回收,并且回收算法只波及到新生代的一小部分。如果一个对象存活到肯定工夫,它将被挪动到老年代。
- 老年代回收:在老年代中,回收算法进行全面的垃圾回收,以确保能够回收所有垃圾对象。
- 整顿内存:回收后,内存被整顿,以确保间断的内存空间能够调配给新对象。
支流的虚拟机个别用的比拟多的是分代收集算法。
分代收集算法长处
- 缩小垃圾回收的工夫:通过将新生代和老年代离开,分代收集算法能够缩小垃圾回收的工夫,因为新生代中的垃圾对象被回收的频率较高。
- 缩小内存碎片:因为新生代的垃圾回收频率较高,分代收集算法能够避免内存碎片的产生。
- 进步内存利用率:分代收集算法能够无效地回收垃圾对象,进步内存的利用率。
- 缩小内存耗费:分代收集算法能够缩小对内存的耗费,因为它仅须要波及小的内存区域,而不是整个 Java 堆。
- 进步零碎性能:分代收集算法能够进步零碎性能,因为它能够缩短垃圾回收的工夫,进步内存利用率,缩小内存耗费。
分代收集算法毛病
- 复杂性:分代收集算法绝对于其余垃圾回收算法来说更简单,须要更多的内存空间来治理垃圾回收。
- 内存调配不平衡:分代收集算法可能导致内存调配不平衡,这可能导致新生代内存不足,老年代内存过多。
- 垃圾对象转移次数:分代收集算法须要挪动垃圾对象,这可能导致更多的计算开销。
- 工夫开销:分代收集算法须要更长的工夫来治理垃圾回收,这可能导致系统性能降落。
- 进展工夫:分代收集算法可能导致长时间的进展,这可能影响零碎的实时性。
3.4.5 内存回算法应用举荐
在 Java 中,两种罕用的内存回收算法别离是新生代回收算法和老年代回收算法。
新生代回收算法举荐场景:
- 对象生命周期短:实用于那些生命周期短的对象,因为它们在很短的工夫内就会被回收。
- 大量生成对象:对于大量生成对象的场景,新生代回收算法能够无效地缩小回收工夫。
老年代回收算法举荐场景:
- 对象生命周期长:实用于生命周期长的对象,因为它们不会很快被回收。
- 内存数据稳固:对于内存数据稳固的场景,老年代回收算法能够进步内存效率。
请留神,这是基于 Java 的默认内存回收算法(即垃圾回收器)的举荐应用场景。您能够通过配置 JVM 参数来更改这些默认设置,以适应您的特定需要。
3.5 Java 内存治理
Android 中的内存是弹性调配的,调配值与最大值受具体设施影响。
对于 OOM 场景其实能够细分为如下两种:
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ad69bd29305543f389ee083ebbfeae04~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
- 可用(被调配的)内存不足:指零碎曾经调配了足够的内存,然而因为程序或者其余应用程序的需要,零碎中的可用(被调配的)内存不足以反对以后的运行。
- 内存真正有余:指零碎中内存总量不足以反对程序的运行,即零碎总内存实际上不够用。
因而,在解决内存不足的问题时,须要首先判断是可用(被调配的)内存不足还是内存真正有余,并依据相应状况采取适当的措施。
如果是可用(被调配的)内存不足,能够通过调整程序的内存配置或者敞开其余应用程序来解决问题。
如果是内存真正有余,则须要通过降级内存或者更换计算机等形式来解决问题。
3.6 Java 援用类型
JVM 场景的援用类型有四种, 别离是强援用、软援用、软援用和虚援用
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/483c5c88c19e4d1a81f0ed0675f29357~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
强援用、软援用、软援用和虚援用的本质区别能够参考如下表:
援用类型 | 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
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01a3e2cd8c6a4daf8d6f1970328b53a8~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
剖析现状
如果发现 APP 在内存方面可能存在很大的问题,第一方面的起因是线上的 OOM 率比拟高。
第二方面的起因是常常会看到在 Android Studio 的 Profiler 工具中内存的抖动比拟频繁。
确认问题
这是一个初步的现状,而后在晓得了初步的现状之后,进行了问题的确认,通过一系列的调研以及深入研究,最终发现我的项目中存在以下几点大问题,比如说:内存抖动、内存溢出、内存透露,还有 Bitmap 粗暴应用。
问题优化
如果想解决内存抖动,Memory Profiler 会出现了锯齿张图形,而后咱们剖析到具体代码存在的问题(频繁被调用的办法中呈现了日志字符串的拼接),就能解决内存透露或内存溢出。
体验晋升
为了不减少业务工作量,应用一些工具类或 ARTHook 大图检测计划,没有任何的侵入性。同时,将技术进行团队分享,团队的工作效率上会有实质晋升。
对内存优化工具如 Profiler Memory、MAT 的应用,能够针对一系列不同问题的状况,写一系列解决方案文档,整个团队成员的内存优化意识会更强。
五、内存优化领导准则
<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/736751e0b5ae428981c1a23958c05ba6~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>
万事俱备星火燎原
做 内存优化 首先应该学习 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 多平台公布