内存优化 · 基础论 · 初识 Android 内存优化

news2024/9/23 3:17:58

【小木箱成长营】内存优化系列文章:

内存优化 · 工具论 · 常见的 Android 内存优化工具和框架

内存优化 · 方法论 · 揭开内存优化神秘面纱

内存优化 · 实战论 · 内存优化实践与应用

Tips: 关注微信公众号小木箱成长营,回复"内存优化"可免费获得内存优化思维导图

一、序言

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

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

alt

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

二、5W2H 分析内存优化

首先我们说说我们的第一部分内容,5W2H 分析内存优化,5W2H 分析内存优化提出了 7 个高价值问题

  • What: 内存优化定义

  • Why: 内存优化原因

  • How: 内存优化归因

  • Who: 内存优化维度

  • When: 内存优化时机

  • How Much: 内存优化价值

  • Where: 内存痛点定位

What: 内存优化定义

alt

Android 内存优化是指优化 Android 应用程序的内存使用,以减少可用内存的消耗,提高应用程序的性能和可靠性。Android 内存优化可以通过减少内存使用量,减少对资源的消耗,以及提高内存利用率来实现。

Why: 内存优化原因

alt

安卓系统对每个应用程序都有一定的内存限制,当应用程序的内存超过了上限,就会出现 OOM (Out of Memory),也就是 App的异常退出。

因此,要改善系统的运行效率、改善用户体验、降低系统资源占用、延长电池寿命、降低系统故障的危险。

Android通过内存优化,可以减少系统内存使用,让系统更加流畅,运行更快,减少系统Crash,提升用户体验。

alt

How: 内存优化归因

关于应用内存分析,需要重点关注四个阶段

  • 应用停留在闪屏页面内存固定值

  • 应用的MainActivity到HomeActivty内存波动值

  • 应用运行十分钟后回归到HomeActivty内存波动值

  • 应用内存使用量分配值汇总

alt

Android 给每个应用进程分配的内存都是非常有限的,那么,为什么不能把图片下载下来都放到磁盘中呢?

alt

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

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

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

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

alt

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

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

Who: 内存优化维度

alt

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

1.2.1 RAM 优化

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

  1. 防止应用发生 OOM。

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

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

1.2.2 ROM 优化

减少程序占用的 ROM,并进行 APK精简。其目标是减少应用程序的占用,防止由于 ROM空间限制而导致程序的安装失败。

When: 内存优化时机

alt

手机不使用 PC 的 DDR 内存,采用的是 LP DDR RAM,也就是“低功率的两倍数据率存储器”。其计算规则如下所示:

LP DDR 系列的带宽=时钟频率 ✖️ 内存总线位数/8

LP DDR4=1600MHZ✖️64/8✖️ 双倍速率=26GB/s。

那么内存占用是否越少越好?

如果当系统内存充足的时候,那么小木箱建议你多用一些内存获得更好的性能。

如果系统内存不足的时候,那么小木箱建议你可以做到“用时分配,及时释放”。

How Much: 内存优化价值

alt

做好内存优化将带来以下三点好处:

第一点好处是减少 OOM,提高应用稳定性。

第二点好处是减少卡顿,提高应用流畅度。

第三点好处是减少内存占用,提高应用后台运行时的存活率。

Where: 内存痛点定位

那么,内存痛点定位主要是有哪几类呢?内存痛点问题通常来说,可以细分为如下三类:

alt

第一,内存抖动。

第二,内存泄漏。

第三,内存溢出。

下面,小木箱带大家来了解下内存抖动、内存泄漏和内存溢出。

alt

1.3.1 内存抖动

1.3.1.4.1 内存抖动定义

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

alt

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

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

1.3.1.4.2 OOM 原因

那么,为什么内存抖动会导致 OOM?

alt

主要原因有如下两点:

第一,频繁创建对象,导致内存不足及不连续碎片;

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 内存抖动解决

alt

这里假设有这样一个场景:点击按钮使用 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);

            }

        };

    }

}

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

1.3.1.4.4 内存抖动常见案例

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

alt
1.3.1.4.1 字符串使用加号拼接
  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

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(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;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 虚拟机有什么区别呢?

alt

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 的内存分配区域分为如下五部分:

alt

alt

3.4 Java 内存回收算法

alt

3.4.1 标记清除算法

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

标记清除算法步骤

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

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

标记清除算法优点

实现比较简单。

标记清除算法缺点

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

  2. 产生大量内存碎片。

3.4.2 复制算法

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

复制算法步骤

alt

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

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

  3. 清理另一块内存。

复制算法优点

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

复制算法缺点

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

3.4.3 标记整理算法

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

标记整理算法步骤

alt

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

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

  3. 清理其余内存。

标记整理算法优点

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

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

标记整理算法缺点

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

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

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

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

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

3.4.4 分代收集算法

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

分代收集算法步骤

alt

  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 场景其实可以细分为如下两种:

alt

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

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

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

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

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

3.6 Java 引用类型

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

alt

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

引用类型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,此时,strongReferencedata 将指向同一个对象。

在之后的代码中,我们断开了 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

alt

分析现状

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

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

确认问题

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

问题优化

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

体验提升

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

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

五、内存优化指导原则

alt

万事俱备水滴石穿

内存优化首先应该学习 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 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/341355.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux驱动开发(二)

一、驱动流程 驱动需要以下几个步骤才能完成对硬件的访问和操作&#xff1a; 模块加载函数 module_init注册主次设备号 <应用程序通过设备号找到设备>驱动设备文件 <应用程序访问驱动的方式> 1、手动创建 &#xff08;mknod&#xff09;2、程序自动创建file_oper…

Synchronized 原理

基本特点(只考虑 JDK 1.8): 1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.3. 实现轻量级锁的时候大概率用到的自旋锁策略4. 是一种不公平锁5. 是一种可重入锁6. 不是读写锁 加锁工作过程 JVM 将 s…

【Kafka】【三】安装Kafka服务器

Kafka基本知识 Kafka介绍 Kafka是最初由Linkedin公司开发&#xff0c;是⼀个分布式、⽀持分区的&#xff08;partition&#xff09;、多副本的 &#xff08;replica&#xff09;&#xff0c;基于zookeeper协调的分布式消息系统&#xff0c;它的最⼤的特性就是可以实时的处理 …

蓝牙安全(AES-CCM)

目录 AES-CCM CCM规范加密过程 CCM规范解密认证过程 formatting函数 counter generation函数 蓝牙AES-CCM加密流程 参考文献 AES-CCM Advanced Encryption Standard-Counter with Cipher Block Chaining-Message Authentication Code 自蓝牙4.1起蓝牙的加密算法开始采…

RabbitMQ-其他问题

一、幂等性问题&#xff1a;消费者在消费MQ中的消息时&#xff0c;MQ已把消息发送给消费者&#xff0c;消费者在给MQ返回ACK时网络中断&#xff0c;故MQ未收到确认消息&#xff0c;该消息会重新发送给其他消费者&#xff0c;或者在网络重连后再次发送给消费者&#xff0c;但实际…

第三章虚拟机的克隆,快照,迁移删除

1.虚拟机的克隆 如果你已经安装了一台linux操作系统&#xff0c;你还想再更多的&#xff0c;没有必要再重新安装&#xff0c;你只需要克 隆就可以&#xff0c;看演示。 方式1&#xff0c;直接拷贝一份安装好的虚拟机文件,再用虚拟机打开这个文件方式2&#xff0c;使用vmware的…

企业三要素核验API接口,你了解多少?

企业三要素核验API接口是指哪些要素&#xff1f;企业三要素是一种有关企业实名认证的应用程序接口也称API&#xff0c;企业的名称、统一社会信用代码和法人代表姓名统称企业三要素。企业三要素核验API接口的资源来自国家工商总局数据库&#xff0c;通过数据库资料三个要素进行核…

字母板上的路径 题解,力扣官方出来挨打(小声)

字母板上的路径 我们从一块字母板上的位置 (0, 0) 出发&#xff0c;该坐标对应的字符为 board[0][0]。 在本题里&#xff0c;字母板为board [“abcde”, “fghij”, “klmno”, “pqrst”, “uvwxy”, “z”]&#xff0c;如下所示。 我们可以按下面的指令规则行动&#xff1a…

Dubbo基本原理和用法讲解

Dubbo基本原理和用法讲解 序言&#xff1a;学习一项新技术&#xff0c;一般从是什么、为什么、怎么用三个方面进行学习。本篇文章也不例外&#xff0c;笔者将从Dubbo是什么&#xff1f;、为什么会产生Dubbo技术&#xff1f;、如何在项目中使用Dubbo技术。最后&#xff0c;笔者…

基于springboot+vue的宠物商城系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

Fluent Python 笔记 第 6 章 使用一等函数实现设计模式

虽然设计模式与语言无关&#xff0c;但这并不意味着每一个模式都能在每一门语言中使用。1996 年&#xff0c;Peter Norvig 在题为“Design Patterns in Dynamic Languages”(http://norvig.com/design- patterns/)的演讲中指出&#xff0c;Gamma 等人合著的《设计模式:可复用面…

序列化和反序列化~如何实现自定义协议?jsoncpp的使用

目录 序列化反序列化的概念 为什么要进行序列化和反序列化&#xff1f; 自定义协议实现业务 jsoncpp实现序列化和反序列化 序列化&#xff1a; 反序列化&#xff1a; 自定义协议jsoncpp实现简易计算器服务整体逻辑 Server.cc Client.cc 运行结果 序列化反序列化的概念…

重建control遗漏数据文件,reseltogs报ORA-1555错误处理----惜分飞

又一客户,误删除oracle redo导致数据库无法正常启动,自己尝试重建ctl,结果遗漏部分oracle数据文件并且尝试过resetlogs,导致部分文件resetlogs scn不一致.导致重建ctl失败Fri Feb 10 12:41:20 2023CREATE CONTROLFILE REUSE DATABASE "orcl"RESETLOGS NOARCHIVELOG M…

GO 中的 init 函数

前言 go 语言中有一个非常神奇的函数 init ,它可以在所有程序执行开始前被执行&#xff0c;并且每个 package 下面可以存在多个 init 函数&#xff0c;我们一起来看看这个奇怪的 init 函数。 init 特性 init 函数在 main 函数之前执行&#xff0c;并且是自动执行&#xff1b…

Docker网络实现原理

目录 1 Docker网络实现原理 1.2 为容器创建端口映射 方法一&#xff1a;随机映射端口&#xff08;从32768开始&#xff09; 方法二&#xff1a;指定映射端口 1.3 查看容器的输出和日志信息 二 Docker的网络模式 2.1 Docker的网络模式&#xff08;41&#xff09; 2.2 查看…

【大数据趋势】2月12日 货币发动机牵着港股和A股走,历史不会简单重演,但是会用类似的方法让人再踏入同一条河流

行情核心源头之一 : 离岸人民币和美元趋势历史对比&#xff0c;预示着一个阶段底部正在形成中 历史总是很容易忘记&#xff0c;应该很少有人记得18年发生了什么。还是让大数据程序来对比一下。【红色标记1】RSI预示着價格強度的动能情况&#xff0c;同样是达到了一个高点&…

2021 WAIC 世界人工智能大会参会总结

前言 2021 年世界人工智能大会&#xff08;WAIC&#xff09;于2021年7月7日至10日在上海世博展览馆举办&#xff0c;本届大会继续秉持「智联世界」的理念&#xff0c;以「众智成城」为主题&#xff0c;促进全球人工智能创新思想、技术、应用、人才和资本的集聚和交流&#xff…

JS逆向案例分享----prototype的妙用

方向不对&#xff0c;努力白费。爬虫也是如此。今天分享的案例很能说明问题。目标&#xff1a;某集团公司采购信息aHR0cHM6Ly9lYy5taW5tZXRhbHMuY29tLmNuL29wZW4vaG9tZS9wdXJjaGFzZS1pbmZv浏览器抓包&#xff0c;请求载荷里就一个加密参数param全局搜索“param:”&#xff0c;有…

【计组】DMA、数据完整性--《深入浅出计算机组成原理》(十三)

目录 一、DMA &#xff08;一&#xff09;理解DMA&#xff0c;一个协处理器 &#xff08;二&#xff09; Kafka 的实现原理 二、数据的完整性 &#xff08;一&#xff09;单比特翻转&#xff1a;软件解决不了的硬件错误 &#xff08;二&#xff09;海明码 1、海明码的纠错…

《狂飙》壁纸大嫂如此惊艳,做成日历壁纸天天看(7)

小朋友们好&#xff0c;大朋友们好&#xff01;我是猫妹&#xff01;话说兔年春节期间&#xff0c;一部反黑反腐电视剧横空出世&#xff0c;收视率和口碑都有不错的成绩&#xff01;这部电视剧叫《狂飙》&#xff01;你看了吗&#xff1f;我没看&#xff01;不过这丝毫不影响它…