乐趣区

关于后端:内存优化-基础论-初识-Android-内存优化

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

内存优化 · 工具论 · 常见的 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>

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

  1. 硬件快:内存自身读取、存入速度快。
  2. 复用快:解码成绩无效保留,复用时,间接应用解码后对象,而不是再做一次图像解码。

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

<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 优化目标有以下三个:

  1. 避免利用产生 OOM。
  2. 升高利用因为内存过大被 LMK 机制杀死的概率。
  3. 防止不合理应用内存导致 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 字符串应用加号拼接
  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(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));
    }
}

下面的代码中,在 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(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 标记革除算法

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

标记革除算法步骤
  1. 标记所有存活的对象。
  2. 对立回收所有未被标记的对象。
标记革除算法长处

实现比较简单。

标记革除算法毛病
  1. 标记、革除效率不高。
  2. 产生大量内存碎片。

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>

  1. 将内存划分为大小相等的两块。
  2. 一块内存用完之后复制存活对象到另一块。
  3. 清理另一块内存。
复制算法长处

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

复制算法毛病

会节约一半的空间,代价大。

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>

  1. 标记过程与 标记 - 革除算法 一样。
  2. 存活对象往一端进行挪动。
  3. 清理其余内存。
标记整顿算法长处
  1. 防止标记革除导致的内存碎片。
  2. 防止复制算法的空间节约。
标记整顿算法毛病
  1. 工夫开销:标记整顿算法须要进行两次扫描,一次标记流动对象,一次整顿内存,这减少了工夫开销。
  2. 空间开销:因为标记整顿算法须要为流动对象留出足够的空间,因而必须挪动内存中的一些对象,这会减少空间开销。
  3. 内存碎片:标记整顿算法在整顿内存时可能会产生内存碎片,使得未应用的内存碎片不能被无效利用。
  4. 速度慢:绝对于其余垃圾回收算法,标记整顿算法的速度较慢,因而不适宜须要高效内存治理的场景。
  5. 效率不稳固:标记整顿算法效率受到内存应用状况的影响,如果内存应用状况不平衡,效率会不稳固。

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>

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

<p align=center><img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ad69bd29305543f389ee083ebbfeae04~tplv-k3u1fbpfcp-zoom-1.image” alt=”” /></p>

  1. 可用(被调配的)内存不足:指零碎曾经调配了足够的内存,然而因为程序或者其余应用程序的需要,零碎中的可用(被调配的)内存不足以反对以后的运行。
  2. 内存真正有余:指零碎中内存总量不足以反对程序的运行,即零碎总内存实际上不够用。

因而,在解决内存不足的问题时,须要首先判断是可用(被调配的)内存不足还是内存真正有余,并依据相应状况采取适当的措施。

如果是可用(被调配的)内存不足,能够通过调整程序的内存配置或者敞开其余应用程序来解决问题。

如果是内存真正有余,则须要通过降级内存或者更换计算机等形式来解决问题。

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,此时,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

<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 多平台公布

退出移动版