前言
使用Java操作过文件的话,对FileNotFoundException肯定是不陌生的,碰到这个问题,我们第一反应就是给定的path文件不存在。但在某种极端条件下
,造成这个问题的并不是文件不存在,而是程序运行时的资源使用不当导致(已看破的同学可以直接拖到结尾了)。
问题现象
在我的程序中有个场景是反编译apk并分析所有bundle的文件字节流,其中有大量的读文件操作,当同时解析多个apk文件的时候,就出现了FileNotFoundException,我们先来看下异常堆栈(已忽略非关键信息):
java.io.FileNotFoundException: /tmp/appOnDiet/..../WindVaneSDK$2.smali (Too many open files in system)
at java.io.FileInputStream.open0(Native Method)
...
at com.tmall.wireless.ycombinator.biz.aod.resourcegc.ResourceFileCollector.isRSmaili(ResourceFileCollector.java:73)
...
WARNING: RMI TCP Accept-1099: accept loop for ServerSocket[addr=0.0.0.0/0.0.0.0,localport=1099] throws
java.net.SocketException: Too many open files in system
at java.net.PlainSocketImpl.socketAccept(Native Method)
...
看到上面的堆栈,第一反应就是check一下/tmp/appOnDiet/…./WindVaneSDK$2.smali,然而文件存在!但是且慢,我们再看一眼日志会发现不仅文件打开失败,Socket流也失败,而且都指向Too many open files in system
,这将是解决整个问题的突破口。
问题排查
初步定位
在ata搜索了一下Too many open files in system
,并没有找到类似的解决方案(这篇文章有提到,但没后续)。在google中找思路的时候,发现linux在内核层面会限制文件
的打开数量,这里文件不仅涵盖普通文件和目录,还包括了socket等网络流。查看当前系统的配置,可以使用sysctl和ulimit查看,比如在我的系统中(Mac PRO, iOS:10.10.5)可以拿到数据如下:
$ sysctl -a | grep maxfiles
kern.maxfiles: 12288
kern.maxfilesperproc: 10240
$ ulimit -n
256
其中sysctl查询到的是内核级别的File Descriptors
(暂且叫他文件句柄吧)限制,默认配置下我的系统支持最大12288个文件句柄,而单个进程在内核级别限制在10240个。ulimit限制的是用户通过shell命令启动的进程文件句柄数目,可参考ulimit manual:ulimit provides control over the resources available to the shell and to processes started by it, on systems that allow such control.
结合“Too many open files in system”,问题可以初步定位在程序打开的文件过多,导致文件句柄数目超出了内核限制,不过具体是超越了哪一个限制?根本的触发原因是什么?初步怀疑是超越了“ulimit -n” 和 “kern.maxfilesperproc”,因为这两个值比较小,但这还需要结合后面的实验来进行分析。
实验分析
假设是超越了ulimit限制,那么写一个循环连续打开文件257次,应该是要复现too many open files这个问题的,写一个很简单的测试方法,故意创建BufferedReader但不关闭,如下: