专栏首页技术小黑屋未关闭的文件流会引起内存泄露么?

未关闭的文件流会引起内存泄露么?

最近接触了一些面试者,在面试过程中有涉及到内存泄露的问题,其中有不少人回答说,如果文件打开后,没有关闭会导致内存泄露。当被继续追问,为什么会导致内存泄露时,大部分人都没有回答出来。

本文将具体讲一讲 文件(流)未关闭与内存泄露的关系。

什么是内存泄露

  • 定义:当生命周期长的实例L 不合理地持有一个生命周期短的实例S,导致S实例无法被正常回收

举例说明

1 2 3 4 5 6 7 8 9 10 11 12 13

public class AppSettings { private Context mAppContext; private static AppSettings sInstance = new AppSettings(); //some other codes public static AppSettings getInstance() { return sInstance; } public final void setup(Context context) { mAppContext = context; } }

上面的代码可能会发生内存泄露

  • 我们调用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 发生)

1 2 3 4 5 6 7 8 9 10 11

package com.example.streamleakssample import java.io.BufferedReader import java.io.Reader class MyBufferedReader(`in`: Reader?) : BufferedReader(`in`) { protected fun finalize() { println("MyBufferedReader get collected") } }

示例代码2

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

package com.example.streamleakssample import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.util.Log import android.view.View import java.io.FileInputStream import java.io.InputStreamReader class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<View>(R.id.textview).setOnClickListener { testInputStream() } } private fun testInputStream() { //需进入设置手动开启应用权限,未处理运行时权限问题 val `is` = FileInputStream("/sdcard/a.txt") val buf = MyBufferedReader(InputStreamReader(`is`)) var line = buf.readLine() val sb = StringBuilder() while (line != null) { sb.append(line).append("\n") line = buf.readLine() } val fileAsString = sb.toString() Log.i("MainActivity", "testInputStream.Contents : $fileAsString") } }

这里我们这样操作

  1. 点击textview视图,触发多次testInputStream
  2. 过几秒后,我们执行heap dump
  3. 我们使用 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为例

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

/** * Ensures that the <code>close</code> method of this file input stream is * called when there are no more references to it. * * @exception IOException if an I/O error occurs. * @see java.io.FileInputStream#close() */ protected void finalize() throws IOException { // Android-added: CloseGuard support. if (guard != null) { guard.warnIfOpen(); } if ((fd != null) && (fd != FileDescriptor.in)) { // Android-removed: Obsoleted comment about shared FileDescriptor handling. close(); } }

是的,在finalize方法中有调用close来释放file descriptor.

但是finalize方法执行速度不确定,不可靠

所以,我们不能依赖于这种形式,还是要手动调用close来释放file descriptor。

关闭流实践

Java 7 之后,可以使用try-with-resource方式处理

1 2 3 4 5 6

static String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }

Kotlin 可以使用use

1 2 3

private fun readFirstLine(): String { BufferedReader(FileReader("test.file")).use { return it.readLine() } }

当然,还有最基础的手动关闭的形式

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

private String readFirstLine() throws FileNotFoundException { BufferedReader reader = new BufferedReader(new FileReader("test.file")); try { return reader.readLine(); } catch (IOException e) { e.printStackTrace(); } finally { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } return null; }

Reference

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 用好 Require,check,assert,写好 Kotlin 代码

    在编码的时候,我们需要做很多的检测判断,比如某个变量是否为null,某个成员属性是否为true,执行某个操作结果是否成功。比如像下面的这段代码

    技术小黑屋
  • 解读文件描述符

    最近由于机缘巧合,结合最近工作中遇到的一些问题,深入了解了文件描述符(File Descriptor,简称FD,以下使用 FD 称谓)。预计会有两到三篇关于 F...

    技术小黑屋
  • 实时预览Markdown利器

    使用MBP15寸有点高不成低不就,接显示器分辨率下降(浪费Retina屏),不接显示器屏幕不是很大。

    技术小黑屋
  • 前端-part11-Bootstrap初识

    2.Bookstrap...复制粘贴...复制粘贴...link导入bookstrap.css,分分列,然后往里面贴

    少年包青菜
  • “汇新杯”科技+社会公益专项赛介绍

    (1)以新兴科技作为主要课题,围绕教育、卫生、体育、生态环境、扶贫济困、社区发展、慈善金融、社会支持等公益慈善和社会价值领域的项目;

    创成汇创新创业大赛
  • “汇新杯”青年创客专项赛介绍

    (2)智慧智能领域:智慧公安、智慧生活、智慧城市、智慧办公、智慧交通、智慧医疗、无人驾驶、智能制造、智能装备;

    创成汇创新创业大赛
  • 从源代码级别看懂MinIO对象存储网关的实现

    我在 《MinIO对象存储的网关架构设计》一文中介绍了MinIO的网关架构设计,它的整体架构如下图所示:

    飞雪无情
  • SAP ABAP实用技巧介绍系列之Debug XSLT transformation

    当前执行到的code会用不同颜色在右边标识出来。每一步执行之后再Result tab处能看到最新的html source code:

    Jerry Wang
  • Sqoop快速入门系列(3) | Sqoop常用命令及参数解析(建议收藏!!!)

      这里给大家列出来了一部分Sqoop操作时的常用参数,以供参考,需要深入学习的可以参看对应类的源代码。

    不温卜火
  • 外文文献翻译,秒否?文献阅读,知否?

    打个比方,我们常说的SCI论文,其实就是发表在被SCI数据库收录的杂志期刊上的论文,那么,这就有两个主要的基本事件:

    百味科研芝士

扫码关注云+社区

领取腾讯云代金券