专栏名称: 鸿洋
你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。
目录
相关文章推荐
鸿洋  ·  初识Android内存优化 ·  昨天  
鸿洋  ·  Android AppOpsService是什么? ·  2 天前  
鸿洋  ·  打造Android热修复专家 ·  3 天前  
郭霖  ·  从零实现一个 KMP 插桩框架: EzHook ·  6 天前  
鸿洋  ·  AndroidManifest中配置的各种信息解读 ·  6 天前  
51好读  ›  专栏  ›  鸿洋

初识Android内存优化

鸿洋  · 公众号  · android  · 2024-12-18 08:35

正文

铁子们,明天就不更新了,欢度国庆去了,等我回来吧,祝各位国庆快乐。

1
序言

Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享内存优化 · 基础论 · 初识 Android 内存优化。

本次分享主要分为五个部分内容,第一部分内容是 5W2H 分析内存优化,第二部分内容是内存管理机制,第三部分内容是内存优化 SOP,第四部分内容是 内存优化指导原则, 最后一部分内容是总结与展望。

如果学完小木箱内存优化的基础论、工具论、方法论和实战论,那么任何人做内存优化都可以拿到结果。


2
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 给每个应用进程分配的内存都是非常有限的,那么,为什么不能把图片下载下来都放到磁盘中呢?

因为放在内存中,展示会更“快”,快的原因两点:

1. 硬件快:内存本身读取、存入速度快。

2. 复用快:解码成果有效保存,复用时,直接使用解码后对象,而不是再做一次图像解码。

那么,问题来了,什么是解码呢?

Android 系统要在屏幕上展示图片的时候只默认“像素缓冲”,而这也是大多数操作系统的特征。jpg,png 等图片格式,是把“像素缓冲”使用不同的手段压缩后的结果。

不同格式的图片,在设备上展示,必须经过一次解码,执行速度会受图片压缩比、尺寸等因素影响。

Who: 内存优化维度

对于 Android 内存优化可以细分为 RAM 和 ROM 两个维度:

1. RAM 优化

主要是降低运行时内存,RAM 优化目的有以下三个:

  1. 防止应用发生 OOM。

  2. 降低应用由于内存过大被 LMK 机制杀死的概率。

  3. 避免不合理使用内存导致 GC 次数增多,从而导致应用发生卡顿。

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. 内存抖动定义

内存波动图形呈锯齿状、GC 导致卡顿。内存抖动在 Dalvik 虚拟机上更明显,因为 ART 虚拟机内存管理、回收策略做了优化,所以内存分配、GC 效率提升了 5~10 倍,内存抖动发生概率小。

当内存频繁分配和回收导致内存不稳定,出现内存抖动,内存抖动通常表现为频繁 GC、内存曲线呈锯齿状。

并且,内存抖动的危害严重,会导致页面卡顿,甚至 OOM。

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 错误。请注意,实际应用中应避免这种不负责任的内存使用行为。

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(030);

            }

        };

    }

}

请注意,这个代码中的消息循环可能会导致内存泄漏,因此您需要在适当的时候删除消息。

4. 内存抖动常见案例

下面列举一些导致内存抖动的常见案例,如下所示:

5. 字符串使用加号拼接
  1. 实际开发中我们不应该使用字符串使用加号进行拼接,而应该使用 StringBuilder 来替代。

  2. 初始化时设置容量,减少 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

6. 资源复用

使用全局缓存池,避免频繁申请和释放的对象。

public class ObjectPool {
    private static ObjectPool instance = null;
    private HashMap 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)。

7. 减少不合理的对象创建
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(00, getWidth(), getHeight());
        canvas.drawRect(rect, paint);
    }

}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 重复创建对象,导致内存抖动
        setContentView(new CustomView(this));
    }
}

上面的代码中,在CustomViewonDraw方法和MainActivityonCreate方法中,每次都重新创建了PaintRect对象,这会导致内存波动,因为系统并不能回收之前创建的对象。

为了避免这种情况,我们可以将PaintRect对象声明为类变量,并在构造方法中初始化,以保证只创建一次:

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(00, 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;i100000;i++){

        Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);

        }

//----------------------------正确示例---------------------------

        Bitmap bitmap;

        for(int i=0;i100000;i++){

        bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.large_image);

        bitmap.recycle();

        }

在这个例子中,每次循环都会创建一个 Bitmap 对象,并将其赋值给局部变量 bitmap。但是,循环结束后, Bitmap 对象不会被立即回收,因此内存不断增加。

8. 使用合理的数据结构

使用 SparseArray 类族、ArrayMap 来替代 HashMap。


public class Main {

    public static void main(String[] args) {
        int N = 100000;
// Create a SparseArray
        SparseArray sparseArray = new SparseArray<>();
        for (int i = 0; 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 arrayMap = new ArrayMap<>();
        for (int i = 0; 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 hashMap = new HashMap<>();
        for (int i = 0; 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");
    }
}


内存泄漏

Android 系统虚拟机的垃圾回收是通过虚拟机 GC 机制来实现的。GC 会选择一些还存活的对象作为内存遍历的根节点 GC Roots,通过对 GC Roots 的可达性来判断是否需要回收。

内存泄漏是在当前应用周期内不再使用的对象被 GC Roots 引用,导致不能回收,使实际可使用内存变小。

对象被持有导致无法释放或不能按照对象正常的生命周期进行释放,内存泄漏导致可用内存减少和频繁 GC,从而导致内存溢出,App 卡顿。

public class MainActivity extends AppCompatActivity {
    private List 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方法释放图片的内存。

内存溢出

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
内存管理机制


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 标记清除算法

标记清除算法是最早的内存回收算法,其工作原理是标记出不再使用的对象并将其回收。

标记清除算法步骤

1. 标记所有存活的对象。

2. 统一回收所有未被标记的对象。

标记清除算法优点

实现比较简单。

标记清除算法缺点

1. 标记、清除效率不高。

2. 产生大量内存碎片。

3.4.2 复制算法

复制算法是一种将内存分为两个区域的算法,其中一个区域用于存储活动对象,另一个区域用于存储不再使用的对象。

复制算法步骤

1. 将内存划分为大小相等的两块。

2. 一块内存用完之后复制存活对象到另一块。

3. 清理另一块内存。

复制算法优点

实现简单,运行高效,每次仅需遍历标记一半的内存区域。

复制算法缺点

会浪费一半的空间,代价大。

3.4.3 标记整理算法

标记整理算法是标记清除算法和复制算法的结合,其工作原理是先标记出不再使用的对象,再整理内存使得活动对象的内存分配连续。

标记整理算法步骤

1. 标记过程与 标记-清除算法 一样。

2. 存活对象往一端进行移动。

3. 清理其余内存。

标记整理算法优点

1. 避免标记清除导致的内存碎片。

2. 避免复制算法的空间浪费。

标记整理算法缺点

1. 时间开销:标记整理算法需要进行两次扫描,一次标记活动对象,一次整理内存,这增加了时间开销。

2. 空间开销:由于标记整理算法需要为活动对象留出足够的空间,因此必须移动内存中的一些对象,这会增加空间开销。

3. 内存碎片:标记整理算法在整理内存时可能会产生内存碎片,使得未使用的内存碎片不能被有效利用。

4. 速度慢:相对于其他垃圾回收算法,标记整理算法的速度较慢,因此不适合需要高效内存管理的场景。

5. 效率不稳定:标记整理算法效率受到内存使用情况的影响,如果内存使用情况不均衡,效率会不稳定。

3.4.4 分代收集算法

分代回收算法是一种将内存分为几个代的算法,并对每个代进行不同的回收策略

分代收集算法步骤

1. 分配新的对象:新创建的对象分配在新生代中,因为大多数新创建的对象都很快失效,并且删除它们的成本很低。

2. 垃圾回收:新生代中的垃圾对象被回收,并且回收算法只涉及到新生代的一小部分。如果一个对象存活到一定时间,它将被移动到老年代。

3. 老年代回收:在老年代中,回收算法进行全面的垃圾回收,以确保可以回收所有垃圾对象。

4. 整理内存:回收后,内存被整理,以确保连续的内存空间可以分配给新对象。

主流的虚拟机一般用的比较多的是分代收集算法。

分代收集算法优点

1. 减少垃圾回收的时间:通过将新生代和老年代分开,分代收集算法可以减少垃圾回收的时间,因为新生代中的垃圾对象被回收的频率较高。

2. 减少内存碎片:因为新生代的垃圾回收频率较高,分代收集算法可以防止内存碎片的产生。

3. 提高内存利用率:分代收集算法可以有效地回收垃圾对象,提高内存的利用率。

4. 减少内存消耗:分代收集算法可以减少对内存的消耗,因为它仅需要涉及小的内存区域,而不是整个 Java 堆。

5. 提高系统性能:分代收集算法可以提高系统性能,因为它可以缩短垃圾回收的时间,提高内存利用率,减少内存消耗。

分代收集算法缺点

1. 复杂性:分代收集算法相对于其他垃圾回收算法来说更复杂,需要更多的内存空间来管理垃圾回收。

2. 内存分配不均衡:分代收集算法可能导致内存分配不均衡,这可能导致新生代内存不足,老年代内存过多。

3. 垃圾对象转移次数:分代收集算法需要移动垃圾对象,这可能导致更多的计算开销。

4. 时间开销:分代收集算法需要更长的时间来管理垃圾回收,这可能导致系统性能下降。

5. 停顿时间:分代收集算法可能导致长时间的停顿,这可能影响系统的实时性。

3.4.5 内存回算法使用推荐

在 Java 中,两种常用的内存回收算法分别是新生代回收算法和老年代回收算法。

新生代回收算法推荐场景:

1. 对象生命周期短:适用于那些生命周期短的对象,因为它们在很短的时间内就会被回收。

2. 大量生成对象:对于大量生成对象的场景,新生代回收算法可以有效地减少回收时间。

老年代回收算法推荐场景:

1. 对象生命周期长:适用于生命周期长的对象,因为它们不会很快被回收。

2. 内存数据稳定:对于内存数据稳定的场景,老年代回收算法可以提高内存效率。

请注意,这是基于 Java 的默认内存回收算法(即垃圾回收器)的推荐使用场景。您可以通过配置 JVM 参数来更改这些默认设置,以适应您的特定需求。

3.5 Java 内存管理

Android 中的内存是弹性分配的,分配值与最大值受具体设备影响。

对于 OOM 场景其实可以细分为如下两种:

1. 可用(被分配的)内存不足:指系统已经分配了足够的内存,但是由于程序或者其他应用程序的需求,系统中的可用(被分配的)内存不足以支持当前的运行。

2. 内存真正不足:指系统中内存总量不足以支持程序的运行,即系统总内存实际上不够用。

因此,在解决内存不足的问题时,需要首先判断是可用(被分配的)内存不足还是内存真正不足,并根据相应情况采取适当的措施。

如果是可用(被分配的)内存不足,可以通过调整程序的内存配置或者关闭其他应用程序来解决问题。

如果是内存真正不足,则需要通过升级内存或者更换计算机等方式来解决问题。

3.6 Java 引用类型

JVM 场景的引用类型有四种,分别是强引用、软引用、软引用和虚引用。

强引用、软引用、软引用和虚引用的本质区别可以参考如下表:

引用类型GC 回收时间用途生存时间
强引用永不对象的一般状态JVM 停止运行时
软引用内存不足时对象缓存内存不足时终止
弱引用GC对象缓存GC 后终止
虚引用未知未知未知


强引用


1. 强引用概念

强引用是 Java 中最常见的引用类型,当对象具有强引用时,它永远不会被垃圾回收。只有在程序结束或者手动将对象设置为 null 时,才会释放强引用。

2. 强引用案例


public class StrongReferenceExample {

    public static void main(String[] args) {

        ArrayList data = new ArrayList<>();

        data.add("Hello");

        data.add("World");

// 创建强引用

        ArrayList 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 后仍然不会被回收。

弱引用


1. 弱引用概念

一种用于追踪对象的引用,不会对对象的生命周期造成影响。在内存管理方面,弱引用不被认为是对象的“有效引用”。

因此,如果一个对象只被弱引用指向,那么在垃圾回收的时候,这个对象可能会被回收掉。

弱引用常被用来在内存敏感的应用中实现对象缓存。在这种情况下,弱引用可以让缓存的对象在内存不足时被回收,从而避免内存泄漏。

2. 弱引用案例


public class WeakReferenceExample {

    public static void main(String[] args) {

        String data = new String("Hello");

// 创建弱引用

        WeakReference 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

软引用


1. 软引用概念

软引用是比强引用更容易被回收的引用类型。当 Java 堆内存不足时,软引用可能会被回收,以腾出内存空间。如果内存充足,则软引用可以继续存在。

2. 软引用案例


public class SoftReferenceExample {

    public static void main(String[] args) {

        Object referent = new Object();

        SoftReference softReference = new SoftReference<>(referent);

        referent = null;

        System.gc();

// 软引用可以在内存不足时被回收

        System.out.println(softReference.get());

    }

}

输出结果:

java.lang.Object@2f92e0f4

这段代码创建了一个 Object 的实例,并使用它作为 SoftReference 的引用对象。

然后,它将该实例设置为 null,并试图强制进行垃圾回收。如果内存不足,软引用会被回收,并且可以从 softReference 获取的对象将为 null

虚引用


1. 虚引用概念

虚引用是 Java 中最弱的引用类型,对于虚引用,对象只存在于垃圾回收的最后阶段,在这个阶段,对象将被回收,而无论内存是否充足。虚引用主要用于监测对象被回收的状态,而不是用于缓存对象。

2. 虚引用案例


public class PhantomReferenceExample {

    public static void main(String[] args) {

        Object referent = new Object();

        ReferenceQueue referenceQueue = new ReferenceQueue<>();

        PhantomReference phantomReference = new PhantomReference<>(referent, referenceQueue);

        referent = null;

        System.gc();

// 虚引用在回收前不会被加入引用队列,但在回收时会被加入引用队列

        System.out.println(referenceQueue.poll() == phantomReference);

    }

}

输出结果:

false

这段代码创建了一个 Object 的实例,并使用它作为 PhantomReference 的引用对象。

然后,它将该实例设置为 null,并试图强制进行垃圾回收。如果垃圾回收发生,虚引用会被加入引用队列,从而可以从引用队列中获取。

 

4
内存优化 SOP



分析现状

如果发现 APP 在内存方面可能存在很大的问题,第一方面的原因是线上的 OOM 率比较高。

第二方面的原因是经常会看到在 Android Studio 的 Profiler 工具中内存的抖动比较频繁。

确认问题

这是一个初步的现状,然后在知道了初步的现状之后,进行了问题的确认,经过一系列的调研以及深入研究,最终发现项目中存在以下几点大问题,比如说:内存抖动、内存溢出、内存泄漏,还有 Bitmap 粗犷使用。

问题优化

如果想解决内存抖动,Memory Profiler 会呈现了锯齿张图形,然后我们分析到具体代码存在的问题(频繁被调用的方法中出现了日志字符串的拼接),就能解决内存泄漏或内存溢出。

体验提升

为了不增加业务工作量,使用一些工具类或 ARTHook 大图检测方案,没有任何的侵入性。同时,将技术进行团队分享,团队的工作效率上会有本质提升。

对内存优化工具如 Profiler Memory、MAT 的使用,可以针对一系列不同问题的情况,写一系列解决方案文档,整个团队成员的内存优化意识会更强。

5
内存优化指导原则



万事俱备水滴石穿

内存优化首先应该学习 Google 内存方面的文档,如 Memory Profiler、MAT 等工具的使用,当在工程遇到内存问题,才能对问题进行排查定位。而不是一开始并没有分析项目代码导致内存高占用问题,就依据自己看的几篇企业博客,不管业务背景,瞎猫碰耗子做内存优化。

结合业务优化内存

如果不结合业务背景,直接对 APP 运行阶段进行内存上报然后内存消耗进行内存监控,那么内存监控一旦不到位,比如存在使用多个图片库,因为图片库内存缓存不公用的,应用内存占用效率不会有质的飞跃。因此技术优化必须结合业务。

解决方案系统科学

在做内存优化的过程中,Android 业务端除了要做优化工作,Android 业务端还得负责数据采集上报,数据上报到 APM 后台后,无论是 Bug 追踪人员或者 Crash 追踪人员,对问题"回码定位"都提供好的依据。

内存劣化 Hook 魔改

大图片检测方案,大家可能想到去是继承 ImageView,然后重写 ImageView 的 onDraw 方法实现。但是,在推广的过程中,因为耦合度过高,业务同学很难认可,ImageView 之前写一次,为什么要重复造轮子呢? 替换成本非常高。所以我们可以考虑使用类似 ARTHook 这样的 Hook 方案。

6
总结与展望


内存优化、启动优化、卡顿优化、包体积优化是 Android 性能优化四驾马车,而内存优化又是四驾马车最难驾驭的一驾,如果你掌握了这项基础技能,那么你将超过绝对多数的 Android 开发。



最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!