问题重现
AIO进行写文件使用了AsynchronousFileChannel类来实现,测试代码如下:
|
|
执行结果如下:
|
|
可见,该问题是内存溢出,不能创建新的线程。
查看原因
那么,为什么会创建这么多的线程呢?
我们先来看一下AsynchronousFileChannelImpl类的write方法:
|
|
这里调用了implWrite方法,implWrite方法是在SimpleAsynchronousFileChannelImpl类中定义的,下面来看一下SimpleAsynchronousFileChannelImpl类的implWrite方法:注意:因为我是在Mac OS上进行测试,windows下是没有SimpleAsynchronousFileChannelImpl类的
|
|
看一下第15行和第22行,这里都使用了executor来执行具体的写操作,而executor是在哪里定义的呢?
由于创建AsynchronousFileChannel对象的时候是如下代码:
|
|
AsynchronousFileChannel的open方法定义如下:
|
|
这里调用了重载的open方法,注意第三个参数为null,该参数的类型就是ExecutorService,查看该方法:
|
|
这里的provider是UnixFileSystemProvider,查看该类的newAsynchronousFileChannel方法:
|
|
调用了UnixChannelFactory的newAsynchronousFileChannel方法,该方法代码如下:
|
|
这里就用到了SimpleAsynchronousFileChannelImpl的open方法:
|
|
可以看到,这里的ExecutorService对象使用了DefaultExecutorHolder中的defaultExecutor:
|
|
再看一下ThreadPool的createDefault方法:
|
|
可以看到,这里默认创建了一个CachedThreadPool
,在newCachedThreadPool方法中使用了SynchronousQueue作为任务队列:
|
|
这里注意第二个参数,第二个参数是设置线程池最大的任务数量,有关线程池请参考之前的文章深入理解Java线程池:ThreadPoolExecutor
也就是说,这里的任务数量是没有限制的,而SynchronousQueue这个队列比较特殊,它是一个没有数据缓冲的BlockingQueue(队列只能存储一个元素),生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样,消费者移除数据操作必须等待生产者的插入。
不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。
根据我们的测试代码来看,写文件的时候会向executor中添加一个线程作为任务来执行,而这时如果磁盘的写速度太慢,而程序在不停地进行写任务的添加,这会导致队列中的对象越来越多,而队列中的对象就是Runnable对象,也就是线程对象。可以在报错信息中看到,异常是在Invoker类中:
|
|
这里执行的时候会创建一个线程对象,在调用了execute方法之后,会调用线程池中的addWorker方法添加任务:
|
|
在添加任务完成后,会调用start方法来启动线程。
所以,在磁盘写速度比较慢的时候,不停地向线程池中添加线程对象并启动线程,而且队列的大小没有限制。
但这个异常并不是堆内存的溢出,堆内存的溢出如下:
|
|
问题分析
那么,究竟为什么会报不能创建线程的异常呢?
我们先把内存按区域进行以下分类:
- MaxProcessMemory:指的是一个进程的最大内存
- JVMMemory:JVM内存
- ReservedOsMemory:保留的操作系统内存
- ThreadStackSize:线程栈的大小
在java语言里, 当你创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是JVMMemory,而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。
具体计算公式如下:
|
|
我们测一下如下代码:
|
|
该代码不停地创建线程,看下结果:
|
|
最终停在了4072,也就是创建了4073个线程后报OOM。
查看一下系统的线程数量限制:
|
|
可见,系统的线程数量限制为4096,从这个数量来说,和我们运行的结果是一致的。
所以,第一个异常Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
并不一定代表是系统内存不足导致的溢出,也可能是创建的线程数量达到了系统的限制。
解决问题
- 如果程序中有bug,导致创建大量不需要的线程或者线程没有及时回收,那么必须解决这个bug,修改参数是不能解决问题的;
如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改MaxProcessMemory,JVMMemory,ThreadStackSize这三个因素,来增加能创建的线程数:
- MaxProcessMemory 使用64位操作系统
- JVMMemory 减少JVMMemory的分配
- ThreadStackSize 减小单个线程的栈大小