一篇自己写的老的文章,那会儿还有Sun和BEA呢,呵呵,就不改这些文字了,希望对大家有所帮助。
JVM中的服务器软件的稳定性
1 引言
1.1 编写目的
我们的很多产品都是基于Java的服务器软件,这些服务器软件,都需要长期(24×7)稳定运行。这些软件,都是用Java预言开发,基于JVM来运行的。这样来看,这些软件的稳定性,不但受限于我们自己所编写的Java代码,还受限于JVM(当然,还受限于外部的各种环境因素,如DB和网络等)。
经过在一些项目上的经验,和一些客户的要求,我们应该提供一些方式,来证明我们的服务器软件,可以长期稳定的运行。这些方式,可以是操作系统提供的,也可以是Java(JDK、JRE)提供的,甚至也可以包括一些我们自己开发的工具。
为了达到上面的要求,我们就需要对Java的JVM有一个比较深入的了解。在服务器软件稳定性方面,主要相关的东西是内存、线程、垃圾收集器,为了能够说明软件的稳定性,还需要掌握操作系统的各种查看工具和Java提供的各种工具。
1.2 条件与限制
本文档只说明了在当前常用的JVM下的服务器稳定性方面的事项,对于一些较为冷辟的JVM,不进行说明;另外,只针对目前较新版本且稳定的JVM(例如1.4、5.0),对于早期版本(如1.2、1.3)不做说明,对于较新但是应用还比较少的J2SE6.0,也不做讨论。
本文档只描述因为服务器和JVM内部原因引起的服务器稳定性问题,例如线程和内存等,对于一些偶然原因引起的服务器运行问题(例如数据库、网络和机器硬件变动、OS本身的bug等等),也不在这个文档的范围内。
1.3 参考资料
《JVM中垃圾收集器研究》
《JDK监控和管理工具》
2 服务器稳定运行的相关因素
说明服务器运行稳定状况的因素当然最直接的是看服务器提供服务的表现,如果出现异常(例如,没有响应、等待服务返回时间过长、拒绝服务、CPU或内存一直100%等等),当然,直接可以说明服务器运行出现不稳定,需要排查。对于没有异常的情况,我们要说明服务器当前运行良好,关注的主要因素有下面几个:
ü 内存(有无内存泄漏)
ü 线程(有无死锁)
ü 资源占用(如果某项资源一直处于极度匮乏的状态也有可能导致服务器崩溃,例如文件句柄、数据库连接和网络资源)和释放
当然,一个很重要的方面是如果服务进程遇到错误或者异常情况,或者出现其它情况而退出时,应该向外部输出某种信息,说明进程退出的原因,例如,收到操作系统退出进程的信号(用户使用kill杀死进程),或者遇到OOM错误(Out Of Memory),或者遇到线程死锁等等。这些信息,对改进服务器软件的稳定性,会有很大的帮助。
3 方法说明
3.1 内存
JVM为运行一个程序定义了几种数据区(Data Area),包括:pc寄存器、JVM堆栈、堆、方法区(Method Area)、运行时常量池(Runtime Constant Pool)以及本机方法堆栈(Native Method Stacks),这些数据区根据其生存期可以分为两种,一种就是和JVM的生存期相同(包括堆和方法区),一种和线程的生存期相同(其它的),和JVM生存期相同的数据区在JVM启动的时候被创建并在JVM退出的时候被销毁,而和线程生存期相同的数据区是每个线程一个的,他们在线程创建的时候被创建,在线程被销毁的时候被销毁。
由于JVM可以同时支持运行多个线程,因此每个线程必然需要各自的PC(program counter)寄存器,无论从什么角度讲,每个JVM线程只能在一个时间只能执行一个方法,该方法也就是线程的当前方法,如果该方法不是本机方法,那么PC寄存器保存的就是当前指令(JVM的指令)的地址,如果是当前方法是本机方法,PC寄存器的值就没有被定义。JVM的PC寄存器的大小足够大,可以容纳一个returnAddress类型或者特定平台的本机指针。
每个JVM线程还拥有一个私有的JVM堆栈,它存储帧(下一篇文章会讲到)。JVM堆栈和像C这样的传统编程语言中的堆栈是类似的,它保存局部变量和部分结果,并且在方法调用和返回中也担任一些职责。因为除了对帧的压入和弹出操作外,对JVM堆栈不能直接进行操作,因此帧可能是在堆上分配的。如果一个线程中计算所需的JVM堆栈大于允许的大小,JVM会抛出StackOverflowError错误,如果JVM堆栈是可以动态伸缩的,如果需要扩展,但是又没有足够的内存可用或者没有足够的内存为一个新线程创建JVM堆栈,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有线程所共享的堆,所有的类实例和数组都是在堆中创建的。堆所存储的对象被一个自动存储管理系统回收(也就是我们所熟知的垃圾收集器(gc))。对象不能被显式的释放,JVM假设没有特定类型的自动存储管理系统,存储管理技术可以根据实现者的系统需求进行选择。如果计算所需的内存堆大于自动存储管理系统可以使用的大小,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有的线程所共享的方法区,方法区类似传统语言的已编译代码的存储区或者UNIX进程的“文本”段。它存储类结构,例如运行时常量池,成员和方法数据以及方法、构造方法的代码(包括用于类和实例的初始化以及接口类型初始化的特定方法(这些特定方法以后会讲到))。虽然从逻辑上讲方法区是堆的一部分,但是JVM的简单实现可以选择不对方法区进行垃圾收集或者压缩(以笔者的理解就是类不能进行卸载)。最新版本(第二版)的JVM规范没有要求方法区的位置或者管理已编译代码的策略。如果方法区的内存不能满足一个分配请求,JVM会抛出OutOfMemoryError。
运行时常量池是类文件中的常量池表的运行时表示,它包含几种常量,范围从编译时就已知的数字常量到运行时必须进行解析的方法和成员引用。运行时常量池扮演的功能类似于传统编程语言中的符号表(symbol table),但是它所包含的数据比典型的符号表更多。
每个运行时常量池时从JVM的方法区中分配的,对于特定方法或者接口的运行时常量池是JVM在创建类或者接口的时候创建的。
当创建一个类或者接口时,如果创建运行时常量池需要的内存比方法区中的可用内容更多的内存,JVM会抛出OutOfMemoryError。
JVM的实现可能使用传统的堆栈(更通常的讲就是C栈)以支持本机方法(不是使用JAVA语言编写的方法),本机方法堆栈也可以用于在像C语言这样的语言中为JVM指令集实现解析器,对于不能加载本机方法以及自身不依赖传统堆栈的JVM实现而言,它可以不提供本机方法堆栈,如果提供,本机方法堆栈通常在线程创建的时候为每个线程分配(以笔者的理解应该是需要使用本机方法的线程)。如果线程计算所需的内存比本机方法堆栈所允许的大,JVM会抛出StackOverflowError错误,如果本机方法堆栈可以动态伸缩,而当需要扩展的时候又没有足够的内存时,或者没有足够的内容用于创建一个本机方法堆栈,JVM会抛出OutOfMemoryError。
对于上面的这些数据区,JVM规范允许它们的大小是固定尺寸的,也可以是根据计算的需要动态伸缩的,如果是固定尺寸的,其尺寸可以在创建时自主选择。JVM的实现可以给程序员或者用户提供控制JVM堆栈的初始大小的方法,同样,在动态伸缩的情况下可以控制最大大小和最小大小,并且它们所使用的内存空间可以不是连续的
3.1.1.1 堆
相应的,JVM的内存分为两部分,称为年轻代,年老代.在初始化时,实际上保留了一个最大的地址空间,还未分配的物理内存,直到需要分配时。所有为对象内存保留的地址空间可被分成年轻代yong feneration和年老代tenured generation。
年轻代由eden加上两个幸存空间组成。对象在eden中被初始化分配,一个幸存空间在任何时候都是空的,作为拷贝在eden中幸存的对象和另一个幸存空间的下一个目的地。
当对象足够老,并变成tenured时,或被拷贝到tenured generation时,对象在幸存空间之间拷贝。
一个年老代tenured generation称为持久代permanent generation的选项很特别,因为它持有所有虚拟机自身的reflective数据,例如类和方法对象,方法区。
3.1.1.2 栈
1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
3. 每个线程都有自己的栈空间
3.1.1.3 虚拟机启动时命令行分配内存
1.-Xms 最小堆大小
2.-Xmx 最大堆大小
3.-XX:NewRatio 年老代和年轻代的比率
默认情形下,young generation 的大小由NewRatio控制.例如:设置 -XX:NewRatio=3意味着young 和tenured generation的比率是1:3。换句话说就是,eden 和幸存空间的组合大小为总堆大小的1/4。
一般情况下,堆内存设置不应该超过系统的物理内存的1/4~1/2。
在现在的32位系统中,JVM可访问的内存和其它32位应用程序一样,是受限于4GB的大小的。在实际的JVM中,可以分配的最大内存还要更小。如下表:
(测试的方法是在命令行下用 java -XmxXXXXM -version进行测试,然后逐渐的增大XXXX的值,如果执行正常就表示指定的内存大小可用,否则会打印错误信息。)
公司 | JVM版本 | 最大内存(兆) | 备注 |
SUN | 1.5.x | 1520 |
|
SUN | 1.5.5(Linux) | 2660 |
|
SUN | 1.4.2 | 1564 |
|
SUN | 1.4.2(Linux) | 1260 |
|
IBM | 1.4.2(Linux) | 1.4.2(Linux) |
|
BEA | JRockit 1.5 (U3) | 1909 |
|
3.1.1.4 垃圾收集器
JAVA使用垃圾收集器的策略进行内存回收。应用正常运行时垃圾收集器应该正常工作,不会出现内存泄露或者内存管理不正常,应用无法运行的错误。表现在垃圾收集的分析图表上,应该是内存不断被分配然后又释放出来。如果有内存泄露,内存使用会不断积累,直到分配给jvm的内存不够使用造成应用崩溃。
应该根据不同的应用策略确定垃圾收集策略,比如追求系统吞吐量时垃圾收集时间所占比例应该尽量小;追求相应时间时垃圾收集造成的应用停顿应该尽量短。
应用启动时加入-Xloggc:<file>参数,将gc日志记录到文件中,通过gc图形查看工具gcviewer查看gc日志。记录GC日志,还有其它的一些命令行写法,请参见《垃圾收集器研究》文档。
如果GC过于频繁,说明服务器的内存配置不是很恰当;可以通过JVM的命令行参数,来调整堆内存的配置。主要是初始堆大小、最大堆大小和新生代的大小。这些,都可以从GCViewer的分析结果中得出。从另外一个角度来看,如果所有的GC活动都是minorGC,没有发生majorGC(Full GC),则说明GC的配置比较好,因为majorGC的时间停顿会比较长。
通常情况下,我们关注GC的两个参数,频度和持续时间。频度和程序产生临时对象(garbage)和速率和java heap的大小有关,根据应用程序的差异,时间从若干秒/次到若干小时/次都是可以接受的。而每次GC占用的时间则是越小越好,因为它是jvm让人感到在暂停的时间。在应用程序和gc线程数给定的情况下,频度和持续时间是矛盾的。一个小了意味着另一个的增大。因为频度低了,说明对大小很大,而大的堆意味着gc要花费较多的时间,即持续时间要增加,反之亦然。
通过GCViewer来分析,一个需要非常关注的地方是程序的停顿时间,应当追求这个停顿时间在整个应用程序运行期所占的时间百分比。这个比值越小,说明Java应用的GC配置越优化。当然,这个比值也应该是随着请求的变化而变化的,如果请求比较密集,服务器程序需要频繁的启动线程来处理这些请求,在线程结束之后,非常多的线程对象都会变成“垃圾”,需要由GC来进行内存的回收,这个时候,堆内存中的新生代空间占用量会比较快增长,所以,GC也会比起请求量小的时候发起的更加频繁,CPU用于内存的时间占用(应用的停顿时间)也就会更长。
3.1.2.1 操作系统的内存查看工具
操作系统提供了一系列的内存查看工具,可以在JVM软件运行的同时,打开这些工具,来监视内存占用量的变化,以获取服务器稳定性的第一手数据。使用这些工具的好处是可以不修改服务器软件,甚至不需要对服务器的命令行加入任何的(调试相关)的改动。当然,这个数据是纯“外部”的,也就是说,只是从操作系统分配个JVM的内存占用来看的,所以无法获得JVM内部的任何信息。这些工具包括:
Top
Vmstat
Free
Nmon(aix)
Prstat(solaris)
Sar(linux)
ü To
在这里我们就不一一详细解释这些工具的使用方法,工具的使用请自行查阅该工具的说明文档。
3.1.2.2 Java的内存查看工具
Sun的JDK从5.0版本开始,提供了标准的监控方法和一系列的监控工具。其中,最常用的工具包括jps和jconsole。这些工具,在《JDK监控和管理工具》文档中,可以看到有比较详细的介绍。另外,IBM的JDK5也提供了分析和监控的工具。
JPS是监控工具的辅助工具,jps将列举运行在本机的Java应用的进程,以供其它监控工具连接到该进程(仅限于JVM5.0以上版本的JVM)。Jconsole是一个实时的监控工具,可以连接到一个运行的Java应用,获取该JVM的运行数据,并将这些数据表现为非常直观的图表,以方便进行分析和处理。在Jconsole可以查看的数据项中,内存是首选的视图。在JVM的内存图表中,可以看到服务器软件的JVM中,堆的使用情况。假如已使用的堆较为平稳,或者虽然有高低变化,但总是围绕这一个标准范围而波动,说明服务器软件没有那些不能回收的内存,即没有内存泄漏。
如果所使用的JDK为BEA的jrockit,可以使用BEA提供的Mission Control工具来查看。该工具和SUN的JConsole比较相似。这里就不再赘述了。
使用这些工具的好处是可以在服务器运行期间,实时监控内存的状态,可以获得软件运行期间的一个非常直观的图表。它的弊端是只有J2SE5.0以上的JVM才支持,另外,使用这些工具要求修改启动服务器的命令行,使之提供监控的各种信息。
3.1.3 内存使用监控图
3.1.3.1 正常的应用内存使用图:
对应的GC图表:
在上面两个图中,我们可以看到,在服务器的运行期间,内存始终在一个范围内波动,这说明服务器分配的对内存,都可以被GC回收(占用的堆内存增长是因为有业务在进行,需要创建对象;堆内存占用数量又减少是因为分配的堆内存被GC回收)。说明服务器软件不存在那些不能被回收的对象,即没有内存泄漏。
下边是一段从上图对应的GC日志中摘出来的一段内容:
403.550: [ParNew 108275K->3157K(511232K), 0.0060260 secs]
405.011: [ParNew 108117K->3272K(511232K), 0.0058760 secs]
409.093: [ParNew 108232K->3349K(511232K), 0.0065070 secs]
410.874: [ParNew 108309K->3205K(511232K), 0.0061460 secs]
412.432: [ParNew 108165K->3178K(511232K), 0.0057460 secs]
414.476: [ParNew 108138K->3715K(511232K), 0.0080270 secs]
418.448: [ParNew 108675K->3270K(511232K), 0.0060210 secs]
419.966: [ParNew 108230K->3187K(511232K), 0.0058110 secs]
424.106: [ParNew 108147K->3218K(511232K), 0.0059760 secs]
425.746: [ParNew 108164K->3188K(511232K), 0.0059450 secs]
427.847: [ParNew 108148K->3375K(511232K), 0.0065830 secs]
431.924: [ParNew 108335K->3129K(511232K), 0.0057210 secs]
433.373: [ParNew 108089K->3223K(511232K), 0.0059960 secs]
435.518: [ParNew 108183K->3327K(511232K), 0.0065540 secs]
440.308: [ParNew 108287K->3091K(511232K), 0.0053920 secs]
441.776: [ParNew 108051K->3169K(511232K), 0.0057540 secs]
446.208: [ParNew 108129K->3145K(511232K), 0.0055130 secs]
447.664: [ParNew 108105K->3229K(511232K), 0.0062320 secs]
451.564: [ParNew 108189K->3192K(511232K), 0.0070390 secs]
453.079: [ParNew 108152K->3174K(511232K), 0.0060350 secs]
454.716: [ParNew 108134K->3216K(511232K), 0.0065420 secs]
458.569: [ParNew 108169K->3439K(511232K), 0.0068630 secs]
460.065: [ParNew 108398K->3259K(511232K), 0.0063260 secs]
461.965: [ParNew 108219K->3279K(511232K), 0.0063540 secs]
466.534: [ParNew 108236K->3304K(511232K), 0.0065140 secs]
467.994: [ParNew 108264K->3313K(511232K), 0.0061010 secs]
472.005: [ParNew 108273K->3227K(511232K), 0.0063930 secs]
473.570: [ParNew 108187K->3339K(511232K), 0.0062130 secs]
475.689: [ParNew 108299K->3136K(511232K), 0.0058440 secs]
480.281: [ParNew 108096K->3189K(511232K), 0.0059010 secs]
481.756: [ParNew 108149K->3103K(511232K), 0.0053470 secs]
483.464: [ParNew 108063K->3264K(511232K), 0.0064050 secs]
487.276: [ParNew 108224K->3112K(511232K), 0.0055590 secs]
488.902: [ParNew 108072K->3328K(511232K), 0.0064740 secs]
492.924: [ParNew 108288K->3166K(511232K), 0.0055760 secs]
494.433: [ParNew 108118K->3196K(511232K), 0.0060070 secs]
496.262: [ParNew 108156K->3068K(511232K), 0.0054370 secs]
500.687: [ParNew 108028K->3182K(511232K), 0.0061820 secs]
502.139: [ParNew 108142K->3128K(511232K), 0.0057400 secs]
504.059: [ParNew 108088K->3228K(511232K), 0.0062660 secs]
508.630: [ParNew 108188K->3226K(511232K), 0.0061710 secs]
510.081: [ParNew 108186K->3212K(511232K), 0.0058310 secs]
511.829: [ParNew 108172K->3479K(511232K), 0.0070610 secs]
515.777: [ParNew 108439K->3304K(511232K), 0.0063330 secs]
517.344: [ParNew 108264K->3309K(511232K), 0.0062730 secs]
519.603: [ParNew 108269K->3119K(511232K), 0.0056350 secs]
523.007: [ParNew 108079K->3252K(511232K), 0.0064360 secs]
525.444: [ParNew 108212K->3275K(511232K), 0.0060740 secs]
527.233: [ParNew 108235K->3125K(511232K), 0.0056750 secs]
531.341: [ParNew 108085K->3077K(511232K), 0.0054160 secs]
532.805: [ParNew 108037K->3127K(511232K), 0.0058350 secs]
534.647: [ParNew 108087K->3185K(511232K), 0.0060830 secs]
539.105: [ParNew 108145K->3185K(511232K), 0.0061300 secs]
540.543: [ParNew 108137K->3266K(511232K), 0.0060260 secs]
544.678: [ParNew 108226K->3092K(511232K), 0.0058330 secs]
546.178: [ParNew 108052K->3292K(511232K), 0.0063760 secs]
548.023: [ParNew 108252K->3403K(511232K), 0.0067870 secs]
551.925: [ParNew 108363K->3337K(511232K), 0.0069590 secs]
553.426: [ParNew 108294K->3253K(511232K), 0.0062840 secs]
558.031: [ParNew 108202K->3237K(511232K), 0.0061090 secs]
从这个日志来看,GC的频度非常高,每4秒钟左右,就会发生一次minorGC,这说明在这个时间段内,程序产生临时对象非常多,对应于业务系统来讲,可能处于业务的高峰期。事实上,上面的GC是我们的服务器程序正在性能测试期间采集的,在性能测试中,始终在进行高并发的业务操作,而每个业务的处理,都会产生非常多的临时对象,在业务处理完成之后,业务线程以及这些临时对象都会成为垃圾对象,会被GC安排回收,因此,垃圾的产生非常快,非常多,所以GC的频度比较高。
3.1.3.2 典型的内存泄露图
3.1.3.2.1 有内存泄露的图
相应的垃圾收集器图形:
上图中,蓝色曲线表明的是内存使用情况。可以看到垃圾收集器没有能够回收到可用内存。
上面的两个图是一个有内存泄漏的长时间运行的Java程序的运行时内存监控图和GC日志分析图。可以清楚的看到,JVM所占用的堆内存随着运行时间的增长,会一直增加,知道到达系统的最大堆内存,这个时候,如果GC还不能回收堆内存(如果系统在处理上存在问题,所分配的堆内存中的对象一直没有释放,GC会认为这些对象还在被引用,因此不能回收这些内存),就会发生NoEnoughMemeoryError,如果服务器软件本身又没有对这种错误的处理(一般发生内存不足的错误,软件是没有什么好的方法的),软件会出错退出。下面的两个图,也证明了这一点。
3.1.3.2.2 .另外一个有内存泄露的图
此图可以看出当内存使用急剧增加时cpu使用率也急剧上升以进行垃圾收集,释放更多的内存供应用使用。
3.2 JVM的进程和线程
3.2.1 JVM的进程和线程
每个Java程序运行的时候,系统都会启动一个jvm来执行java程序,所以从操作系统的角度看,每个java程序只启动一个进程----JVM。
Java启动时,会启动一系列(大约10个到20个)基础线程来运行,这些线程包括类装载、GC等等,都是JVM自身的线程。然后,随着用户应用(启动线程的语句)的调用,启动其它的线程。一般情况下(不使用线程池的情况),随着业务量的增长,线程数也会相应增长。
在有些操作系统中,使用进程来模拟线程,例如有些Linux系统中。这个时候,使用进程查看工具来看时,一个服务器软件,在业务量较多时,体现为多个Java进程在同时运行。
3.2.2 使用线程可能会出现的问题
无论是客户端还是服务器端多线程Java程序,最常见的多线程问题包括死锁、隐性死锁和数据竞争。
3.2.2.1 死锁
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。
3.2.2.2 隐性死锁。
隐性死锁由于不规范的编程方式引起,但不一定每次测试运行时都会出现程序死锁的情形。由于这个原因,一些隐性死锁可能要到应用正式发布之后才会被发现,因此它的危害性比普通死锁更大。下面介绍两种导致隐性死锁的情况:加锁次序和占有并等待。
3.2.2.3 数据竞争。
数据竞争是由于访问共享资源(例如变量)时缺乏或不适当地运用同步机制引起。如果没有正确地限定某一时刻某一个线程可以访问变量,就会出现数据竞争,此时赢得竞争的线程获得访问许可,但会导致不可预知的结果。
由于线程的运行可以在任何时候被中断(即运行机会被其它线程抢占),所以不能假定先开始运行的线程总是比后开始运行的线程先访问到两者共享的数据。另外,在不同的VM上,线程的调度方式也可能不同,从而使数据竞争问题更加复杂。
有时,数据竞争不会影响程序的最终运行结果,但在另一些时候,有可能导致不可预料的结果。
3.2.3 线程监控
基于Java的产品,在运行时可以通过操作系统或者其他工具查看线程使用情况。如果出现死锁,会出现cpu使用率不高,线程长时间不退出。系统正常运行时,线程会不断创建、执行、退出,当没有业务时线程数会降低到很低的水平。当然,对于使用了线程池的服务器软件来说,又是完全不同的一种情形,线程监控得到的结果应该和服务器软件的线程池配置是一致的。
3.2.3.1 OS提供的线程监控
Windows
可以通过资源管理器来监控服务器软件中线程的运行情况。
Linux/Unix:
可以通过ps命令来监控线程的情况(具体选项可以查阅各平台ps命令的帮助)。当然,有些Linux不支持查看线程,例如Redhat
3.2.3.2 Java的线程监控工具
使用sun和IBM的jdk1.5以上产品,可以通过jconsole工具来查看;如果使用BEA的产品,可以使用BEA JRockit(R) Mission Control 2.0工具。关于这些工具的使用方法,请参见各工具的使用手册。这里就不再赘述了。
3.3 线程使用监控图:
3.3.1 正常的应用线程使用图:
图一
由上图可以看出,当应用繁忙时服务线程会迅速提升,当应用空闲时线程数会下降到比较低的水平。
图二:
查看活动线程状态,如果没有发现状态为BLOCKED或者DeadLocked的,则可以得到结论,在软件运行期间,没有发生线程阻塞或死锁。
3.3.2 产生死锁的应用的线程图
根据曲线有线程没有正常退出;下面鼠标点中的蓝色部分显示,有两个线程被堵塞。如果大型应用产生了死锁,线程数可能会迅速增加耗尽系统资源。
4 资源使用
服务器软件运行期间,需要占用特定的系统资源,除了CPU、内存和磁盘空间之外,关键资源还包括下列的这些:
ü TCP连接
ü 线程
下面对这些资源的占用情况对服务器稳定性的影响进行逐一描述。
4.1 TCP连接
服务器软件通常是运行在TCP协议之上,通过一个TCP连接(通常是Socket连接)接受一定的请求信息,经过处理,通过这个TCP连接,发送特定的响应信息给请求者。通常情况下,服务器监听一个端口,接受到客户请求后,TCP连接就建立起来,然后,服务器还可以同样接受其它客户的请求,这样,可以建立很多的TCP连接。
TCP连接都要占用一定的内存,并且,每个连接都需要服务器资源来进行读取、处理和写入数据。大体上来说,TCP的连接数量都是和业务量相关的,在业务高峰时段,TCP连接数目会非常多,而随着业务量的减小,TCP连接数目也会减小。
当然,从操作系统TCP的实现上来讲,TCP的连接即便在接受到关闭指令之后,都需要经过一系列TCP信号的往复才能正常关闭,而这些往复,往往需要一定的时间,这个时间,可以通过操作系统的参数来查看和进行调整。所以,通过操作系统目录来查看TCP连接的数量和状态时,所得到的结果和业务量的变化相比,是有一定的延迟的(存在一定数目的WAIT状态的TCP连接)。但是,总体趋势上来讲,TCP连接数和业务量的变化应该是大体一致的,或者说,可以通过服务器上查看TCP连接的数量和状态了解到前端业务的状况;如果有不一致的情况出现,则应该去检查服务器和业务系统,确定是否有异常状况的出现。
有一种特例的情况,即前端业务系统和服务器通信时采用了Socket池的技术,这样的情况下,前端在建立TCP连接后就会一直保持,知道退出程序或者按照Socket池的某种策略,将这些TCP连接真正关闭。在这种机制下,一个TCP连接建立完成后,进行一次业务通信完成之后并不关闭,而是放回池中,等待下一次业务通信时复用。这时,我们查看服务器的TCP连接时,可以看到即便业务量比较小的时段内,还是有大量的TCP连接连到服务器,这也说明了前端业务系统和服务器之间的Socket池机制在正常工作。
在某些操作系统中,TCP连接也作为文件被打开,而在操作系统中,很多都有一个“可打开的最大文件数”的限制,对于需要长时间运行的服务器软件,所打开的文件和TCP连接加起来很容易超出这个限制,这个时候软件就会出现错误,所以大多数的时候,需要调整操作系统的这个参数。在Linux/unix下,这个命令是uname,请参见该命令的文档。
在windows和Linux/Unix系统中,查看TCP连接,所使用的命令为netstat。关于此命令的使用方法,请参考各操作系统的文档描述,这里不再赘述。
4.2 线程
服务器软件一般是启动一个线程监听某些端口,然后,在这个监听接受到一个请求之后,产生一个新的线程来对该TCP连接进行读取、业务处理和写入响应的工作。每个线程建立和启动时,系统都会为线程分配资源,为了性能的考虑,很多服务器采用了线程池的机制,这样,一方面在Java中,实现了线程对象的复用(节省可每次创建线程的资源消耗和时间),一方面,也避免了业务高峰时,创建数量非常多的线程有可能造成的系统崩溃。如果采用了线程池的机制,就需要根据性能测试的情况,找到一个合适的池大小配置和池的管理策略。
一般来将,如果没有采用线程池机制的情况下,线程数和业务量的变化应该是一致的。一般JVM有一些线程是JVM自身需要的,服务器中,也有一些线程是固定的,另外的部分,就是随着业务量变化的部分了。如果发现这个线程数量和业务量的变化不一致,也应该查看是否有异常状况的产生。
5 结论
5.1 怎样才能证明服务器稳定性
在现在这个阶段,系统的下面指标都在正常范围内,大体上说明服务器能够稳定运行。当然,环境千变万化,所有的正式的结论,只能由完整的系统测试和较长时间的稳定性测试才能最终确定。在这里根据上文的各项进行一个简单的总结,也作为判断系统稳定性的一个依据。
ü 内存
² 可用内存平稳,业务量大时会在一个范围内波动,但不会一直增长上去
² GC稳定运行,没有造成太大的应用程序停顿
ü 线程(不使用线程池的情况)
² 随着业务量的增长,线程数也增长,但是活动的线程也会在一个范围内波动,不会一直增长。
² 没有阻塞太长时间的线程,没有死锁状态的线程
ü CPU
² 随业务量的增长而上升,业务量减小之后恢复到正常;不会一直处于峰值
ü 系统资源
² 系统资源的使用没有一直处于高峰状态,例如网络连接、数据库连接和线程等,应该和业务量的变化一致。对于网络服务器软件来说,TCP连接也是一个很重要的因素。
5.2 软件内部特性与稳定性相关的因素
ü 对系统资源的使用(包括数据库连接、线程)应该有池化机制,不会在业务高峰到来时造成服务器或者数据库等设施崩溃。并且应该可以查看池资源的使用状况,来确定池的大小和管理策略的配置是否恰当。
ü 对资源的占用时间应该尽量短,尽可能占用较短的时间,尽可能晚的打开,尽可能早的释放和关闭,以避免因为后来的业务无法得到系统资源而出错,也可以避免大量业务在等待资源而排队时,积累太多的待处理业务而导致服务器崩溃。
5.3 有关崩溃
如上文描述,软件出现崩溃现象,有可能是必然原因(软件BUG、数据问题等),也有偶然原因(网络、数据库异常等)。我们应该在软件的运行中,加入一种机制,使软件在遇到问题,进程退出时,将出错信息记录到某个日志文件,根据日志的记录,来排查出错的原因。具体记录的方法,根据操作系统、JVM的不同也不尽相同。关于这个可以查看不同操作系统下不同JVM的说明文档。