背景

内存优化–相关知识

需求–内存走漏优化,PSS有所下降, OOM率削减

主要是与某个版别作基准进行比照(一般是最新版别的前一个版别作原数据),优化后,PSS有所下降,线上OOM率削减(Bugly版别比照),走漏点削减(从捉取一些线上上传回来的内存堆栈信息剖析,或本地测试后dump下hprof文件剖析)。

内存走漏优化的思路

  • 了解什么是内存走漏
  • 了解虚拟机中的目标的创立进程
  • 了解Java内存分配模型
  • 了解废物收回分代搜集理论
  • 了解java的引证类型
  • GC是怎么判别目标存活
  • 有哪些目标可作为GC Roots
  • 了解内存走漏的东西
  • 了解Android内存剖析命令
  • 常见的内存问题场景
  • 剖析常见的内存走漏问题场景比如
  • 总结

什么是内存走漏

App程序中己动态分配的堆内存,由于某种原因,App程序未开释或无法开释,会造成体系(手机)内存的糟蹋。长生命周期目标持有短生命周期目标强引证,从而导致短生命周期目标无法被收回。 咱们留意这两个关键词堆内存、强引证

虚拟机中的目标的创立进程(类的生命周期)

什么都不用说,先上张自画图。为咱们引荐一本书《深入理解JVM》

记一次项目内存优化--内存泄漏

榜首步,当虚拟机遇到一条new指令时,首要查看这条指令的参数是否能在常量池中定位到一个类的符号引证,而且查看这个符号引证代表的类是否已被加载、解析和初始化过。假如没有,那有必要先履行相应的类加载进程。

第二步,假如查看通往后,虚拟机将为这个new出的目标进行分配内存。区分内存是经过指针磕碰、空间列表的组合,一起也考虑并发安全问题(CAS(Compare And Swap的缩写–达观锁)失利重试、本地线程内存缓冲)。这中是进行内存分配哦,这时候还不能确认目标所需求的内存巨细。在类加载完结后才确认内存的巨细。java虚拟机创立目标时经过什么确认目标所需内存的巨细?

第三步,内存分配完结后,虚拟机将分配到的内存空间进行初始化为零值(默许的初始值),但不包含目标头信息,假如运用TLAB(Thread Local Allocation Buffer ,即线程本地分配缓冲区),这进程能够提前至TLAB分配时进行(Eden区区分出一小块区域作为TLAB)。这一步操作是保证了目标的实例成员(字段)在Java代码中能够不赋初始值就直接运用,程序能访问到这些字段的数据类型所对应的零值。

第四步,目标进行必要的设置(主要是一些目标头信息的设置),例如这个目标的运转状态、GC分代年纪、锁状态、归于哪个类的实例、哈希码等等信息。

第五步,从前面几个步骤知道,只是设置了目标头信息和所有类成员(字段)赋为默许的初始值,目标并没有履行办法,所以最后是会接着履行办法,这样才算创立出一个真实可用的目标。简直所有目标是寄存在堆区。

Java内存分配模型

运用一张图,快速了解一下内存分配模型。

记一次项目内存优化--内存泄漏

办法区

编译时就分配好,在程序整个运转期间都存在。它用于存储现已被虚拟机加载的类信息、静态变量、常量等数据。

记一次项目内存优化--内存泄漏

堆区

简直所有 new 出来的目标是寄存在堆区,由 Java 废物收回器收回。堆中的目标是废物收回的要点。 堆区的区分新生代、老时代:

记一次项目内存优化--内存泄漏

新生代

新生代是用来寄存新new出来的目标,区分为 Eden区、From Survivor区 、To Survivor区 。简直所以的new出来的目标都会寄存在Eden区 (假如new出来的目标占用的内存十分大,在新生代中寄存不下,直接进入老时代寄存)。

当Eden区的内存空间缺乏时,体系会触发Minor GC /Young GC进行收回Eden区的目标(From Survivor区 、To Survivor区不会触发GC),经过GC后,一些目标依然存活(目标被引证着–经过GC Root可达性来判别),则会被移到To Survivor区寄存,当目标在Survivor区熬过一次GC后,此目标的GC年纪就会+1(GC年纪是目标头信息的一个符号参数),会被仿制到From Survivor区,当From Survivor区的目标到达必定年纪时(默许年纪是15,但能够经过XX:MaxTenuringThreshold设置),被移到老时代,不然仿制到To Survivor区。

老时代

老时代是新生代寄存不下的大目标,或目标经过多次Minor GC /Young GC后依然存活的目标(长期存活的目标)。

当跟着Eden区的Minor GC /Young GC继续进行,老时代的目标继续增加,会导致老时代可用的内存空间也会继续削减,终究体系会触发Major GC。

元空间(永久代)

永久代(耐久代)是寄存包含应用的类/办法信息,以及JRE库的类和办法信息。然而在Java8中,元空间替代了永久代,元空间(Metaspace)被称为“元数据区”。

需求留意的是:元空间并不在虚拟机中哦,而是运用本地内存(以前永久代是在jvm中的)。这样就处理了以前永久代的OOM问题,元数据和class目标寄存在永久代中,容易呈现功能问题和内存溢出,毕竟是和老时代共享堆空间。

堆内存分配战略

记一次项目内存优化--内存泄漏

内存分配原则

  • 目标优先在Eden分配—-假如说Eden内存空间缺乏,就会发生Minor GC /Young GC。
  • 大目标直接进入老时代—-大目标:需求许多连续内存空间的Java目标,比如很长的字符串和大型数组。会导致新生代内存有空间,还是需求提前进行废物收回获取连续空间来放此大目标。Survivor区会进行许多的内存仿制,-XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老时代分配,缺省为0 ,表明绝不会直接分配在老时代。当Eden分配和Survivor区都没有满足空间寄存此大目标时,则直接分配到老时代。
  • 长期存活的目标将进入老时代—-Survivor区的目标到达必定年纪时,直接移到老时代。默许15岁,能够经过XX:MaxTenuringThreshold设置。
  • 动态目标年纪断定—-为了能更好地习惯不同程序的内存状况,虚拟机并不是永远地要求目标的年纪有必要到达了MaxTenuringThreshold才能晋升老时代,假如在Survivor区中相同年纪所有目标巨细的总和大于Survivor区的一半,年纪大于或等于该年纪的目标就能够直接进入老时代,无须等到MaxTenuringThreshold中要求的年纪。
  • 空间分配担保:新生代中有许多的目标存活,Survivor区不够,当呈现许多目标在Minor GC后依然存活的情况(最极点的情况便是内存收回后新生代中所有目标都存活),就需求老时代进行分配担保,把Survivor区无法容纳的目标直接进入老时代,只要老时代的连续空间大于新生代目标的总巨细或许每次晋升的平均巨细,就进行Minor GC,不然Full GC。

栈平分配目标

  • 逃逸剖析—-假如符合逃逸剖析规则,则在栈平分配目标。

堆中的优化技术

  • TLAB —-Thread Local Allocation Buffer ,即线程本地线程分配缓冲。

栈区

当办法履行时,会在栈区内存中创立办法体内部的局部变量,办法结束后主动开释内存。

废物收回分代搜集理论

记一次项目内存优化--内存泄漏

咱们都知道,在java中不同的目标存在不同的生命周期的,java目标在JVM中也寄存在不同的区域,所以对不同生命周期不同的寄存区,采纳不同的收回战略,以提高功率。

当Eden区的内存空间缺乏时,体系触发Minor GC/Young GC, 跟着GC继续进行,老时代的目标继续增加,导致老时代的内存空间缺乏,体系触发Major GC。当堆区或办法区内存空间缺乏时,体系触发Full GC。

Full GC:整理本钱高,体系资源耗费高,对体系功能产生影响,许多功能什么都是针对Full GC进行的。 触发Full GC的条件有:

  • 调用System.gc()
  • 办法区空间缺乏
  • 堆区空间缺乏

不同阶段GC的特点

  • Minor GC/Young GC — 履行十分频繁,速度特别快。
  • Major GC — 速度上,一般会比Minor GC/Young GC慢十倍以上。
  • Full GC — Minor GC和Major GC都会履行,会发出”Stop the World”事件,会中断程序运转,直到GC完结。所以Full GC时,咱们会感知到APP有卡顿之感。

废物收回分代搜集对应的收回算法

  • 仿制算法: 完成简单,运转高效,内存仿制,内存运用率只要一半。
  • 符号-清除: 运用率100%,不需求内仿制,有内存碎片
  • 符号-整理:运用率100%,没有内存碎片,需求内存仿制(整理存活的目标,将其拷贝到一块连续内存中)

GC是怎么判别目标存活

  • 可达性剖析 (java) 经过一系列称之为“GC Roots”的目标作为起始点,从这些节点向下查找,查找所有的引证链,当一个目标到GC Roots有引证链,则说明这个目标存活着;当一个目标到GC Roots没有任何引证链(即GC Roots到目标不可达)时,则证明此目标是不可用的(所谓的废物)。

  • 引证计数算法(JVM前期运用的—现已不运用) A目标引证B 目标(+1),一起C目标引证B目标(1+1=2),计数法便是引证一次累加1次,假如没有引证就累减1次,假如归到0时,说明没有引证。缺陷:便是彼此引证。如A目标引证B目标,一起B目标引证A目标,很难去判别目标是否应该收回。

记一次项目内存优化--内存泄漏

在Java, 可作为GC Roots的目标包含:

  • 办法区: 类静态特点的目标;
  • 办法区: 常量的目标;
  • 虚拟机栈(本地变量表)中的目标。
  • 本地办法栈JNI(Native办法)中的目标。

四种引证类型

  1. 强引证(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 收回具有强引证的目标。
  2. 软引证(SoftReference):只要在内存空间缺乏时,目标才会被收回。
  3. 弱引证(WeakReference):在 GC 时,一旦发现了只具有弱引证的目标,不管当时内存空间满足与否,目标都会被收回。
  4. 虚引证(PhantomReference):任何时候都能够被GC收回,当废物收回器准备收回一个目标时,假如发现它还有虚引证,就会在收回目标的内存之前,把这个虚引证加入到与之相关的引证行列中。程序能够经过判别引证行列中是否存在该目标的虚引证,来了解这个目标是否将要被收回。能够用来作为GC收回Object的标志。

咱们界说目标,应该考虑运用那种引证,多考虑运用软引证(界说一些还有用但并非有必要的目标)或弱引证(界说非有必要目标)。

Andriod中剖析内存走漏的东西MAT(会出一篇文章解说)

  1. MAT(Memory Analyzer Tools)是一个 Eclipse 插件,它是一个快速、功能丰厚的JAVA heap剖析东西,它能够帮助咱们查找内存走漏和削减内存耗费。
  2. MAT 插件的下载地址:www.eclipse.org/mat
  3. MAT 运用办法介绍:www.cnblogs.com/larack/p/60…

运用leakcanary检测走漏 (会出一篇文章解说)

  1. LeakCanary: 让内存走漏无所遁形www.liaohuqiu.net/cn/posts/le…
  2. LeakCanary:中文运用说明www.liaohuqiu.net/cn/posts/le…

LeakCanary的内存走漏提示一般会包含三个部分:

  1. 榜首部分(LeakSingle类的sInstance变量)。
  2. 引证第二部分(LeakSingle类的mContext变量)。
  3. 导致第三部分(MainActivity类的实例instance)走漏。

Android Studio的profiler东西

  1. 咱们也能够运用Android Studio的profiler东西,方便快速查找、观察,简单剖析一些目标生成情况。
  2. 也能够dump下hprof文件,结合MAT深入剖析与排查,目标发生是否走漏。
  3. 留意:MAT打开Android Studio的profiler里dump下hprof文件时,运用AS自带的hprof东西转化一下格式(经过命令hprof-conv -z 原hprof文件 输出hprof文件),不然打开是乱码。

当然检测内存走漏的东西和办法有许多,就不一一列举了,感兴趣的能够网上查阅一下。

常见的内存问题场景

  • 静态成员/单例
    • 作为GC ROOT,持有短生命周期引证(如Activity)导致其短生命周期目标无法开释。
  • 集合类
    • 当运用集合时,只要增加元素,没有对应的删除元素。
  • 非静态内部类/匿名内部类
    • 如Handler postDelayed一个匿名Runnable,退出Activity时音讯没处理完。
  • 上下文 — Context
    • 持有的上下文,需求特别留意。
  • 注册/反注册
    • 如EventBus只要注册没有刊出。addXXXListener函数,需求有对应的removeXXXListener等等。
  • 未关闭/开释资源
    • 如FileOutputStream未close。
  • 体系Bug
    • WebView、InputMethodManager等

常见的内存问题场景比如

//todo 专门有一篇文章去介绍

总结

  • 上面的内存相关知识也是自己学习的一种总结,有过错的能够留言纠正。
  • 内存优化,需求对下面的知识有必定的了解。
    • Java内存分配模型
    • Java的四大引证及其运用场景
    • 内存检测东西及常用命令
    • GC Root的界说
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。