1、这家公司有一个数据库代理程序,用于数据库服务器的代理,游戏服务器执行sql指令,DBAgent接受此指令,执行一些组织后,调用JDBC执行数据库操作,然后将结果返回。
2、发生的问题:内存一直升高,处理客户端请求的线程并不多(高峰期大概300左右吧),数据库上的连接数也不多(100的样子)。运行5-6天,基本上内存就用完了,而且得不到数据库的连接。他们非常急,我就试着接下这个项目。
3、接到这个优化项目,查看了他们的部分代码。发现连接池写得有些问题,得不到数据库连接后wait,但是不会接到任何有效的notify,也就是说只要一等待就会超时。还有其他的问题,一开始我以为是这个问题,修改后让他们去跑,结果还是一样,内存上去后一直下不来,最后崩溃。
4、通过这个发现就是内存泄露了。一开始用jprofiler测试,发现内存上去后就Out of memory了,而且hashmap和long[]的对象非常多一直下不去。但是找不到这些对象是怎么产生的。折磨了我好几天,还请教了很多人,都得不到答案。后来发现是java启动参数中内存参数设置的太低了,本来需要100多M的内存,你就设置给16M,不崩溃才怪呢。于是改成了128M。结果内存上去之后,到达一个峰值就下来了,然后震荡,但一直就没有上去。那程序没有泄露?可是生产上怎么泄露了呢?
5、就在我基本上要放弃的时候,我想到了测试环境可能真实环境不同,有必要看一下他们生产服务器上虚拟机的运行情况。记得Java有自带的工具查询java虚拟机运行情况的。jmap这个工具可以查看jvm中运行实例的个数以及实例的类名。让他们的人用了下这个工具,将结果发给我了,我一看,吓了一大跳。排在第一位的是int[],竟然达到了1G。最有问题的是com.mysql.jdbc.PreparedStatement对象,达到了6万多。com.mysql.jdbc.ResultSetImpl和java.util.HashMap$Entry[]也达到了6万多。不用说,肯定是PreparedStatement没有关闭。
6、查看源代码,发现PreparedStatement对象都在finally块中关闭了,怎么会泄露呢?找了1个小时没有找到,就去洗澡了。在洗澡的时候突然想到,里面有一个for循环,PreparedStatement对象可能被赋值N次,那前面的N-1次不就没有关闭啊,对,找到答案了。赶紧擦干身子出来找到那段代码:
1 String[] valuesArray = value.split(";");
2 for (int i = 0 ;i < valuesArray.length;i++){
3
4 String[] valueArray = valuesArray[i].split(",");
10 ps = conn.prepareStatement(sqlbean.getSql());
11 for(int k = 0;k <valueArray.length;k++){
12 if("s".equalsIgnoreCase(paraTypeArray[k])){
13 ps.setString(k+1,valueArray[k]);
14 }else if("i".equalsIgnoreCase(paraTypeArray[k])){
15 ps.setInt(k+1,Integer.parseInt(valueArray[k]));
16 }
17 }
18
19 rsString = "" + ps.executeUpdate();
20
21 }
确实如此,循环的N-1个PreparedStatement对象没有关闭,导致了泄漏。解决办法就是将
ps = conn.prepareStatement(sqlbean.getSql());
移到for循环的外面,这样就没有问题了。不过从这段代码也可以看出,写得也是在是烂,这个干吗放到
循环的里面呢,本身从语言上来说就有问题。管它呢,解决问题就行了,呵呵。
几乎兴奋了一个晚上。第二天找他们的人一说,他们说是循环N次的,不只是一个值。问题不就解决了?Great。
7、让他们去测试运行吧。运行第一天的晚上九点(这是高峰期)以后,内存非常平稳。问题彻底解决了。
总结这次的优化项目:
对Java虚拟机的认识提高了。对java性能测试工具(JProfiler)更加熟练了,可以和eclipse集成呢,非常方便。