欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

Android性能优化之内存优化浅析

时间:2023-08-01

一、背景

        Android由于是以Java语言为主要开发语言,所以它的内存管理并不像C语言那样由开发者去管理内存的分配以及回收等,而是交由JVM虚拟机的内存回收机制去处理。这就导致我们在开发过程中难免会遇到内存相关的各种问题带来的困扰。而这些问题的根源就在于我们对Android的内存管理机制的不够了解,以及开发过程中缺少对内存的关注导致。所以这里借这篇文章我们再回顾一下Android的内存管理机制,以及如何在开发过程中去尽量避免这些内存问题。

二、内存管理—内存划分

Android中的内存是由谁来管理?答案是JVM虚拟机——内存大管家。

通常我们Android都得基于Java语言做的开发。(这里要排除新的Flutter开发,它是基于Dart语言开发。)而Java语言从API的角度来讲,就是由我们的JDK组成的。同时JRE又是JDK的一部分,而我们这里要说的JVM呢又是JRE的组成部分,简称Java虚拟机。它的主要作用就是实现Java语言的跨平台。它能够将我们的Java语言编译成二进制机器码,然后运行在各平台上。除了帮助跨平台之外,它还承担着内存管理的任务。Android开发中常说的内存基本都是指运行时的内存。

从线程的角度来看,JVM在执行Java程序的过程中会将所管理的内存划分为线程私有和线程共有两大类。

2.1、线程私有

(1)程序计数器:Java是多线程的,既然是多线程就需要线程间切换、通信等操作。如何保证线程间操作时每个线程的执行顺序能按代码的步骤正常执行呢?这时候程序计数器就是关键了,它会帮我们记录当前线程执行到的位置(在字节码中记录的这些位置统称为指令地址)。这个内存区不会出现OOM

2)虚拟机栈:我们常提在嘴上的堆和栈,其中栈指的就是Java虚拟机栈。每个线程在创建时都是创建一个对应的虚拟机栈,而虚拟机栈又有一个个的栈帧组成。每一个栈帧对应着一次方法调用。当前正在执行的方法对应的是当前栈帧。栈只会执行两种操作:压栈和出栈。栈帧中存储着对应方法的局部变量表,操作数栈,动态链接和方法的返回地址。即虚拟机栈中存储着栈帧,而每个栈帧里面存储着当前方法所需的数据、指令和返回等信息。这个内存区会出现栈溢出、OOM异常

(3)本地方法栈:这个和虚拟机栈作用类似,区别就是虚拟机栈为执行的Java服务,本地方法栈为Native服务

2.2、线程共有

(1)堆内存:Java虚拟机中最大的一块内存,主要目的就是存放对象实例和数组。也是Java虚拟机进行垃圾回收的主要工作区域。

(2)方法区:我们都知道Java中每个类都对应一个Class对象保存这个类的信息,方法区就是用来保存这个类信息的区域。同时它还会保存常量(包括运行时常量池)、静态变量以及编译器编译后的代码等。在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来

三、内存管理—内存分配

Java中的内存时如何分配的?

3.1、堆内存区划分

我们这里所说的内存分配主要说的是堆内存块的分配。而要了解堆内存块的分配规则,我们要先了解JVM对堆内存块的区域划分。

JVM将堆内存进行了进一步的划分,如下:

●新生代(PSYoungGen):MinorGC。

新生代又细分成如下三个空间

  1)、Eden空间  

  2)、FromSurvivor空间

  3)、ToSurvivor空间

Eden空间用来优先分配对象、数组。From和To空间是个交换区。默认情况下这三个空间的内存占比是8:1:1。当然这个比例也是可以通过虚拟机调的。

●老年代(ParOldGen):Full GC

3.2、堆内存区的分配方式

我们创建一个对象时,JVM会去判断当前堆内存是否是规整状态。通过当前堆内存的规整状态来选择指针碰撞还是空闲列表方式进行分配。

指针碰撞:如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”

空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

四、内存分配—内存回收

前面我们也讲了,Java中的内存回收不由开发者来控制,而是由JVM虚拟机的垃圾回收器(GC)来进行回收管理。Java中GC是如何回收一个对象的?

4.1、GC如何判断一个对象可回收

1、引用计数法

  顾名思义当一个对象被另一个对象引用时,将该对象的引用计数+1。相反当该对象被引用对象释放时则-1。当这个对象的引用计数为0时,则认为这个对象是垃圾对象可回收。但是这种方法是不靠谱的,因为这种方法是无法判断一个对象是否真实无用可回收的。比如对象A引用了对象B,同时对象B也引用了A。虽然这两个对象在各自的引用者中都没有被用到,但是这个时候引用计数法就无法判断这个对象是否是可回收对象了。

2、可达性分析算法

  Java中可达性分析首先要确定Gc Roots对象。然后将所有与这个GCRoot对象有直接或间接引用立链的对象都统计为不可回收对象。

这个Gc Roots对象是个啥对象?所谓Gc Roots对象就是如下这些对象实列:

●方法区中类静态属性引用的对象,以及方法区中常量引用的对象

●虚拟机栈(本地变量表)中引用的对象

●本地方法栈(JNI开发Native方法)中引用的对象

 

4.2、GC如何回收内存

在GC经过上述算法确定完那些对象可回收之后就是进行对象的回收操作了。但是回收操作是要根据不同的GC回收器的能力来完成的。而这个能力就是回收器的回收算法能力。

●复制回收算法:它的主要工作区域是年轻代

  它会将年代年轻的内存分出两块同样大小的区域(其实就是前面说的From、To两块内存区)。一块用来正常使用,一块空闲备用等待复制。判断完对象存活状态后,进行回收时,将存活对象复制到备用空间,然后清空之前的那块内存空间等待备用。复制回收算法的缺点就是内存只有50%利用率。这里就可以解释一下为什么前面的年轻代内存区占比是8:1:1了,在Java中我们通常认为有90%的对象是不需要回收的,只有10%的对象需要被回收,而复制回收算法需要预留一份与这个10%一样大小的内存区来备用。所以就得出了8:1:1的空间占比。复制回收算法就是在From和To之间进行内存Copy

 

标记-清除算法:主要作用域是老年代

  判断完内存区对象的存活状态后,会对垃圾对象做一个可回收的标记。等垃圾回收器扫描完所有内存后,一次性清除被标记为可回收的对象。标记算法的缺点是垃圾回收后内存空间是不连续的,存在内存碎片(也就是前面提到的内存不规整状态)。

标记-整理算法:主要作用域是老年代

  标记整理算法就是在标记清除算法的基础上解决了垃圾回收后内存碎片的问题,即清除垃圾对象后,会对内存区进行整理,使其成为规整状态。可是既然要整理内存区,就必然要进行内存移动,就会降低效率。所以它的效率比标记清除算法要低。

 

五、内存管理—内存优化

首先为啥会出现内存优化一说?这是因为我们在开发中经常忽视内存相关的问题,导致功能开发完成后经过测试或者上线后发现经常由于内存的问题带来一连串的恶性循环的后果。比如:内存抖动、内存泄漏等等容易在开发中被忽视的问题最终都有可能引发严重的线上问题。

5.1、内存抖动优化

首先要知道什么是内存抖动:短时间内有大量对象创建销毁,它伴随着频繁的GC。比较直观的现象就是如下图:

 

●内存抖动的危害

(1)频繁的GC会导致程序出现卡顿的现象,简而言之, 就是执行GC操作的时候,任何线程的任何操作都会需要暂停,挂起(stop the word),等待GC操作完成之后,其他操作才能够继续运行, 故而如果程序频繁GC, 自然会导致界面卡顿

(2)频繁GC会导致我们的内存不连续(内存碎片),从而导致OOM。

●内存抖动的预防

(1)避免在循环中创建对象

(2)避免在频繁调用的方法中创建对象,如View的onDraw方法

(3)允许复用的情况下,使用对象池进行缓存。

5.2、内存泄漏优化

为什么会出现内存泄漏?该释放的资源未能够及时释放,导致系统内存的浪费,可用内存逐渐减少。长生命周期对象持有短生命周期对象的强引用,从而导致短生命周期的对象无法被回收。这就是内存泄漏的主要原因。

●内存泄露的危害

(1)频繁的GC会导致程序出现卡顿的现象,简而言之, 就是执行GC操作的时候,任何线程的任何操作都会需要暂停,挂起(stop the word),等待GC操作完成之后,其他操作才能够继续运行, 故而如果程序频繁GC, 自然会导致界面卡顿

(2)频繁GC会导致我们的内存不连续(内存碎片),从而导致OOM。

●内存泄漏优化

(1)合理使用如下四种引用关系

强引用:new出来的对象都是强引用。被强引用的对象无法被垃圾回收。即使内存不足时也无法回收,就有可能造成OOM

软引用SoftReference:被软引用的对象,当内存不足时(即将OOM时),则被回收。

弱引用WeakReference:垃圾回收线程扫描内存区时,发现被弱引用的对象立即回收。

虚引用PhantomReference:任何时候都有可能被回收。

(2)合理使用内存泄漏检测工具

(1)LeakCanary是一个Android和Java的内存泄漏检测库,可以大幅可以大幅度减少了开发中遇到的OOM问题。

(2)Profile + MAT

Android Studio自带的Profile工具大部分时候都能够帮助我们定位内存的使用情况,但也会有出现泄漏时无法通过Profile工具来直观定位到泄漏点的情况,这时候就需要Profile工具配合MAT工具一起来排查泄漏点了。

这里简单列一下两个工具配合使用的步骤:

●导出profiler 的Heap Dump 文件

●找到Android SDK中platfrom-tools目录下的hprof-conv.exe工具(可以直接配置到环境变量里面)

●转换导出的Heap Dump文件: hprof-conv.exe -z 导出的HeapDump文件.hprof  转换后的目标文件.hprof

  如果不转换,格式不兼容,MAT无法查看。

●使用MAT工具打开转换后的hprof文件。

 

六、总结

Android开发过程中内存一直是个容易引发各类问题的根源,而这个问题根源又常因为开发过程中的各类因素没有被及时的发现,所以在开发过程中重视内存的检测和优化是我们要时刻关注的一个点。

 

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。