“无论从网络Socket还是从本地文件系统读取二进制数据,程序都是以流的形式操作这些二进制数据。Java提供了两个超类InputStream、OutputStream提供了操作支持。我们在编码时用的最多的是这两个超类的派生类,如FileInputStream、DataInputStream等子类。当然,在操作完后我们还必须在程序中手动的调用.close( ) 方法关闭流,释放资源。那到底释放了什么资源?如果不调用close方法为什么会造成内存泄漏
”
将文件读进内存的步骤
我们以一个例子来说明
FileInputStream fileInputStream = new FileInputStream("/1.png");
Java程序中执行完上面一句代码后,Jvm就通过系统调用打开'1.png'这个文件(一张图片)。此时在操作系统内部维护了以下一段流程:
先从磁盘这个硬件开始讲起,机械磁盘最核心的三大件:柱面、磁头、扇区。只要确定了这三大件,那么文件数据就能够在磁盘上存取。为了更好的对磁盘进行操作,操作系统将这三大组件做了一层封装,这层封装叫做数据块(Block),这样我们读写文件只要操作Block就行,而Block与三大机械组件的映射关系由该文件的FCB(File Control Block)维护,即只要提供Block号,从FCB中就能找到三大组件的参数并操作磁盘。这就是FCB的作用,当打开一个文件时,会将该文件的FCB加载进v-node(virtual node)表中,v-node表也可以称为inode表即index node。
磁盘一个扇区的大小为512个byte,一个Block对应多个扇区,根据操作系统的不同,Block对应的扇区个数也不同,比如我手头上CentOS7系统中的Block就对应了8个扇区。运行下面命令查看sda1磁盘挂载到的文件系统中Block大小:
xfs_growfs /dev/sda1
有了Block后,操作系统为了上层应用更加舒服的操作文件又对Block进行了再次封装,这就是打开文件表。有了打开文件表后,我们就能根据文件字节流算出对应的Block。被程序open的文件相关的信息都会形成文件表存在于内核的打开文件表。有了打开文件表、FCB,应用程序就能很方便的对磁盘进行读写了。
这时在JVM中只需要有一个指针,指向打开的文件表,JVM就能操作文件。这个指针也就是文件描述符。文件描述符存在于JVM对应的PCB描述符表中。比如上图描述符表中fb1就是一个文件描述符,fb1指向了‘1.png’这张图片。
以上就是new FileInputStream("/1.png")
JVM操作1.png文件的过程。当然,读取文件都是先把数据读进内核态内存,然后拷贝到用户态内存,这里就不展开了,这两点内存空间的占用也就是该文件流内存泄漏的事发地。
通过测试案例深入理解
通过以上说明,站在JVM的角度,如果IO流不调用close方法,那么文件描述符表中永远会存在指向‘1.png’的fd1。这样就导致了一直有指针指向该文件的文件表,从而使得占用的内存无法得到释放,而且该文件还无法被删除,除非等到整个JVM停止
import java.io.FileInputStream;
public class Test01 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("/EthanHe/test/1.png");
// fileInputStream.close();
Thread.sleep(100000000);
}
}
运行上面把close方法注释掉的程序,运行一下命令查看JVM的文件描述符表
[root@localhost dev]# jps
2178 Test01
2190 Jps
[root@localhost dev]# lsof -p 2178
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 2178 root cwd DIR 253,0 151 55524895 /EthanHe/test
java 2178 root 5r REG 253,0 14543 55524908 /EthanHe/test/1.png
其中可以看到lsof输出的第二行中fd为5r的文件就是1.png
把close方法放开后再执行一遍
[root@localhost dev]# jps
2241 Jps
2229 Test01
[root@localhost dev]# lsof -p 2229
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 2229 root cwd DIR 253,0 151 55524895 /EthanHe/test
java 2229 root rtd DIR 253,0 268 64 /
可以发现1.png文件描述符从PCB中删除了
看到这里有人就可能怀疑是不是被GC回收掉了?如果怀疑的话那么我们可以显示的调用GCSystem.gc()
然后再运行上面的命令查看描述符,发现依然会存在‘1.png’这个文件的描述符。
总结
我们在编写程序的时候,对系统资源的调用一定要小心,因为JVM属于应用程序,它的权限相对来说还是比较低。如果对系统资源没有正确使用,那么可能会引发系统的崩溃等原因。好比本篇中对文件资源的调用,如果没有关闭系统资源,那么系统资源会被JVM这个程序一直占用,最后导致内存泄漏。