内存泄漏探讨上(JAVA篇)

    |     2016年3月2日   |   Android经验   |     0 条评论   |    1588

内存泄漏很直白:该释放的对象没释放,一直被某个实例握着却不再使用,GC 收不走。先把 Java 的分配和回收讲清楚,下一篇再落到 Android 工具和对策。

640

最近读了大量相关资料,打算做个总结沉淀下来,也给自己一个警示:以后 coding 时怎么避开这些坑,提高应用体验和质量。会从 Java 内存泄漏基础知识开始,再用例子说明 Android 引起泄漏的原因,以及如何用工具分析。


一、Java 内存分配策略

运行时有三种分配:静态分配、栈式分配、堆式分配,对应静态存储区(方法区)、栈区、堆区。

  • 静态存储区(方法区):静态数据、全局 static、常量。编译期就分配好,整个运行期间都在。
  • 栈区:方法执行时,方法体内局部变量在栈上创建,方法结束自动释放。分配运算在处理器指令集里,效率高,容量有限。
  • 堆区:动态分配,通常是运行时 new 出来的内存,不用时由 GC 回收。

栈与堆的区别

方法体内的局部变量(基本类型和对象引用)在栈上。超过作用域就无效,空间可被复用。

堆存放所有 new 出来的对象(含成员变量)和数组,由 GC 管理。堆上产生对象后,栈上会有一个引用变量,取值等于对象在堆里的首地址,通过它访问堆中对象。

640

Sample 的局部变量 s2 和引用 mSample2 都在栈中,mSample2 指向的对象在堆上。mSample3 指向的对象实体在堆上(包括成员 s1、mSample1),它自己在栈中。

结论:局部变量的基本类型和引用在栈,引用的对象实体在堆——它们属于方法,生命周期随方法结束。成员变量全部在堆(基本类型、引用和对象实体)——它们属于类,类对象终究要被 new 出来用。


二、Java 如何管理内存

内存管理就是对象的分配和释放。程序员用 new 申请(基本类型除外),对象都在堆上。释放由 GC 决定和执行。分配归程序、释放归 GC,简化了程序员,也加重了 JVM——这也是 Java 相对慢的原因之一。GC 必须监控每个对象的申请、引用、被引用、赋值,才能正确释放。

释放的根本原则:对象不再被引用。可以把对象看成有向图的顶点,引用关系看成有向边,从引用者指向被引对象。每个线程对象可作为起始顶点,例如从 main 开始就是一棵根树。根可达的是有效对象,GC 不回收;与根不可达的(注意是有向图)不再被引用,可以回收。

0

有向图能消掉循环引用:三个对象互相引用,只要和根进程不可达,GC 照样回收。精度高、效率较低。另一种常用技术是计数器(如 COM),精度低、难处理循环引用,但执行快。


三、什么是 Java 中的内存泄漏

Java 里的泄漏对象有两个特点:一是可达(有向图里有通路连到它);二是无用(程序以后不会再用)。满足这两条,GC 收不走,却占着内存。

C++ 里泄漏范围更大:有些对象分配了却不可达,没有 GC,内存永远回不来。Java 里不可达对象由 GC 负责,程序员不用管这部分。C++ 程序员要管边和顶点,Java 程序员只管边(不用管顶点释放)。

0 (1)

因此 Java 也有泄漏,但范围比 C++ 小:语言保证任何对象都可达,不可达的交给 GC。GC 对程序员基本透明。System.gc() 并不保证一定会跑。不同 JVM 算法不同,GC 线程优先级通常较低;有的到一定用量才工作,有的定时,有的平缓,有的中断式。除非实时系统被 GC 打断,才需要调参数(例如把回收拆成小步骤,HotSpot 支持)。

一个典型例子:

10

循环申请 Object 放进 Vector,如果只释放引用本身,Vector 仍引用该对象,GC 收不走。加入 Vector 后还必须从中删除,最简单是把 Vector 对象置为 null。

区域 放什么 谁释放
方法区 static、常量 进程结束
栈 局部基本类型和引用 方法结束自动释放
堆 new 的对象、数组成员 GC;仍被集合握着则泄漏

原文出处: Android 内存泄漏探讨

一句话总结:Java 泄漏不是不可达,而是“还够得着、却再也不用”;集合、静态引用这类边没剪断,GC 再勤也收不走。

转载请注明来源:内存泄漏探讨上(JAVA篇)
本文链接地址:https://ai.zhousir.top/?p=1267
回复 取消