Skip to content

Latest commit

 

History

History
297 lines (209 loc) · 13.2 KB

performance_memory3.md

File metadata and controls

297 lines (209 loc) · 13.2 KB

[TOC]

三、内存优化

3.1 Android内存管理机制

3.1.1 Java虚拟机概念

Java虚拟机是一台“抽象的计算机”,它拥有自己的处理器,堆栈,寄存器以及相应的指令系统;Java虚拟机屏蔽了与具体操作系统相关的平台信息,使得Java程序只需要生成在该虚拟机上运行的目标代码,就可以在多平台上运行。虽然叫Java虚拟机,但在它之上运行的语言不仅有JavaKotlinGroovyScala等都可以运行。

Java虚拟机包括:类加载系统、运行时区域、执行引擎、本地方法库等。

3.1.2 Java虚拟机执行流程

image

3.1.3 Java虚拟机运行时数据区域

  1. 方法区(公有):被JVM加载的类的结构信息,包括运行时常量池、字段、方法信息、静态变量等数据;
  2. Java堆(公有):JVM启动时创建,存储几乎所有对象的实例,可以细分为老年代、新生代(EdenFrom SurvivorTo Survivor),垃圾回收器主要就是管理堆内存,如果满了,就会出现OutOfMemoryError
  3. Java虚拟机栈(私有):存储Java方法调用的状态,一个线程会执行一个或多个方法,一个方法对应一个栈帧,栈帧内容包含:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等;栈内默认最大是1M,超出则抛出StackOverflowError
  4. 本地方法栈(私有):执行native方法;
  5. 程序计数器(私有):多线程中记录程序执行下一条指令的计数器,记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行Native方法,则计数器值为空。

image

  • 线程独自:每个线程都会有它独立的空间,随线程生命周期而创建和销毁;
  • 线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁。

3.1.4 class文件内容

class文件包含JAVA程序执行的字节码:数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符;文件开头有一个0xcafebabe(16进制)特殊的一个标志,如下图所示:

image

3.1.5 强软弱虚引用

  • 强引用:当新建的对象为强引用时,垃圾回收器绝对不会回收它,宁愿抛出OutOfMemoryError异常,让程序异常终止也不会回收;
  • 软引用:当新建的对象为软引用时,在内存不足时,回收器就会回收这些对象,如果回收后还是没有足够的内存,抛出OutOfMemoryError异常;
  • 弱引用:当新建的对象为弱引用时,垃圾回收器不管当前内存是否足够,都会回收它的内存;
  • 虚引用:虚引用跟其他引用都不同,如果一个对象仅持有虚引用,在任何时候都可能被GC回收,只是当它被回收时会收到一个系统通知。

3.1.6 垃圾标记算法

  • 引用计数算法:每个对象都有一个引用计数器,当对象每被引用一次时就加1,引用失效时就减1;当计数为0时则将该对象设置为可回收的“垃圾对象”; 缺点:循环引用不能回收;
class Bean{
  public Object b = null;
  private byte[] data = new byte[1024 * 1024];
}

public class GCTest{
  public static void main(String[] args) {
    Bean b1 = new Bean();
    Bean b2 = new Bean();
    b1.b = b2;
    b2.b = b1;
    b1 = null;
    b2 = null;
    System.gc();
  }
}
  • 可达性分析算法:将对象及其引用关系看做一个图,选定活动对象作为GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之间不可达,也就是不存在引用,那么认为是可回收对象。

image

可以作为GC Roots的对象:

  1. 虚拟机栈中正在引用的对象;
  2. 本地方法栈中正在引用的对象;
  3. 静态属性引用的对象;
  4. 方法区常量引用的对象;

3.1.7 垃圾收集算法

  • 标记-清除算法:用根搜索算法标记可被回收的对象,之后将被标记为“垃圾”的对象进行回收; --> 内存碎片
  • 复制算法(年轻代):先把内存一分为二,每次只使用其中一个区域,垃圾收集时,将存活的对象拷贝到另一个区域,然后对之前的对象全部回收;--> 减小了内存使用空间
  • 标记-压缩算法(老年代):在标记可回收的对象后,将所有的存活对象压缩到内存的一段,让它们排在一个,然后对边界以外的内存进行回收;
  • 分代收集算法:Java堆中存在的对象生命周期有较大差别,大部分生命周期很短,有的很长,设置与应用程序或者Java虚拟机生命周期一样。因为分代算法就是根据对象的生命周期长短,将对象放到不同的区域;

image

堆区:堆区分为年轻代和老年代,其空间大小理论比值为2:1;其中年轻代又会分为Eden区和Survivor区,其空间大小理论比值为8:2;Surfivor区又分为from区和to区,其空间大小理论比值为1:1。 gc流程:创建对象时首先会被放入Eden区,该区存满时会触发gcgc时清除可回收对象,然后把Eden区剩余存活对象移动到From区;新创建的对象会继续被放入Eden区,第二次gc时清除Eden区和和From区可回收对象,然后把Eden区和From区剩余存活对象移动到To区;第三次gc时会把Eden区和To区中剩余存活对象移动到From区……依次反复进行。

image

从年轻代进入老年代的条件: 大对象,大对象会直接进入老年代; 每次gc时会对已存活对象进行标记(每次+1),标记达到一定次数(Java为15次,AndroidCMS垃圾回收器为6次)时该对象会从年轻代进入老年代; Survivor区中FromTo区中的相同标记(相同年龄)对象大小总和大于等于FromTo区的一半时,这些对象可以进入老年代。 在Java环境的bin目录下有一个jvisualvm工具,该工具可以观察到程序运行过程中内存的动态情况,从而证实上述描述。

3.2 内存抖动

内存抖动通常指在短时间内发生了多次内存的分配和释放,主要原因是短时间内频繁地创建对象。为了应对这种情况,虚拟机会频繁地触发GC操作,当GC进行时,其它线程会被挂起等待GC完成,频繁GC会使UI在绘制时超过16ms一帧,从而导致画面卡顿。

3.2.1 内存抖动测试

public class ChurnActivity extends AppCompatActivity {
    private Handler mHandler;
    private Button btnChurn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_churn);

        initView();
        setViewsListener();
    }

    private void initView() {
        btnChurn = findViewById(R.id.btn_churn);
        mHandler = new Handler();
    }


    private void setViewsListener() {
        btnChurn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                churn(0);
            }
        });
    }

    Runnable r = new Runnable() {
        @Override
        public void run() {
            allocate();
        }
    };

    private void allocate() {
        for (int i = 0; i < 1000; i++) {
            String ob[] = new String[10000];
        }
        churn(50);
    }

    private void churn(int delay) {
        mHandler.postDelayed(r, delay);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacks(r);
    }
}

3.2.2 内存抖动分析

使用Profiler查看内存抖动:

image

3.3 内存泄漏及排查工具的使用

  • 内存泄漏:一个不再被程序使用的对象或变量依旧存活在内存中无法被回收;
  • 内存溢出:当程序申请内存时,没有足够的内存供程序使用;

比较小的内存泄漏并不会有太大的影响,但内存泄漏多了,占用的内存空间就更大,程序正常需要申请的内存则会相应减少。

内存泄漏分析工具使用MAT,它除了可以分析内存泄漏之外,还可以分析大对象。

3.3.1 下载MAT

官方下载地址:https://www.eclipse.org/mat/downloads.php

3.3.2 安装MAT

下载的是MAC版,安装时遇到一个问题:

The platform metadata area could not be written: /private/var/folders/9j/zj116b2n765fkk7qm1s7ctq8000

解决方法:在应用程序中右键mat.app-->显示包内容-->Contents/Eclipse/MemoryAnalyzer.ini,修改内容如下:

image

3.3.3 获取hprof文件

借助Android StudioProfile工具,在操作页面之前dump(截取该时间点内存中存在的对象)一份文件,操作页面(比如进入SecondActivity 然后再返回主页面)之后再dump一份文件。然后把这两份文件(memory1.hprof,memory2.hprof)保存到本地。

image

3.3.4 转换hprof文件

使用Android SDK环境sdk/platform-tools/目录下的hprof-conv工具将3.3.3获取的hprof文件转换为MAT可以识别的文件:

hprof-conv -z memory1.hprof memory1_after.hprof
hprof-conv -z memory2.hprof memory2_after.hprof

3.3.5 Mat分析hprof文件

首先用Mat打开(Open Heap Dump..)两个转换后的hprof文件:

image

选择直方图:

image

排除其他引用:

image

定位结果:

image

因为我们进入SecondActivity之后又退出页面了,按道理其不应该存在,但此时排除其他引用之后发现它仍然存活,由此可以判断内存泄漏。从上图可以看出这是匿名内部类持有外部类引用引起的内存泄漏,需要在页面销毁时结束动画。

参考:JVM共享区深入了解及内存抖动/泄漏排查优化

3.3.6 项目实战

模块lqr_wechat是一个仿微信的项目,登录账号:dawa,密码:123456。在多次登录并退出之后会发现MainActivity存留多个,表明存在内存泄漏情况。通过Mat排查可知因使用网易云信的SDK并且没有及时注销引起内存泄漏,解决方式如下:

    @Override
    protected void onDestroy() {
        unRegisterBroadcastReceiver();
        super.onDestroy();
        // 在这里将网易云信的注册 注销掉
        NimAccountSDK.onlineStatusListen(mOnlineStatusObserver, false);
        NimUserInfoSDK.observeUserInfoUpdate(userInfoobserver, false);
        NimFriendSDK.observeFriendChangedNotify(changedNotifyObserver, false);
        NimSystemSDK.observeReceiveSystemMsg(systemMessageObserver, false);
        NimTeamSDK.observeTeamRemove(teamobserver, false);
    }

另在低版本上因输入法引起的内存泄漏解决方法:

@Override
protected void onDestroy() {
  unRegisterBroadcastReceiver();
  super.onDestroy();
  // InputMethodManagerdManager
  // mServedView
  // mNextServedView
  method("mServedView");
  method("mNextServedView");
}

// 暴力置null(反射)
public void method(String attr){
  InputMethodManager im = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
  try {
    Field field = InputMethodManager.class.getDeclaredField(attr);
    field.setAccessible(true);
    Object curView = field.get(im);
    if(null != curView){
      Context context = ((View)curView).getContext();
      if(context == this){
        field.set(im,null);
      }
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
}

3.4 内存优化总结

3.4.1 工具使用

  • 使用Memory profiler检测内存抖动;
  • 使用MAT检测内存泄漏;
  • 使用LeakCannary线下监控;
  • 采用Glide等三方库加载图片。

3.4.2 优化点

  • 避免在for循环里分配对象占用内存;
  • 自定义ViewonDraw方法避免执行复杂的方法与创建对象;
  • 采用对象池模型解决频繁创建与销毁;
  • bitmap做缩放,重用bitmap
  • 配置LargeHeap属性;
  • onTrimMemory进行处理;
  • 使用松散数组:SparseArray,ArrayMap