Android 内存优化(基础轮)必看~
本次分享主要分为五个部分内容,第一部分内容是 5W2H 分析内存优化,第二部分内容是内存管理机制,第三部分内容是内存优化 SOP,第四部分内容是 内存优化指导原则, 最后一部分内容是总结与展望。
如果学完内存优化的基础论、工具论、方法论和实战论,那么任何人做内存优化都可以拿到结果。
二、5W2H 分析内存优化
首先我们说说我们的第一部分内容,5W2H 分析内存优化,5W2H 分析内存优化提出了 7 个高价值问题
- 内存优化定义
- 内存优化原因
- 内存优化归因
- 内存优化维度
- 内存优化时机
- 内存优化价值
- 内存痛点定位
内存优化定义
Android 内存优化是指优化 Android 应用程序的内存使用,以减少可用内存的消耗,提高应用程序的性能和可靠性。Android 内存优化可以通过减少内存使用量,减少对资源的消耗,以及提高内存利用率来实现。
内存优化原因
安卓系统对每个应用程序都有一定的内存限制,当应用程序的内存超过了上限,就会出现 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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mButton = (Button) findViewById(R.id.button);mButton.setOnClickListener(new View.OnClickListener() {@Overridepublic 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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mButton = (Button) findViewById(R.id.button);mButton.setOnClickListener(new View.OnClickListener() {@Overridepublic 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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mButton = (Button) findViewById(R.id.button);mButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {mHandler.sendEmptyMessage(0);}});mHandler = new Handler() {@Overridepublic 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");// 使用StringBuilderStringBuilder 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);}@Overrideprotected 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 {@Overrideprotected 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();}@Overrideprotected 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 {@Overrideprotected 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 SparseArraySparseArray<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 ArrayMapArrayMap<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 HashMapHashMap<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<>();@Overrideprotected 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;@Overrideprotected 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());}}
输出结果:
情况1: java.lang.Object@2f92e0f4
情况2: null
这段代码创建了一个 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 知识点归整
Android 性能调优系列:https://0a.fit/dNHYY
Android 车载学习指南:https://0a.fit/jdVoy
Android Framework核心知识点笔记:https://0a.fit/acnLL
Android 音视频学习笔记:https://0a.fit/BzPVh
Jetpack全家桶(含Compose):https://0a.fit/GQJSl
Kotlin 入门到精进:https://0a.fit/kdfWR
Flutter 基础到进阶实战:https://0a.fit/xvcHV
Android 八大知识体系:https://0a.fit/mieWJ
Android 中高级面试题锦:https://0a.fit/YXwVq
后续如有新知识点,将会持续更新,尽请期待……
相关文章:
Android 内存优化(基础轮)必看~
本次分享主要分为五个部分内容,第一部分内容是 5W2H 分析内存优化,第二部分内容是内存管理机制,第三部分内容是内存优化 SOP,第四部分内容是 内存优化指导原则, 最后一部分内容是总结与展望。 如果学完内存优化的基础论…...
STM32单片机GSM短信自动存取快递柜
实践制作DIY- GC0104-自动存取快递柜 一、功能说明: 基于STM32单片机设计-自动存取快递柜 二、功能介绍: STM32F103C系列最小系统板0.96寸OLED显示器DY-SV17F串口语音播报模块4*4矩阵键盘GSM短信模块4路舵机(模拟4个柜子) ***…...
力扣(LeetCode)410. 分割数组的最大值(2023.02.12)
给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。 设计一个算法使得这 m 个子数组各自和的最大值最小。 示例 1: 输入:nums [7,2,5,10,8], m 2 输出:18 解释: 一共有四种方法…...
管理还原数据
还原数据还原数据是:• 原始的、修改之前的数据副本• 针对更改数据的每个事务处理而捕获• 至少保留到事务处理结束• 用于支持:– 回退操作– 读取一致性查询– Oracle 闪回查询、Oracle 闪回事务处理和 Oracle 闪回表– 从失败的事务处理中进行恢复存…...
c的关键字有那些
编程语言中的关键字 C语言简洁、紧凑,使用方便、灵活。ANSI C标准C语言共有32个关键字,9种控制语句,程序书写形式自由,区分大小写。把高级语言的基本结构和语句与低级语言的实用性结合起来。 C 语言可以像汇编语言一样对位、字节和…...
链表OJ(一)
目录 从尾到头打印链表_牛客题霸_牛客网 160. 相交链表 141. 环形链表 142. 环形链表 II 138. 复制带随机指针的链表 从尾到头打印链表_牛客题霸_牛客网 输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。 如输入…...
MySQL第三次作业
1、显示所有职工的基本信息。 2、查询所有职工所属部门的部门号,不显示重复的部门号。 3、求出所有职工的人数。 4、列出最高工和最低工资。 5、列出职工的平均工资和总工资。 6、创建一个只有职工号、姓名和参加工作的新表,名为工作日期表…...
Python中的类和对象(7)
1.私有变量 在大多数面向对象的编程语言中,都存在着私有变量(private variable)的概念,所谓私有变量,就是指通过某种手段,使得对象中的属性或方法无法被外部所访问。 Python 对于私有变量的实现是引入了一…...
【JVM】7种经典的垃圾收集器
文章目录1. 垃圾收集器概述2. Serial 收集器3. ParNew 收集器4. Paraller Scavenge 收集器5. Serial Old收集器6. Parller Old收集器7. CMS 收集器8. Garbage First 收集器本文参考:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版ÿ…...
2023/2/12总结
滑动窗口(1)滑动窗口是一种基于双指针的思想,两个指针指向的元素形成一个窗口。一般用于求取数组或字符串的某个子串、子序列、最长最短等最值或者求某个目标值时,并且该问题本身可以通过暴力解决。滑动窗口分为固定窗口和不定窗口…...
Linux之正则表达式
正则表达式是组成“操作”的基本语法,而这些“操作”是应用于Sed和Awk必备的能力。因此只有了解了正则表达式,才能学好Sed和Awk。正则表达式分为基础正则表达式(Regular Expression)与扩展正则表达式(Extended Regular…...
前端高频面试题-HTML和CSS篇(一)
💻 前端高频面试题-HTML和CSS篇(一) 🏠专栏:前端面试题 👀个人主页:繁星学编程🍁 🧑个人简介:一个不断提高自我的平凡人🚀 🔊分享方向…...
Redis 专题总结
1. 什么是Redis ? 处理:内容缓存,主要用于处理大量数据的高访问负载。Redis是一款高性能的NOSQL系列的非关系型数据库,NoSQL(NoSQL Not Only SQL),意即“不仅仅是SQL”,是一项全新的数据库理念࿰…...
【Python百日进阶-Web开发-Vue3】Day515 - Vue+ts后台项目2:登录页面
文章目录 一、创建登录路由1.1 安装 Vue VSCode Snippets插件1.2 处理路径引用的红色波浪线1.3 入口文件 main.ts1.4 主组件 App.vue1.5 路由文件 router/index.ts1.6 首页组件 views/HomeView.vue1.7 登录组件 views/LoginView.vue二、实现登录页面的表单展示2.1 element-plus…...
【博客620】prometheus如何优化远程读写的性能
prometheus如何优化远程读写的性能 场景 为了解决prometheus本地存储带来的单点问题,我们一般在高可用监控架构中会使用远程存储,并通过配置prometheus的remote_write和remote_read来对接 远程写优化:remote_write 远程写的原理:…...
redis可视工具AnotherRedisDesktopManager的使用
redis可视工具AnotherRedisDesktopManager的使用 简介 Another Redis DeskTop Manager 是一个开源项目,提供了以可视化的方式管理 Redis 的功能,可供免费下载安装,也可以在此基础上进行二次开发,主要特点有: 支持 W…...
【idea】idea生产类注释和方法注释
网上有很多类似的文章,但是我在按照他们的文章设置后,出现了一些问题,因此我这边在解决了问题后,总结一篇文章,发出来给大家借鉴一下。在此先说明一下idea的版本,是2020.1.3 设置动态模板,File…...
jenkins +docker+python接口自动化之jenkins容器安装python3(二)
jenkins dockerpython接口自动化之jenkins容器安装python3(二) 目录:导读 前提是在docker下已经配置好jenkins容器了,是将python安装在jenkins容器下的 1、先看你的jenkins是否安装好 2、以root权限进入jenkins容器࿱…...
go 命令行工具整理
这里会整理可能会使用到的命令行参数,比如 go build、go run,诸如此类。了解这些内容对我们工作会有什么帮助吗?更多的时候,是能让我们理解代码编译的意图,或者,给我们一种排查问题的手段。 比方说&#x…...
RuntimeError: CUDA out of memory
今天在训练模型的时候突然报了显存不够的问题,然后分析了一下,找到了解决的办法,这里记录一下,方便以后查阅。 注:以下的解决方案是在模型测试而不是模型训练时出现这个报错的! RuntimeError: CUDA out of…...
Kubernetes1.25中Redis集群部署实例
1、概述我们知道在 Kubernetes 容器编排平台中, 我们可以非常方便的进行应用的扩容缩, 同时也能非常方便的进行业务的迭代,本章主要讲解在Kubernetes1.25搭建Redis单实例和Redis集群主从同步的环境流程步骤, 如果是高频访问重要的线上业务我们最好是部署在物理机器上…...
C++11实现计算机网络中的TCP/IP连接(Windows端)
目录引言1、TCP2、IP2.1 IP路由器3、TCP/IP4、TCP/IP协议C11实现参考文献引言 TCP/IP 指传输控制协议/网际协议(Transmission Control Protocol / Internet Protocol)。[1] 在TCP/IP协议簇中主要包含以下内容: TCP (传输控制协议) - 应用程序…...
Spring框架自定义实现IOC基础功能/IDEA如何手动实现IOC功能
继续整理记录这段时间来的收获,详细代码可在我的Gitee仓库Java设计模式克隆下载学习使用! 7.4 自定义Spring IOC 创建新模块,结构如图![[Pasted image 20230210173222.png]] 7.4.1 定义bean相关POJO类 7.4.1.1 定义propertyValue类 /** …...
pip离线安装windows版torch
文章目录前言conda创建虚拟环境安装torchtorch官网在线安装离线手动安装测试是否安装成功后记前言 学习的时候遇到几个机器学习相关的项目,由于不同的项目之间用到的依赖库不太一样,于是想利用conda为不同的项目创建不同的环境方便管理和运行࿰…...
Redis核心知识点
Redis核心知识点Redis核心知识点大全五种数据类型redis整合SpringBoot序列化问题渐进式扫描慢查询缓存相关问题数据库和缓存谁先更新缓存穿透缓存雪崩缓存击穿实际应用超卖问题分布式锁全局唯一ID充当消息队列Feed流附近商户签到HyperLogLog实现UV统计持久化RDBAOF持久化小结事…...
14. 最长公共前缀
14. 最长公共前缀 一、题目描述: 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀,返回空字符串 “”。 示例 1: 输入:strs [“flower”,“flow”,“flight”] 输出:“fl” 示例 2: …...
SignalR注册成Windows后台服务,并实现web前端断线重连
注意下文里面的 SignalR 不是 Core 版本,而是 Framework 下的 本文使用的方式是把 SignalR 写在控制台项目里,再用 Topshelf 注册成 Windows 服务 这样做有两点好处 传统 Window 服务项目调试时需要“附加到进程”,开发体验比较差…...
【前端笔试题二】从一个指定数组中,每次随机取一个数,且不能与上次取数相同,即避免相邻取数重复
前言 本篇文章记录下我在笔试过程中遇到的真实题目,供大家参考。 1、题目 系统给定一个数组,需要我们编写一个函数,该函数每次调用,随机从该数组中获取一个数,且不能与上一次的取数相同。 2、思路解析 数组已经有了…...
专栏关注学习
Node学习专栏(全网最细的教程) 【spring系列】 SpringCloud 前端框架Vue java学习过程 RocketMQ Spring Tomcat websocket 从头开始学Redisson 从头开始学Oracle 跟着大宇学Shiro 吃透Shiro源代码 Git基础与进阶 Java并发编程 Spring系列 手写…...
【手写 Vuex 源码】第八篇 - Vuex 的 State 状态安装
一,前言 上一篇,主要介绍了 Vuex 模块安装的实现,针对 action、mutation、getter 的收集与处理,主要涉及以下几个点: Vuex 模块安装的逻辑;Vuex 代码优化;Vuex 模块安装的实现;Vue…...
关于做网站的问卷调查/美容美发培训职业学校
什么是单元测试? 程序开发过程中,写代码是为了实现需求。当我们的代码通过了编译,只是说明它的语法正确,功能能否实现则不能保证。 因此,当我们的某些功能代码完成后,为了检验其是否满足程序的需求。可以通…...
网站做301有什么用/seo推广人员
Linux 基础 操作系统 操作系统 Operating System 简称 OS ,是软件的一部分,它是硬件基础上的第一层软件,是硬件和其它软件沟通的桥梁。 操作系统会控制其他程序运行,管理系统资源,提供最基本的计算功能,…...
设计师网站推荐/我要推广网
使用环境(蓝色粗体字为特别注意内容) 1、软件环境:Keil MDK 5.15 2、硬件环境:STM32F103C8T6最小系统 在项目中打算用基本定时器6作为延时定时器,编译发现报错:main.c(77): error: #20: identifier "RCC_APB1ENR_TIM6EN&qu…...
wordpress使用置顶文章没用/网络平台的推广方法
PPT文件上呦限制编辑,不知道密码应该怎么去掉? 取消限制编辑在不知道密码的情况下,需要用到工具的帮助【PPT解密大师】快速找回密码_轻松移除使用限制-奥凯丰okfone 选择【解除限制】将PPT文件添加到软件中,点击一下【开始】就可…...
网站的外链是怎么做的/桂平seo关键词优化
从计算机到手机,计算平台的发展不断将人类文明推向新的高峰,但随着移动互联网领域的开发潜力逐渐殆尽,越来越多的科技爱好者和开发者将目光投向了下一代计算平台——机器人。作为蕴含重大机会的领域,机器人产业发展却没有理想中迅…...
网站备案失败/企业邮箱哪个好
首先参考该文:http://www.cnblogs.com/happyhippy/archive/2007/04/08/710933.html下载具体的symbols文件,我用的是xpsp2的188M的那个文件,然后按照该文的设置进行,如果你进入windbg界面再命令行下输入.sympath没有什么错误提示&a…...