191206_01 Java中的句柄与资源泄露

Java中的句柄与资源泄露

作者:邵发
官网:http://afanihao.cn/java

本文内容介绍Java中的句柄与资源泄露问题,是Java网站开发中必须清楚掌握的基本概念。句柄包括两类:文件句柄和网络句柄。本文是Java学习指南系列教程的官方配套文档,配套示例代码或者视频讲解。

在项目开发中,句柄的概念极为重要。如果不了解句柄,就可能发生以下错误:

  • 文件被占用,无法删除或移动
  • Tomcat资源占满,无法访问
  • MySQL数据库连接数已满,无法连接

 

1.  文件句柄

句柄 ( Handle ) ,是一种系统资源。在实际编程中分为两类:文件句柄,网络句柄(Socket)。

先说文件句柄。当在程序里打开一个文件的时候,操作系统内核里创建了一个文件句柄,该句柄为当前进程拥有。比如,
     InputStream inputStream = new FileInputStream(“C:\test\example.txt”);
此时就打开了一个文件句柄。文件句柄在用完之后一定要执行close 关闭,在close的时候会释放系统资源。即使用以下的代码框架:

InputStream inputStream = new FileInputSteam( file );
try{
   ... 读取文件 ...
}
finally{
    inputStream.close();
}

无论在try{}中有无异常发生,总是在finally{}中执行close()方法,确保文件句柄的释放。

涉及到文件句柄的地方有:

// 读取一个文件
InputStream inputStream = new FileInputStream( file )
// 写入一个文件
OutputStream outputStream = new OutputStream (file)
// 随机读写文件 ( 不常见 )
RandomAccessFile  raf = new RandomAccessFile (file, "rw")

也就是说,创建一个InputStream / OutputStream / RandomAccessFile 对象时,各自对应了一个句柄。所有带句柄的对象,在用完之后要记得关闭掉。

需要区分的是,File对象只是一个文件路径,相当于String的包装类,它是不对应文件句柄的。例如,File f = new File(“C:\test\example.txt”) 这只是定义了一个路径,什么事都没有发生,不会创建文件句柄。

 

2.  网络句柄

当在项目中使用网络编程技术时,也需引起我们的注意。一个Socket对应一个句柄,可以称为网络句柄。网络句柄和文件句柄一样,都是有限的资源,也是用完了就必须关闭。

涉及网络句柄的地方有:

2.1 Socket 和 ServerSocket

在TCP网络编程中的Socket和ServerSocket代表了网络句柄,以下示例详见Java学习指南系列的《网络通信篇》视频教程。

// 服务器:创建一个ServerSocket的时候,句柄数加1
ServerSocket  serverSock = new ServerSocket(2019);
// 服务器:接收到一个客户端连接的时候,句柄数加1
Socket sock = serverSock.accept();
// 客户端:连接上服务器的时候,句柄数加1
Socket sock = new Socket();
sock.connect( new InetSocketAddress("127.0.0.1",2019));

也就是说,一个Socket / ServerSocket 对象各自对应了一个句柄。

2.2 JDBC Connection

在数据库JDBC编程中,需要获取一个Connection对象,代表一个与MySQL服务器之间的一个链接。例如,

Connection conn = DriverManager.getConnection(connectionUrl, username, password);
try{
   ...
} finally {
   conn.close();
}

这个Connection对象内部会包含一个Socket连接,所以它也对应一个句柄,在用完后应当及时关闭。

 

2.3 HttpServletRequest和HttpServletResponse

在Java网站开发时,HttpServletRequest和HttpServletResponse内部包含了网络Socket连接,但是这个Socket是被Tomcat框架自己管理的,我们看不到也管不着。

比如,在处理Servlet请求时,

OutputStream outputStream = response.getOutputStream();
outputStream.write( data );

可以脑补一下,从HttpServletResponse获取OutputStream,其内部实现不就是从Socket中获取OutputStream吗?

Tomcat本身是一个Java写的TCP服务器,当一个客户端请求到来时,它会创建一个Socket并创建一个线程来处理。所以在实际运行时,一个请求对应一个线程和一个Socket资源的。再强调一遍,这个Socket是由Tomcat自行管理的,我们看不到也管不着。

 

3.  句柄数量的监测

一个进程可创建的句柄是有限的,以前的Windows或Linux上最多可以创建约2000多个。但是最新的Win10系统似乎突破了这个限制。

可以自己试一下,

public static void main(String[] args) throws Exception
{
	File file = new File("C:/test/example.txt");
	byte[] buf = new byte[4000];
	for(int i=1; i<10000; i++)
	{
		InputStream is = new FileInputStream(file);
		is.read(buf);
		System.out.println("创建第" + i + "个文件句柄");
	}
	System.out.println("Exit.");
}

在任务管理器里,可以观察到一个进程所占用的系统资源,如CPU、内存、线程数、句柄数。其中,线程和句柄两列默认是隐藏的,必须打开这两列的显示。按 CTRL + SHIFT + ESC,打开任务管理器,如下图所示。

191206_01 Java中的句柄与资源泄露_第1张图片

如果没有显示句柄这一列的话,可以右键点一下CPU这个标签,选择列即可显示(自己百度一下)。

在这里可以看到每一个进程所占用的资源情况,我们的Java进程的名称是java.exe或者javaw.exe。比如,Eclipse启动时对应一个javaw.exe进程。

以单步调试的方式运行上述代码,可以发现每创建一个FileInputStream对象,该进程的句柄数会加1。如果调用了close()方法,则句柄数会减1。

4.  句柄与GC垃圾回收

Java里有一个GC垃圾回收机制,可以把失去引用的对象自动回收。比如,

for(int i=1; i<10000; i++)
{
	InputStream inputStream = new FileInputStream(file);
	inputStream.read(buf);
	System.out.println("创建第" + i + "个文件句柄");
}

此处,每一轮循环创建一个对象inputStream,此对象在循环之后失去引用,会被自动回收(销毁)。但是需要区分的是,此处销毁的是对象自身,并没有释放句柄啊!

在Java里,一定要显式地调用inputStream.close()才能够释放这个inputStream所对应的句柄资源。句柄释放和GC机制是两码事!

 

5.  try-with-resources

在有的代码里,形式上看不到close()的调用,但是句柄在内部被关闭了。比如,在MyBatis编程中经常可见的代码:

try ( SqlSession session = sqlSessionFactory.openSession() )  {
  // do work

}

在这里,并没有看到 session.close() 的调用,为什么呢?

此种try语法称为 try-with-resources,在小括号里是一个称为资源的对象。资源对象类型,就是实现了java.lang. AutoCloseable接口的类型。语法规定,在try{} 运行完后会自动调用resource.close()方法来关闭资源。可以认为这种语法是对try { } finally{} 的简写,相当于:

SqlSession session = sqlSessionFactory.openSession();
try {
// do work

}
finally {
	session.close();
}

记住,此种try-with-resources语法,是要求资源对象类型是实现了AutoCloseable接口的。你得自己检查一下,有这个接口的类型才能这么简写。

 

6.  线程与句柄

线程数和句柄数,都是有限的资源。一个程序不能开太多的线程,也不能开太多的句柄。

在网站开发中,这两个概念是相关的。表现在:
(1) 当一个请求到来时,Tomcat创建一个线程,在线程里处理这个请求Socket。所以一个请求对应了一个线程和一个Socket句柄
(2) 无论是线程占满、还是句柄占满,都会导致系统不可访问。

比如,如果句柄占满,则Tomcat无法创建新的Socket连接,自然也就不能访问了。

 

7.  句柄与服务器的稳定性

在网站开发中,通过观察任务管理器中的句柄数,可以检查有没有句柄泄露的情况。

如果句柄数保持稳定,则没有问题。如果持续增加、不见减少,就说明你的程序代码里有句柄泄露。如果已经达到上限(2000-3000),则服务器已经不可访问。

所谓句柄泄露,就是说你打开一个句柄但是忘了close掉。比如,
- 打开了一个FileInputStream / FileOutputStream / RandomAccessFile 对象
- 打开了一个数据库连接 Connection 对象 ( 或者封装后的 SqlSession 等)
- 打开了一个Socket连接对象
- 使用HttpClient / FTP Client等封装后的API,打开了一个网络连接但没有close掉 ...

凡是涉及到这些调用的地方,一定要仔细核对,因为这关系到你的服务器是否能长期稳定地运行。

 

最后,本文演示所用的项目源码及视频讲解 在此处获取 ,更多教程请注意 阿发你好 的Java学习指南系列教程。

你可能感兴趣的:(Java)