值得一提:在java虚拟机规范中,此处定义了一个异常
(1)OutOfMemoryError
- 运行时常量池:属于“方法区”的一部分,用于存放编译器生成的各种字面量和符号引用。
字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
符号引用:编译语言层面的概念,包括以下3类:
(1) 类和接口的全限定名
(2)字段的名称和描述符
(3)方法的名称和描述符
2、JAVA回收机制
java中是通过GC(Garbage Collection)来进行回收内存,那jvm是如何确定一个对象能否被回收的呢?这里就需讲到其回收使用的算法
(1) 引用计数算法
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
优点:
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。例如下面代码片段中,最后的Object实例已经不在我们的代码可控范围内,但其引用仍为1,此时内存便产生泄漏。
/举个例子/
Object o1 = new Object() //Object的引用+1,此时计数器为1
Object o2;
o2.o = o1; //Object的引用+1,此时计数器为2
o2 = null;
o1 = null; //Object的引用-1,此时计数器为1
(2) 可达性分析算法
可达性分析算法是现在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来介绍,稍安勿躁。
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);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/1eecf902a52f7d826cdd0e507ff31423.jpeg)
要如何成为Android架构师?
搭建自己的知识框架,全面提升自己的技术体系,并且往底层源码方向深入钻研。
大多数技术人喜欢用思维脑图来构建自己的知识体系,一目了然。这里给大家分享一份大厂主流的Android架构师技术体系,可以用来搭建自己的知识框架,或者查漏补缺;
对应这份技术大纲,我也整理了一套Android高级架构师完整系列的视频教程,主要针对3-5年Android开发经验以上,需要往高级架构师层次学习提升的同学,希望能帮你突破瓶颈,跳槽进大厂;
最后我必须强调几点:
1.搭建知识框架可不是说你整理好要学习的知识顺序,然后看一遍理解了能复制粘贴就够了,大多都是需要你自己读懂源码和原理,能自己手写出来的。
2.学习的时候你一定要多看多练几遍,把知识才吃透,还要记笔记,这些很重要! 最后你达到什么水平取决你消化了多少知识
3.最终你的知识框架应该是一个完善的,兼顾广度和深度的技术体系。然后经过多次项目实战积累经验,你才能达到高级架构师的层次。
你只需要按照在这个大的框架去填充自己,年薪40W一定不是终点,技术无止境
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
g-UXuapbPo-1712861660719)]
对应这份技术大纲,我也整理了一套Android高级架构师完整系列的视频教程,主要针对3-5年Android开发经验以上,需要往高级架构师层次学习提升的同学,希望能帮你突破瓶颈,跳槽进大厂;
最后我必须强调几点:
1.搭建知识框架可不是说你整理好要学习的知识顺序,然后看一遍理解了能复制粘贴就够了,大多都是需要你自己读懂源码和原理,能自己手写出来的。
2.学习的时候你一定要多看多练几遍,把知识才吃透,还要记笔记,这些很重要! 最后你达到什么水平取决你消化了多少知识
3.最终你的知识框架应该是一个完善的,兼顾广度和深度的技术体系。然后经过多次项目实战积累经验,你才能达到高级架构师的层次。
你只需要按照在这个大的框架去填充自己,年薪40W一定不是终点,技术无止境
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!