1、JAVA内存解析
要想知道内存泄露,须要先理解java中运行时内存是怎么构成的,才能知道是哪个地方导致。话不多说,先上图
java内存模型
运行时的java内存分为两大块:线程私有(蓝色区域)、共享数据区(黄色区域)
线程私有:紧张用于存储各个线程私有的一些信息,包括:程序计数器、虚拟机栈、本地方法栈
共享数据区:紧张用于存储公用的一些信息,包括:方法区(内含常量池)、堆程序计数器:让程序中各个线程知道自己接下来须要实行哪一行。在java中多线程为抢占式(由于cpu在某一时候只会实行一条线程),当线程切换时,须要连续哪一行便由程序计数器奉告。举个例子:A、B两条线程,此时CPU实行从A切换至B,过了段韶光从B切换回A,此时A须要早年次停息的地方连续实行,此时从哪一行实行便是由程序计数器来供应。值得一提:(1)若实行java函数时,程序计数器记录的是虚拟机字节码的地址;(2)若实行native方法时,程序计数器便置为了null。(3)在java虚拟机规范中,程序计数器是唯一没有定义OutOfMemoryError。虚拟机栈:描述的是java方法的内存模型,平时说的“栈”实在便是虚拟机栈,其生命周期与线程相同。每个方法(不包含native方法)实行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。值得一提:在java虚拟机规范中,此处定义了两个非常(1)StackOverFlowError (在递归中常看到,递归层级过深)(2)OutOfMemoryError本地方法栈:是为虚拟机利用到的Native方法供应内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如主流的HotSpot虚拟机。值得一提:在java虚拟机规范中,此处定义了两个非常(1)StackOverFlowError (在递归中常看到,递归层级过深)(2)OutOfMemoryError方法区:紧张存储已加载是类信息(由ClassLoader加载)、常量、静态变量、编译后的代码的一些信息。GC在这里比较少涌如今这块区域。堆:存放的是险些所有的工具实例和数组数据。是虚拟机管理的最大的一块内存,是GC的主沙场,以是也叫“GC堆”、“垃圾堆” 。值得一提:在java虚拟机规范中,此处定义了一个非常(1)OutOfMemoryError运行时常量池:属于“方法区”的一部分,用于存放编译器天生的各种字面量和符号引用。字面量:与Java措辞层面的常量观点附近,包含文本字符串、声明为final的常量值等。符号引用:编译措辞层面的观点,包括以下3类:(1) 类和接口的全限定名(2)字段的名称和描述符(3)方法的名称和描述符2、JAVA回收机制
java中是通过GC(Garbage Collection)来进行回收内存,那jvm是如何确定一个工具能否被回收的呢?这里就需讲到其回收利用的算法
引用计数算法
引用计数是垃圾网络器中的早期策略。在这种方法中,堆中每个工具实例都有一个引用计数。当一个工具被创建时,且将该工具实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个工具的引用时,计数加1(a = b,则b引用的工具实例的计数器+1),当一个工具实例的某个引用超过了生命周期或者被设置为一个新值时,工具实例的引用计数器减1。任何引用计数器为0的工具实例可以被当作垃圾网络。当一个工具实例被垃圾网络时,它引用的任何工具实例的引用计数器减1。
优点:
引用计数网络器可以很快的实行,交织在程序运行中。对程序须要不被永劫光打断的实时环境比较有利。
缺陷:
无法检测出循环引用。如父工具有一个对子工具的引用,子工具反过来引用父工具。这样,他们的引用计数永久不可能为0。例如下面代码片段中,末了的Object实例已经不在我们的代码可控范围内,但其引用仍为1,此时内存便产生泄露。
/举个例子/Object o1 = new Object() //Object的引用+1,此时计数器为1Object o2;o2.o = o1; //Object的引用+1,此时计数器为2o2 = null;o1 = null; //Object的引用-1,此时计数器为1
可达性剖析算法
可达性剖析算法是现在java的主流方法,通过一系列的GC ROOT为起始点,从一个GC ROOT开始,探求对应的引用节点,找到这个节点往后,连续探求这个节点的引用节点,当所有的引用节点探求完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点(即图中的ObjD、ObjE、ObjF)。由此可知,即时引用成环也不会导致泄露。
java中可作为GC Root的工具有:
1、方法区中静态属性引用的工具
2、方法区中常量引用的工具
3、本地方法栈JNI中引用的工具(Native工具)
4、虚拟机栈(本地变量表)中正在运行利用的引用
但是,可达性剖析算法中不可达的工具,也并非一定要被回收。当GC第一次扫过这些工具的时候,他们处于“去世缓”的阶段。要真正实行去世刑,至少须要经由两次标记过程。如果工具经由可达性剖析之后创造没有与GC Roots干系联的引用链,那他会被第一次标记,并经历一次筛选,这个工具的finalize方法会被实行。如果工具没有覆盖finalize或者已经被实行过了。虚拟机也不会去实行finalize方法。Finalize是工具越狱的末了一次机会。
3、四种引用
说到底,内存泄露是由于引用的处理不正当导致的。以是,我们接下来须要旧调重弹一下java中四种引用,即:强懦弱虚(引用强度依次减弱)。
(1)强引用(Strong reference):一样平常我们利用的都是强引用,例如:Object o = new Object();只要强引用还在,垃圾网络器就不会回收被引用的工具。
(2)软引用(Soft Reference):用来定义一些还有用但并非必须的工具。对付软引用关联着的工具,在系统将要内存溢出之前,会将这些工具列入回收范围进行第二次回收,如果回收后还是内存不敷,才会抛出内存溢出。(即在内存紧张时,会对其软引用回收)
(3)弱引用(Weak Reference):用来描述非必须工具。被弱引用关联的工具只能生存到下一次垃圾网络发生之前。当垃圾网络器回收时,无论内存是否足够,都会回收掉被弱引用关联的工具。(即GC扫过期,便将弱引用带走)
(4)虚引用(Phantom Reference):也称为幽灵引用或者幻影引用,是最弱的引用关系。一个工具的虚引用根本不影响其生存韶光,也不能通过虚引用得到一个工具实例。虚引用的唯一浸染便是这个工具被GC时可以收到一条系统关照。
软引用与弱引用的决议
如果只是想避免OutOfMemory非常的发生,则可以利用软引用。如果对付运用的性能更在意,想尽快回收一些占用内存比较大的工具,则可以利用弱引用。其余可以根据工具是否常常利用来判断选择软引用还是弱引用。如果该工具可能会常常利用的,就只管即便用软引用。如果该工具不被利用的可能性更大些,就可以用弱引用。
4、小结
至此,我们知道内存泄露是由于堆内存中的长生命周期的工具持有短生命周期工具的引用,只管短生命周期工具已经不再须要,但是由于长生命周期工具持有它的引用而导致不能被回收。
5、安卓内存泄露排核对象
所谓工欲善其事必先利其器,这一小节先简述下所需借用到的内存泄露排核对象,如果已经熟习的话可以跳过。
(1)Android Profiler
这一工具是Android Studio自带,可以查看cpu、内存利用、网络利用情形,Android Studio3.0中用于替代Android Monitor
① 逼迫实行垃圾网络事宜的按钮。
② 捕获堆转储的按钮。
③ 记录内存分配的按钮。
④ 放大韶光线的按钮。
⑤ 跳转到实时内存数据的按钮。
⑥ 事宜韶光线显示活动状态、用户输入事宜和屏幕旋转事宜。
⑦ 内存利用韶光表,个中包括以下内容:
• 每个内存种别利用多少内存的堆栈图,如左边的y轴和顶部的颜色键所示。
• 虚线表示已分配工具的数量,如右侧y轴所示。
• 每个垃圾网络事宜的图标。
(2)MAT(Memory Analyzer Tool)
MAT用于锁定哪里泄露。由于从Android Profiler中,知道了泄露,但比较难锁定详细哪个地方导致了泄露,以是借助MAT来锁定,详细利用待会会借助一个例子合营Android Profiler来先容,稍安勿躁。
下载地址:http://www.eclipse.org/mat/downloads.php
6、内存泄露检讨与办理流程
经由前面的一段理论,可能很多小伙伴都有些不耐烦了,现在便来真正的操作。
温馨提示:理论是进阶中必要的支持,否则只是知其然而不知其以是然。
(1)第一步:对待检测功能扫雷式操作
当我们须要检讨一块模块,或是全体app哪个地方有内存泄露时,有时会比较茫然,有些大海捞针的觉得,毕竟泄露不是每个页面都会有,而且有时是一个功能才会导致泄露,以是我们可以采纳“扫雷式操作”,也便是在须要检讨的页面和功能中随便先利用一番,举个例子:假设检讨MainActivity泄露情形,可以登录进入后,此时来到了MainActivity,后又登出,再次登录进入MainActivity。
(2)第二步:借助 Android Profiler得到内存快照
利用Android Profiler的GC功能,逼迫进行垃圾回收,再dump下内存(\"大众Android Profiler功能简介\"大众图的②按钮)。然后等待一段韶光,会涌现图中赤色框部分:
在这里得到的页面,实在比较难直不雅观得到内存剖析的数据,最多只是选择“Arrange by package”按照包进行排序,然后进到自己的包下,查看运用内的activity的引用数是否正常,来判断其是否有正常回收
图中列的解释
Alloc Cout : 工具数
Shallow Size : 工具占用内存大小
Retained Set : 工具引用组占用内存大小(包含了这个工具引用的其他工具)
(3)第三步:借助Android Studio剖析
至此,我们还是没得到直不雅观的内存剖析数据,我们须要借助更专业的工具。我们现将通过下图中红框内的按钮,将刚才的内存快照保存为hprof文件。
将保存好的hprof文件拖进AS中,勾选“Detect Leaked Activities”,然后点击绿色按钮进行剖析。
如果有内存泄露的话,会涌现如下图的情形。图中很清晰的可以看到,这里涌现了MainActivity的泄露。并且不雅观察到这个MainActivity可能不止一个工具存在,可能是我们上次退出程序的时候发生了泄露,导致它不能回收。而在此打开app,系统会创建新的MainActivity。但至此我们只是知道MainActivity泄露了,不知详细是哪里导致了MainActivity泄露,以是须要借助MAT来进一步剖析。
(4)第四步:hprof文件转换
在利用MAT打开hprof文件前先要对刚才保存的hprof文件进行转换。通过终端,借助转换工具hprof-conv(在sdk/platform-tools/hprof-conv),利用命令行:hprof-conv -z src dst
-z:打消不是app的内存,比如Zygote
src:须要进行转换的hprof的文件路径
dst:转换后的文件路径(文件后缀还是.hprof)
(5)第五步:通过MAT进行详细剖析
在MAT中打开转换了的hprof文件,如下图
打开后会看到如下图
我们须要进入到\公众Histogram\"大众来剖析,点击下图中的按钮
打开\公众Histogram\"大众后,会看到下图,在红框中输入在AS中不雅观察到的泄露的类,例如上面得知的MainActivity
然后将搜索得到的结果进行合并,打消“软”、“弱”、“虚”引用工具,右键点击搜索到的结果,选择如下图的选项
得到合并结果如下
从剖析结果可知,MainActivity是由于com.netease.nimlib.g.e中的一个hashMap持有导致,这里的e类是第三方库的类,显然已被稠浊,造成泄露无非两种可能,一种是第三方库的bug,一种是自己利用不当,例如忘却解绑操作等。详细的打断这个持有须要按照自己的代码进行剖析,实例中的问题是由于利用第三方库注册后,在退出页面没有进行注销导致的。当我们办理完后,可以再次进行一轮内存快照,直到没有内存泄露,过程会比较呆板,但一点点的办理泄露终极会给app一个质的飞跃。
7、常见的内存泄露缘故原由
(1)凑集类
凑集类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个凑集类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一贯指向它),那么没有相应的删除机制,很可能导致凑集所占用的内存只增不减。
(2)单例模式
禁绝确利用单例模式是引起内存透露的一个常见问题,单例工具在被初始化后将在 JVM 的全体生命周期中存在(以静态变量的办法),如果单例工具持有外部工具的引用,那么这个外部工具将不能被 JVM 正常回收,导致内存透露。
public class SingleTest{ private static SingleTest instance; private Context context; private SingleTest(Context context){ this.context = context; } public static SingleTest getInstance(Context context){ if(instance != null){ instance = new SingleTest(context); } return instance; }}
这里如果通报Activity作为Context来得到单例工具,那么单例持有Activity的引用,导致Activity不能被开释。
不要直接对 Activity 进行直接引用作为成员变量,如果许可可以利用Application。如果不得不须要Activity作为Context,可以利用弱引用WeakReference,相同的,对付Service 等其他有自己生命周期的工具来说,直接引用都须要谨慎考虑是否会存在内存透露的可能。
(3)未关闭或开释资源
BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。值得把稳的是,关闭的语句必须在finally中进行关闭,否则有可能由于非常未关闭资源,致使activity泄露
(4)Handler
只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 工具将被线程 MessageQueue 一贯持有。特殊是handler实行延迟任务。以是,Handler 的利用要尤为小心,否则将很随意马虎导致内存透露的发生。
public class MainActivity extends AppCompatActivity { private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { //do something } }; private void loadData(){ //do request Message message = Message.obtain(); mHandler.sendMessage(message); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadData(); }}
这种创建Handler的办法会造成内存泄露,由于mHandler是Handler的非静态匿名内部类的实例,以是它持有外部类Activity的引用,我们知道行列步队是在一个Looper线程中不断轮询处理,那么当这个Activity退出时行列步队中还有未处理的或者正在处理,而行列步队中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,以是导致该Activity的内存资源无法及时回收,引发内存泄露,以是其余一种做法为:
public class MainActivity extends AppCompatActivity { private MyHandler mHandler = new MyHandler(this); private void loadData() { //do request Message message = Message.obtain(); mHandler.sendMessage(message); } private static class MyHandler extends Handler { private WeakReference<Context> reference; public MyHandler(Context context) { reference = new WeakReference<Context>(context); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity mainActivity = (MainActivity) reference.get(); if (mainActivity != null) { //do something to update UI via mainActivity } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadData(); }}
创建一个静态Handler内部类,然后对Handler持有的工具利用弱引用,这样在回收时也可以回收Handler持有的工具,这样虽然避免了Activity泄露,不过Looper线程的行列步队中还是可能会有待处理的,以是我们在Activity的Destroy时或者Stop时该当移除行列步队中的,
@Overrideprotected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null);}
利用mHandler.removeCallbacksAndMessages(null);是移除行列步队中所有和所有的Runnable。当然也可以利用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。
(5)Thread
和handler一样,线程也是造成内存透露的一个主要的源头。线程产生内存透露的紧张缘故原由在于线程生命周期的不可控。比如线程是 Activity 的内部类,则线程工具中保存了 Activity 的一个引用,当线程的 run 函数耗时较长没有结束时,线程工具是不会被销毁的,因此它所引用的老的 Activity 也不会被销毁,因此就涌现了内存透露的问题。
(6)系统bug
比如InputMethodManager,会持有activity而没开释,导致泄露,须要通过反射进行打断。