什么是内存泄露
定义:当生命周期长的实例 L 不合理地持有一个生命周期短的实例 S,导致 S 实例无法被正常回收
代码例子
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;
}
}
解释
上面的代码可能会发生内存泄露
我们调用 1AppSettings.getInstance.setup() 传入一个Activity实例
当上述的 Activity 退出时,由于被 AppSettings 中属性 mAppContext 持有,进而导致内存泄露。
为什么上面的情况就会发生内存泄露
以 JAVA 为例,GC 回收对象采用GC Roots强引用可到达机制。
Activity实例被AppSettings.sInstance持有
AppSettings.sInstance由于是静态,被AppSettings类持有
AppSettings类被加载它的类加载器持有
而类加载器就是GC Roots的一种由于上述关系导致Activity实例无法被回收销毁。
ps: 内存泄漏,其实就是本来应该被回收的对象,因为被更长生命周期的对象持有,而无法回收的情况。
验证是否引起内存泄露
因此,想要证明未关闭的文件流是否导致内存泄露,需要查看文件流是否是GC Roots强引用可到达。
示例代码1(辅助验证GC 发生)
import java.io.BufferedReader;
import java.io.Reader;
class MyBufferedReader(`in`: Reader?) : BufferedReader(`in`) {
protected fun finalize() {
println("MyBufferedReader get collected")
}
}
示例代码2
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById(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")
}
}
操作步骤
这里我们这样操作
点击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
从 inode table 中查找到对应的文件节点
根据用户代码的一些参数(比如读写权限等)在 open file table 中创建 open file 节点
将上一步的 open file 节点信息保存,在 file descriptor table 中创建 file descriptor
返回上一步的 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 为例
/**
* Ensures that the close
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。
ps: 也就是说不会造成内存泄漏。实际导致的是资源泄漏,因为 finalize 回收的线程优先级非常低。
关闭流实践
手动关闭
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;
}
TWR
Java 7 之后,可以使用 try-with-resource 方式处理
String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
个人收获
知识在于存疑
对于最常见的文件不关闭会内存泄漏,99% 的程序员都知道。
但是为什么会内存泄漏,估计知道的人少之又少。
jvm
jvm 的相关命令,一直停留于表面。
没有真正的去使用。
文件系统
操作系统的文件系统,如果没有系统学习,很多东西都是不知道的。
参考资料