1. 发现问题

    最近在检查爬虫程序运行情况的时候发现采集速度非常慢,查看cpu占用率的时候发现有一个内核一直是100%,按理说多线程的程序不应该会出现这样的情况。第一反应就是用jstack -l [pid]把线程dump出来查看栈情况,但没发现什么异常。然后就考虑用jmap -heap [pid]把内存使用情况打印出来看看,奇怪的是出现好几次连接不上的情况,等打印出来发现居然旧生代已经满了。把内存调大,发现过不了一会又是这样,确实非常蹊跷。

2. 深入调查

    看来问题比较棘手,只好用更细致的方式去分析了。一方面打开了GC日志,同时在运行时用jstat -gcutil [pid] 1000来跟踪GC情况。现象很奇怪,旧生代空间缓慢增长,但突然新生代内存占用100%,旧生代紧接着也变成100%,然后就一直处于full gc,但内存丝毫没有减少,整个过程持续了5分钟。(截图忘记保存了,悲剧...)

    看现象就是对象爆炸,但程序已经运行很久了,如果有这么严重的bug应该早就发现了。严谨起见,我用jmap -histo [pid]把程序的对象情况打印出来,结果非常惊讶。

 
    
  1. num     #instances         #bytes  class name 
  2. --------------------------------------------- 
  3.   1:      30110820     1204432800  org.jsoup.parser.ParseError 
  4.   2:         33076      156025088  [Ljava.lang.Object; 
  5.   3:         68836       98796360  [C 

    系统中居然出现那么多ParserError对象,这个是程序中用到的一个Jsoup开源包里面的东西。看命名像是一个Exception,但查看源码时才发现居然是一个Class。至于这个东西从哪里蹦出来的呢?!我在现象出现的时候打印了下线程栈信息,果然定位到了生成这个对象的那个位置。

    在源码中找到相应的实现,主要是调用了这样一个方法:

 
    
  1. private boolean trackErrors = true;
  2. ......
  3. void error(TokeniserState state) { 
  4.     if (trackErrors) 
  5.         errors.add(new ParseError("Unexpected character in input", reader.current(), state, reader.pos())); 

    这里有一个trackErrors做开关,默认是true,但找了一下,居然没有最外层的方法去控制它,而errors这个对象也是一个private值,没有任何调用,可能是作者自己测试用的。

3. 问题的处理

    原因调查清楚,处理就很简单了。直接把源码中trackErrors的默认值改成false,再确认了一下其他相关调用的方法,然后重新编译打包,把原始包替换了。

4. 总结

    找问题还是要先分析出可能的原因,然后借用工具去定位问题,很多时候数据比经验更可靠。