PS:从我的新浪博客搬过来的文章
前段时间发现我负责的版本发布系统出现了一次OutOfMemoryError,很奇怪这个系统日常访问量不大,而且最大heap设置为1G以上,当时没 有重视,只是简单重启了应用,一切又正常了。 过了27天后,偶然发现JVM的old gen已经占用了700MB,当时系统压力很低,顿时觉得事情不妙,一定有内存泄漏了。立即用jmap导出了当时Heap的快照,用mat分析后发现确实 有明显的内存泄漏。发现导致内存占用过多的对象是TimerTask的一个子类TimeoutTask,一共有14013个实例,占用了650MB的 heap空间,这个类来自SVNKit。由于版本发布系统需要自动从SVN上checkout代码,然后编译打包,所以使用了SVNKit的 API(svnkit-1.3.5)。后来下载了源代码发现TimeoutTask应该是负责检测与SVN链接是否timeout的一个task,但很悲 催的发现它竟然会不停的把自己加入到java.util.Timer的调度队列中。下面是节选自DefaultSVNRepositoryPool.java的代码片段:
private class TimeoutTask extends TimerTask { public void run() { boolean scheduled = false; try { long currentTime = System.currentTimeMillis(); synchronized (myInactiveRepositories) { for (Iterator repositories = myInactiveRepositories.keySet().iterator(); repositories.hasNext();) { SVNRepository repos = (SVNRepository) repositories.next(); long time = ((Long) myInactiveRepositories.get(repos)).longValue(); if (currentTime - time >= getTimeout()) { repositories.remove(); repos.closeSession(); } } if (myTimer != null) { myTimer.schedule(new TimeoutTask(), 10000); scheduled = true; } } } catch (Throwable th) { SVNDebugLog.getDefaultLog().logSevere(SVNLogType.WC, th); if (!scheduled && myTimer != null) { myTimer.schedule(new TimeoutTask(), 10000); scheduled = true; } } } }
SVNKit为什么要这么设计呢?实际上是为了保持客户端与SVN服务器的链接打开,每10秒一次检测。貌似没有问题,但我们创建SVN客户端时用的是下面的方法:
SVNClientManager client = SVNClientManager.newInstance();
这个模式会每次都创建一个TimeoutTask实例,而TimeoutTask的执行结束前每次又都会创建一个TimeoutTask实例,10 秒后新创建的TimeoutTask会再次执行相同的动作。也就是说我创建过多少SVNClientManager实例就会有多少个 TimeoutTask实例存在,而且永远不会被垃圾回收掉,因为这些TimeoutTask都被加入到了一个全局的 Timer(DefaultSVNRepositoryPool中定义的一个私有静态Timer)对象的调度队列中,从而导致JVM的Old Gen会缓慢增长。
有两种解决方法,一个是SVNClientManager只创建一次;另一个是SVNClientManager使用完了以后马上执行dispose方法就会释放掉那些TimeoutTask。
从这个问题我也有一些自己的感受:
1.第三方API有的虽然功能很强大,但在使用时务必要仔细参考其说明文档,甚至是源代码,否则出了问题会比较棘手。
2.遇到一个问题一定要有解决到底的精神,如果只是一知半解不想把问题搞清楚,那么你永远只能知道些皮毛。我感觉现在遇到的很多刚毕业参加工作的程序员, 总是把精力放在学习如何使用各种开源框架上,但对这些开源框架的原理和基本实现却并不关心,遇到问题就知道上baidu搜一下现成的答案,而不是去看官方 的文档,其实有些问题各种开源框架的文档写的非常清楚,并且上面会告诉你框架的开发者是怎么做的和这样做的原理。作为合格的程序员,你所关心的不应当是如何使用那些外表华丽的框架,你应当能了解他们的实现原理,并从原理中学习涉及的基础知识,比如HTTP协议,Socket通讯,J2EE的Servlet标准,多线程技术等,知道这些基础知识远比知道怎么简单使用一个框架要有用得多。