系统正常运行一段时间后,QA报给我一个异常:
java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.text.DecimalFormatSymbols.clone(Unknown Source)
at java.text.DecimalFormat.clone(Unknown Source)
at java.text.SimpleDateFormat.initialize(Unknown Source)
at java.text.SimpleDateFormat.<init>(Unknown Source)
at java.text.SimpleDateFormat.<init>(Unknown Source)
at com.thomsonreuters.ce.exportjobcontroller.task.file.iiroutage.entity.AnalyticTask.<init>(AnalyticTask.java:18)
参考文档,此类异常是由于:
"if too much time is being spent in garbage collection: if more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, an OutOfMemoryError will be thrown."
异常是在测试人员做压力测试的时候抛出,直接原因是大量业务实体对象AnalyticTask的创建(>1w)导致了GC overhead limit exceeded。经分析,初步判定是业务实体类中定义的SimpleDateFormat成员导致。
首先,了解一下这个类的两个特性:
1. SimpleDateFormat 不是线程安全的;
2. 创建它的实例将会耗费很大的代价。
以上两点都是由它的一个成员属性造成的。它继承了DateFormat,在DateFormat中定义了一个protected属性的Calendar类的对象:calendar。而Calendar 是一个非常复杂的类,因为涉及到时区、本地化以及闰年等等因素。在本例中,由于在业务实体类中定义了SimpleDateFormat类型的属性,每一个业务对象的创建,都会创建一个SimpleDateFormat实例对象,大量的实例对象占用了大量的jvm空间。由于它的复杂性,GC的工作量比较大,效率很低,从而导致问题的发生。
进一步分析线程安全的问题:
JDK原始文档如下:
Synchronization:
Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally
format方法实现:
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
calendar.setTime(date)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。
中断来了。
线程2开始执行,它也改变了calendar。
又中断了。
线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
分析一下format的实现,我们不难发现,用到成员变量calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。
这也同时提醒我们在开发和设计系统的时候注意下一下三点:
1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
2.对线程环境下,对每一个共享的可变变量都要注意其线程安全性
3.我们的类和方法在做设计的时候,要尽量设计成无状态的
解决方案:参考 http://www.cnblogs.com/peida/archive/2013/05/31/3070790.html
本列采用ThreadLocal变量来解决问题。