IT168 技术文档】近段时间,我们项目中用到的WebSphere应用服务器(WAS),但在客户的production环境下极不稳定,经常宕机。给客户造成非 常不好的影响,同时,也给项目组很大压力。为此,我们花了近一个月时间对其诊断,现在基本上稳定了,需要继续观察一段时间。现在我主要将工作做一个阶段性 的总结。 
我们的产品环境是:WAS6.0+DB2 8.1+AIX5.3+RS/6000。在该产品环境下,出现的问题非常多,现象如下: 
WAS经常不稳定、宕机几乎一天一次,经常报告OutOfMemory(内存泄漏吗?NO)。 
DB2连接数过大,有时把DB2撑死,有时也把AIX撑死。 
AIX虚拟内存报错、分页报错、IO也报错、还有很多其它莫名奇妙的错。

    总是,每次问题发生的现象和理论上的总是不一致,导致我们不知道从何入手,也无从检测自己的优化参数。咨询过多次IBM技术支持,只解决了某些局部问题。 
虽然问题依然存在,但我想,解决问题的思路、特别是理论基础,还是有一些规律和原则。

对于WAS这块,我近段时间的主要时间集中在以下几个方面(时间顺序): 
1、Java性能监测工具:Jprofiler,也用到Jprobe。后来发现Jprofiler在AIX下几乎不可用。 
2、IBM Java虚拟机和WAS技术细节,特别是IBM JVM的GC原理,我发现它和sun、bea的差别很大。 
3、IBM的heap分析器Heap Analyzer、GCCollector。这两个事后监测工具非常实用,特别是我们的产品运行环境,非测试环境。 
4、某些Application的怀疑和诊断。 
5、AIX诊断,我几乎没有这个能力,只能常规监测一下,需另请高人。

我打算将本文分成以下几个部分总结: 
JVM原理、IBM JVM的GC策略和调优。 
Jprofiler和IBM工具的实际体会 
WAS的诊断体会和AIX调优

    下面开始主题吧,可能比较零碎,另外,开始的理论篇基本上看书都可以,我只是总结一下,再添加一些自己的理解。

以下是我参考的最重要的两本电子书和一些网站: 
《Inside Java Vrtual Machine》:半部分有约四章我认为非常棒,其它章节可能意义不大。 
《The Java Virtual Machine Specification, 2nd》:前半部分有两三章很不错,不过可以对照上一本书看。 
sun的hotspot虚拟机技术:http://java.sun.com/javase/technologies/hotspot/ 
BEA的JRockit虚拟机技术:http://edocs.bea.com/jrockit/geninfo/genintro/index.html JVM技术文档入口,虚拟机理论,内存泄漏诊断等的索引页。 
IBM诊断资料:http://www-128.ibm.com/developerworks/java/jdk/diagnosis/ 上面有一个500多页的pdf文档,对IBM JVM技术和诊断讲解很深入。

    我不得不提的是,在查资料这块,BEA和Sun都有很好的官方文档和论坛支持,并且官方文档导航非常好。虽然IBM的诊断资料也不少,但需要搜索,其搜索 是很痛苦的。而且,IBM官方论坛很差。如果用IBM的产品出问题,切记:找IBM技术支持,千万不要蒙着头搞!反正它们的产品很少免费。说实话,它们的 技术支持还是挺负责的,一般会为你推荐很多support资料,而该资料往往都在developerworks网站上,属于support那个频道,但你 就是搜不着。

Java虚拟机规范概要 

    研究Java虚拟机,首先要了解Sun的Java虚拟机规范。现在,该实现版本很多,如比较有名的Sun、IBM、BEA、Apple、HP、MS、 Apache Harmony。它们都实现了JVM规范,但有各自扩展。譬如,针对IBM虚拟机的堆碎片导致OutOfMemory(OOM),在Sun的虚拟机上就不 会发生。Sun的JVM有maxPermSize的概念,IBM就没有,如果你设置这个参数,虚拟机根本就启动不了。

    比较有意思的是,学Java,就一定要了解各种规范,这和MS的风格很不一样。Sun总是在定义一些规范,实现都留给各厂商。我们除了理解规范本身外,一 定要理解规范和实现之间的关系,譬如JDBC规范和JDBC驱动的关系,它们是怎么组合到一起的。要是你用过php的xml解析库,或db函数,就会体会 深刻,它们可没有什么规范可言,所以每个数据库厂商的db函数用法都不一样。我推荐大家研读一下HSQLDB的jdbc和Tomcat的servlet相 关实现,因为我认为它们还是比较好懂的。 

    JVM规范只是定义一个虚拟机该做什么,但它并没有要求你该怎么做。例如我们最常见的Servlet规范,在该规范中,有 HttpServletRequest、HttpServletResponse,HttpSession等接口,但它们的实现都留给了各个容器厂商。遗 憾的是,规范留下的空白,会把我们这些开发人员给整惨了:容器间移植有时候就是恶梦。譬如J2EE并没有SSO规范,但它很重要,我以前专门针对它做过 WebSphere AppServer和Weblogic AppServer的SSO项目,差别还是不小,不过还是有点共通,那就是都遵循JAAS规范。


JVM的结构 

    从功能上分,Java虚拟机主要由六个部分组成,可以分成三类: 
第一类:JVM API:就是我们最常用的Java API,它是开发人员和Java交互的入口,它主要是JAVA_HOME/jre/lib下的运行时类库rt.jar和编译相关的tools.jar

第二类:JVM内部组件 
类装载器(ClassLoader):将Byte Array的 .class文件装载、链接和初始化。 
内存管理(Memory Managent):为对象分配内存,以及释放内存。后者就是垃圾回收Garbage Collector(GC)。由于JVM最复杂的、最影响性能的就是GC,所以内存管理一般就指垃圾回收。 
诊断接口(Diagostics Interface):这主要体现在JVMTI(jdk1.4下的JVMPI和JVMDI),它主要用来诊断程序的问题和性能,一般提供给工具厂商实现。如eclispe IDE下的debug功能,Jprofiler性能调优工具。 
类解释器(Interpreter):解释装载进虚拟机的class对象,包括JIT等特性相关。

第三类:平台相关接口(Platform Interface):主要为了跨操作系统平台重用JVM代码,不过,它和我们开发人员关系不大。

    在以上六个组件中,我们开发人员最关心的是ClassLoaderGC, 用Java做系统框架、容器和它们密切相关。做业务系统时一些基础代码也和它们打交道,譬如最常用的Class.forName(), Thread.currentThread.getContextClassLoader()。我们仔细想想,为什么是上面两个问题?因为,它和我们 class的整个生命周期最为相关:怎么将一个class和相关class加载进来,class实例什么时候创建,什么时候被销毁? 
所以,下面的部分我们要专门讨论这些问题。

ClassLoader 

    JVM主要有三类ClassLoader:Bootstrap、Extention、Application,该三类ClassLoader从上到下是分级(hierarchy)结构,遵循代理模型(Delegation Model)。 
Tip:大家可以看看sun.misc.Launcher的源码,Bootstrap和Extention就在该文件里。该src可以在sun的网站上下载该压缩包,约60M(jdk-1_5_0-src-scsl.zip),它不在jdk自带的那个src.zip里。

    Bootstrap ClassLoader:也称为primordial(root) class loader。主要是负责装载jre/lib下的jar文件,当然,你也可以通过-Xbootclasspath参数定义。该ClassLoader不能 被Java代码实例化,因为它是JVM本身的一部分。

    Extention ClassLoader:该ClassLoader是Bootstrap classLoader的子class loader。它主要负责加载jre/lib/ext/下的所有jar文件。只要jar包放置这个位置,就会被虚拟机加载。一个常见的、类似的问题是,你 将mysql的低版本驱动不小心放置在这儿,但你的Web应用程序的lib下有一个新的jdbc驱动,但怎么都报错,譬如不支持JDBC2.0的 DataSource,这时你就要当心你的新jdbc可能并没有被加载。这就是ClassLoader的delegate现象。常见的有log4j、 common-log、dbcp会出现问题,因为它们很容易被人塞到这个ext目录,或是Tomcat下的common/lib目录。

    Application ClassLoader:也称为System ClassLoaer。它负责加载CLASSPATH环境变量下的classes。缺省情况下,它是用户创建的任何ClassLoader的父 ClassLoader,我们创建的standalone应用的main class缺省情况下也是由它加载(通过Thread.currentThread().getContextClassLoader()查看)。 
我们实际开发中,用ClassLoader更多时候是用其加载classpath下的资源,特别是配置文件,如ClassLoader.getResource(),比FileInputStream直接。

ClassLoader是一种分级(hierarchy)的代理(delegation)模型。 
Delegation: 其实是Parent Delegation,当需要加载一个class时,当前线程的ClassLoader首先会将请求代理到其父classLoader,递归向上,如果该 class已经被父classLoader加载,那么直接拿来用,譬如典型的ArrayList,它最终由Bootstrap ClassLoader加载。并且,每个ClassLoader只有一个父ClassLoader。 
Class查找的位置和顺序依次是:Cache、parent、self。 
Hierarchy: 上面的delegation已经暗示了一种分级结构,同时它也说明:一个ClassLoader只能看到被它自己加载的classes,或是看到其父 (parent) ClassLoader或祖先(ancestor) ClassLoader加载的Classes。 
在一个单虚拟机环境下,标识一个类有两个因素:class的全路径名、该类的ClassLoader。

    我碰到的一个典型的例子是:在做WAS的SSO开发时,由于我们的类是由WAS在启动时加载,该ClassLoader比下面的部署的Applicaton的ClassLoader的级别高。所以,在我们自己的类中没法用到应用程序的连接池,必须自建。 
代 理模型是Java安全模型的保证。譬如,我们自己写一个String.java,并且编译、package到自己的java.lang包下。按照代理模 型,当前线程的ClassLoader会将其代理到父ClassLoader,父ClassLoader(最终会是Bootstrap)会找到 rt.jar下的String.class,也就是说我们的String.class不会捣乱。

自定义ClassLoader 
    我们前面说过,自定义ClassLoader的缺省父ClassLoader是Application ClassLoader。一般的应用开发用不到它,但我们最好理解。因为在内存泄漏查找、应用程序部署出问题时,很多都和它有关。 
譬 如,内存泄漏是怎么产生的?这就涉及到ClassLoader和Class的生命周期。我曾经碰到这样一个问题:我们的程序用到了Webwork和 Spring框架,当部署到Tomcat下时没有任何问题,但部署到WAS下,报告找不到Webwork的xml的DTD文件,而且Spring的日志也 总是失效。Why?因为解析xml dtd时,用的是IBM的Xerces,不是我们的。而Spring日志问题是因为应用程序用的是WAS的Common-log.jar,而不是我们的。 将应用的ClassLoader从默认的Parent-First,改成Parent-Last就可以解决,不过我们项目中用到其它库,又发生了其它问 题。

一般来说,用到自定义ClassLoader有三种情况: 
1、应用框架可以自己控制Classes的目录,并且自动部署。 
我读过Jive公司的Wildfire(著名的即时通讯服务器),它自己有一套应用框架,非常灵活,遵循该框架插件规范的的第三方的plug-in放置在指定目录可以自动部署,实现某些扩展功能,如文件传输、语音聊天。 
2、区分用户代码 
这被广泛应用在Servlet容器和类似容器,譬如EJB Container设计中,大家看到Tomcat下有common、server、share三个目录吧(ClassLoader顺序从左到有),另外也有用户应用的WEB-INF目录,它是我们自己开发的。 
3、允许Classes卸载 
如 果没有自定义的ClassLoader,那么我们自己应用中的classes永远都不能被卸载,因为这些类被Application ClassLoader加载后cache起来了,我们的classes一直对该ClassLoader有引用,而该系统级的ClassLoader永远都 不会被卸载,除非JVM shutdown了。JSP和Servlet的动态部署就用到这个特性。