专注于Java领域优质技术,欢迎关注
来自:技术小黑屋
最近接触了一些面试者,在面试过程中有涉及到内存泄露的问题,其中有不少人回答说,如果档案开启后,没有关闭会导致内存泄露。当被继续追问,为什么会导致内存泄露时,大部分人都没有回答出来。
本文将具体讲一讲 档案(流)未关闭与内存泄露的关系。
什么是内存泄露
定义:当生命周期长的例项L 不合理地持有一个生命周期短的例项S,导致S例项无法被正常回收举例说明上面的程式码可能会发生内存泄露
我们呼叫AppSettings.getInstance.setup()传入一个Activity例项当上述的Activity退出时,由于被AppSettings中属性mAppContext持有,进而导致内存泄露。为什么上面的情况就会发生内存泄露
以 Android 为例,GC 回收物件采用GC Roots强引用可到达机制。Activity例项被AppSettings.sInstance持有AppSettings.sInstance由于是静态,被AppSettings类持有AppSettings类被载入它的类载入器持有而类载入器就是GC Roots的一种由于上述关系导致Activity例项无法被回收销毁。验证是否引起内存泄露
因此,想要证明未关闭的档案流是否导致内存泄露,需要检视档案流是否是GC Roots强引用可到达。
示例程式码1(辅助验证GC 发生)
示例程式码2
这里我们这样操作
点选textview检视,触发多次testInputStream过几秒后,我们执行heap dump。我们使用 MAT 对上一步的dump档案进行分析(需进行格式转换)
分析上图,我们发现
FileInputStream 只被 FinalizerReference 这个类(GC Root)持有上述持有的原因是,FileInputStream重写了finalize,会被加入到FinalizerReference的析构处理集合上述引用会随着Finalizer守护执行绪处理后解除,即FileInputStream例项彻底销毁。所以,我们再来操作一波,验证上面的结论。
然后利用工具执行强制GC回收过几秒后,我们执行heap dump。我们使用 MAT 对上一步的dump档案进行分析(需进行格式转换)堆分析档案,查询MyBufferedReader或者FileInputStream或者InputStreamReader 没有发现这些例项,说明已经GC回收出于谨慎考虑,我们按照包名查询java.io在排除无关例项外,依旧无法找到testInputStream中的例项。再次证明已经被GC回收因而我们可以确定,正常的使用流,不会导致内存泄露的产生。
当然,如果你刻意显式持有Stream例项,那就另当别论了。
为什么需要关闭流
首先我们看一张图
如上图从左至右有三张表
file descriptor table 归属于单个程序global file table(又称open file table) 归属于系统全域性inode table 归属于系统全域性从一次档案开启说起
当我们尝试开启档案/path/myfile.txt
1.从inode table 中查询到对应的档案节点
2.根据使用者程式码的一些引数(比如读写许可权等)在open file table 中建立open file 节点
3.将上一步的open file节点资讯储存,在file descriptor table中建立 file descriptor
4.返回上一步的file descriptor的索引位置,供应用读写等使用。
file descriptor 和流有什么关系
当我们这样FileInputStream(/sdcard/a.txt) 会获取一个file descriptor。出于稳定系统性能和避免因为过多开启档案导致CPU和RAM占用居高的考虑,每个程序都会有可用的file descriptor 限制。所以如果不释放file descriptor,会导致应用后续依赖file descriptor的行为(socket连线,读写档案等)无法进行,甚至是导致程序崩溃。当我们呼叫FileInputStream.close后,会释放掉这个file descriptor。因此到这里我们可以说,不关闭流不是内存泄露问题,是资源泄露问题(file descriptor 属于资源)。
不手动关闭会怎样
不手动关闭的真的会发生上面的问题么? 其实也不完全是。
因为对于这些流的处理,源代码中通常会做一个兜底处理。以FileInputStream为例
是的,在finalize方法中有呼叫close来释放file descriptor.
但是finalize方法执行速度不确定,不可靠
所以,我们不能依赖于这种形式,还是要手动呼叫close来释放file descriptor。
关闭流实践
Java 7 之后,可以使用try-with-resource方式处理
Kotlin 可以使用use
当然,还有最基础的手动关闭的形式
Reference
https://stackoverflow.com/questions/26541513/why-is-it-good-to-close-an-inputstreamhttps://www.reddit.com/r/learnjava/comments/577769/why_do_you_need_to_close_streams/来自:https://droidyue.com/blog/2019/06/09/will-unclosed-stream-objects-cause-memory-leaks/