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

内存优化 · 东西论 · 常见的 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,进步用户体会。

How: 内存优化归因

关于运用内存剖析,需求重点重视四个阶段

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

  • 运用的MainActivity到HomeActivty内存动摇值

  • 运用运转十分钟后回归到HomeActivty内存动摇值

  • 运用内存运用量分配值汇总

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

由于放在内存中,展现会更“快”,快的原因两点:

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

  2. 复用快:解码效果有用保存,复用时,直接运用解码后方针,而不是再做一次图画解码。

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

Android 体系要在屏幕上展现图片的时分只默许“像素缓冲”,而这也是大多数操作体系的特征。jpg,png 等图片格局,是把“像素缓冲”运用不同的手段压缩后的成果。

不同格局的图片,在设备上展现,有必要经过一次解码,执行速度会受图片压缩比、尺度等要素影响。

Who: 内存优化维度

关于 Android 内存优化能够细分为 RAM 和 ROM 两个维度:

1.2.1 RAM 优化

首要是下降运转时内存,RAM 优化目的有以下三个:

  1. 防止运用产生 OOM。

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

  3. 防止不合理运用内存导致 GC 次数增多,然后导致运用产生卡顿。

1.2.2 ROM 优化

削减程序占用的 ROM,并进行 APK精简。其方针是削减运用程序的占用,防止由于 ROM空间约束而导致程序的装置失利。

When: 内存优化机遇

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

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

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

image.png

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

假如当体系内存充足的时分,那么小木箱主张你多用一些内存取得更好的功能。

假如体系内存缺乏的时分,那么小木箱主张你能够做到“用时分配,及时开释”。

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

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 后停止
虚引证 不知道 不知道 不知道

强引证

强引证概念

强引证是 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());
    }
}

输出成果:

状况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 功能优化系列: Java 内存优化篇

  • 抖音 Android 功能优化系列:Java OOM 优化之 NativeBitmap 计划

  • 拯救 OOM!字节自研 Android 虚拟机内存办理优化黑科技 mSponge

  • 腾讯游戏学院专家:UE 手游研发中,如何做好 Android 内存优化?

  • 深化探究 Android 内存优化(炼狱等级-上)

  • 深化探究 Android 内存优化(炼狱等级-下)

  • 微信 Android 终端内存优化实践

  • Android 内存走漏自动化链路剖析组件

  • 内存优化-4GB 内存时代,再谈内存优化