在使用多线程分批写入文件时,发生如下异常:
java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at com.ds.ProductRawDataTask.run(ProductRawDataTask.java:128)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.text.DateFormatSymbols.getShortMonths(DateFormatSymbols.java:433)
at java.util.Calendar.getFieldStrings(Calendar.java:2241)
at java.util.Calendar.getDisplayName(Calendar.java:2111)
at java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1125)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:966)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:936)
at java.text.DateFormat.format(DateFormat.java:345)
at ...
使用的多线程代码如下:
try{
List<Future> futures = new ArrayList<Future>();
//通过线程池写出多个渠道的数据文件到磁盘
futures.add(executorService.submit(new ProductWeiboRawDataExportTask()));
futures.add(executorService.submit(new ProductForumRawDataExportTask()));
futures.add(executorService.submit(new ProductTiebaRawDataExportTask()));
futures.add(executorService.submit(new ProductNewsRawDataExportTask()));
futures.add(executorService.submit(new ProductShortVideoRawDataExportTask()));
executorService.shutdown();
//阻塞等待所有线程完成
for(Future f: futures) { f.get(); } //ProductRawDataTask.java:128
//将所有渠道的数据文件打包发送到远程ftp服务器
sendFilesToFTP();
}catch(Exception e){
log.error(e.getMessage(),e);
}
也就是说,遇到的问题是:在使用线程池执行任务,并且通过Future阻塞等待所有线程执行完成时,因为某个线程抛出了OutOfMemoryError,导致将文件打包发送到远程ftp服务器的操作没有执行。真实原因是因为get()方法抛出了ExecutionException异常,导致跳过了sendFilesToFTP()方法,进入了异常捕获逻辑当中。
首先,让我们理解以下知识点:
1、java.lang.OutOfMemoryError: GC overhead limit exceeded
Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
3. 怎么捕获OutOfMemoryError
try {
.....
} catch (Exception e) {
e.printStackTrace();
}
无法捕获OutOfMemoryError
try {
.....
} catch (Throwable t) {
.....
}
只有用catch (Throwable t)捕获OutOfMemoryError
因为,它们的继承关系如下:
4、FutureTask#run()方法会捕获Throwable,并将Throwable设置为内部的outcome变量
public void run() {
.........
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
//捕获Throwable
} catch (Throwable ex) {
result = null;
ran = false;
//将Throwable设置为内部的outcome成员变量
setException(ex);
}
if (ran)
set(result);
}
} finally {
.........
}
}
5、FutureTask#get()方法会将捕获到的Throwable封装为ExecutionException,并抛出该异常
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
/**
* Returns result or throws exception for completed task.
*
* @param s completed state value
*/
@SuppressWarnings("unchecked")
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
//如果outcome成员变量为Throwable类型,将Throwable封装为ExecutionException,并抛出该异常
throw new ExecutionException((Throwable)x);
}
最终,我们会想到相关解决方法如下:
1、如果可以忽略抛出oom的线程写出的磁盘文件,只要求将其他线程成功写出的文件打包发送到ftp。对FutureTask#get()方法加入try/catch异常捕获块即可。
executorService.shutdown();
//阻塞等待所有线程完成
for(Future f: futures) {
try{
f.get();
}catch(Exception e){
.....
}
}
2、增大堆内存,避免oom