JVM问题——解决方案

文章详细分析了JVM内存管理中的问题,包括如何识别JVM问题,如频繁GC和FullGC,以及如何调整新生代、老年代和Survivor区的大小以优化性能。同时,讨论了CPU消耗问题,指出上下文切换和线程数过多可能导致CPU利用率过高,并提出了解决方法。此外,还涉及了IO问题,特别是文件IO和网络IO的优化策略,如异步写入、批量操作和限流。最后提到了协程在处理高并发时的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • JVM问题
  1. 怎样算JVM问题,JVM问题的特征有哪些?

在JVM内存中是否出现频繁GC,特别是fullGC比较频繁,GC动作会挂起应用线程,严重影响性能。

查看线程的运行情况,线程是否阻塞、是否出现了死锁。

宿主机器问题、排查日志,检查程序代码

当minor GC过于频繁,或发现经常出现 minor GC时,Survivor的一个区域空间满,且 Old Gen 增长超过了Survivor区域大小时,就需要考虑新生代大小的调整了。  -- a.避免新生代大小设置过小

Full GC频繁执行,minor GC的耗时大幅度增加   --b.避免新生代设置过大

几乎每次minor GC后都会有部分对象从新生代转入旧生代,但从第二次Full GC来看,这些转入旧生代的对象其实都是可以被回收的,也就是说,只要这些对象能够在新生代多存活一段时间,就可以在minor GC阶段被回收掉   -- c.避免Survivor区过小或过大

  1. 我的解决方案是什么

思路:通过常用的内存管理调优的方法,尽量降低GC所导致的应用暂停时间

在不采用G1(G1不区分minor GC和Full GC)的情况下,通常minor GC会远快于Full GC,各个代的大小设置直接决定了minor GC和Full GC触发的时机,在代大小的调优上,最关键的参数为:-Xms -Xmx -Xmn-XX:SurvivorRatio -XX:MaxTenuring Threshold。

-Xms 和-Xmx通常设置为相同的值,避免运行时要不断地扩展JVM内存空间,这个值决定了JVMHeap所能使用的最大空间。

-Xmn 决定了新生代(New Generation)空间的大小,新生代中 Eden、SO和S1三个区域的比率可通过-XX:SurvivorRatio来控制。

-XX:MaxTenuringThreshold控制对象在经历多少次Minor GC后才转入旧生代,通常又将此值称为新生代存活周期,此参数只有在串行GC时有效,其他GC方式时则由Sun JDK自行决定。

代大小调优策略

a.避免新生代大小设置过小

当新生代大小设置过小时,会产生两种比较明显的现象,一是minor GC 的次数更加频繁;二是有可能导致minor gc对象直接进入旧生代,此时如进入旧生代的对象占据了旧生代剩余空间,则触发Full GC.

书中案例以-Xms135M -Xmx135M -Xmn20M -XX:+UseSerialGC执行上面的代码,通过jstat跟踪到的GC状况为:

共触发7次minor GC,耗时为0.318秒,共触发2次Full GC,耗时为0.09秒,GC总耗时为0.408秒。

其中第一次Full GC是代码中主动调用System.gc造成的,而第二次Full GC则是因为minor GC时Survivor区空间不足,导致了对象直接进入了旧生代,这些对象加上旧生代原有的一些对象占满了旧生代空间,于是Full GC被触发。而根据代码来看,其实只要新生代的空间能够支撑到tmpObjects 中的对象填充完毕,那么下次minor GC就可以把tmpObjects 所占据的空间全部回收掉,避免掉这次不必要的Full GC。按照这样的思路,调大新生代到30MB,重新执行,执行后通过jstat跟踪到的GC状况为:

共触发4次minor GC,耗时为0.303秒,共触发1次Full GC,耗时为0.063秒,GC总耗时为0.366秒。

从上面的结果可以看出,经过这样的调整,有放地减少了Full GC的频率

除了调大新生代大小外,如果能够调大JVM Heap的大小,那就更好了,但JVMHeap调大通常意味着单次GC时间的增加。调整时的原则是在不能调大JVM Heap的情况下,尽可能放大新生代空间,尽量让对象在minor GC阶段被回收,但新生代空间也不可过大;在能够调大JVM Heap的情况下,则可以按照增加的新生代空间大小增加JVM Heap大小,以保证旧生代空间够用。

b.避免新生代设置过大

新生代设置过大会带来两个典型的现象,一是旧生代变小了,有可能导致Full GC频繁执行;二是minor GC的耗时大幅度增加。

仍然用上面的例子,调整为以下参数执行: -Xms135M -Xmx135M -Xmn105M,通过 jstat观察到其GC状况为:

共触发1次minor GC,耗时为0.141秒,共触发2次Full GC,耗时为0.152秒,GC总耗时为0.293秒。

从这个调整和之前把新生代调为30MB时对比,此时minor GC下降了,但Full GC仍然多了一次。原因在于,当第二次到达minor GC的触发条件时,JVM基于悲观原则,判断目前old区的剩余空间小于可能会从新生代晋升到old 区的对象的大小,于是执行了Full GC,而从minor GC消耗的时间来看,单次minor GC的时间也比以前慢了不少。

从上面的分析来看,可见新生代通常不能设置得过大,大多数场景下都应设置得比旧生代小,通常推荐的比例是新生代占JVM Heap区大小的33%左右。

 c.避免Survivor区过小或过大

根据第3章“深入理解JVM”中的讲解,在采用串行GC时,默认情况下Eden、SO、S1的大小比例为8:1:1,调整为以下参数执行上面示例的代码:-Xms135M -Xmx135M -Xmn20M-XX:SurvivorRatio=10 -XX:+UseSerialGC,通过jstat观察到其GC状况为:

共触发6次minor GC,耗时为0.324秒,共触发1次Full GC,耗时为0.055秒, GC总耗时为0.379秒。

从上面的调整结果来看,在没有调大新生代空间的情况下,同样避免了第二次Full GC的发生。简单分析一下原因:在上面的场景中,tmpObjects 在创建的过程中需要大概16MB 的空间,新生代大小设置为20MB,默认情况下Eden区的大小为16MB,两个Survivor 区分别为2MB,当tmpObjects创建时,会填满Eden space,从而触发minor GC。而此时这16MB的对象都是有引用的对象,minor GC时只能将其放入Survivor区,但Survivor区只有2MB的空间,因此将有14MB 的对象转入旧生代中,而旧生代的空间大小为115MB,之前已用了101MB 左右的空间,当这14MB对象加入时,旧生代空间被占满,于是Full GC被触发。在加入了-XX:SurvivorRatio=10参数后,Eden区的大小调整为16.7MB,当tmpObjects创建完毕时,还不足以触发minor GC,自然 Full GC也被避免了。

从上面的分析来看,在无法调整JVM Heap 以及新生代的大小时,合理调整Survivor 区的大小也能带来一些效果。调大'SurvivorRatio值意味着Eden区域变大,minor GC 的触发次数会降低,但此时Survivor区域的空间变小了,如有超过Survivor空间大小的对象在minor GC后仍没有被回收,则会直接进入旧生代;调小SurvivorRatio则意味着Eden区域变小,minor GC的触发次数会增加,Survivor区域变大,意味着可以存储更多在minor GC后仍存活的对象,避免其进入旧生代。

d.合理设置新生代存活周期

新生代存活周期的值决定了新生代的对象经过多少次Minor GC后进入旧生代,因此这个值也需要根据应用的状况来做针对性的调优,JVM参数上这个值对应的为-XX:MaxTenuringThreshold,默认值为15次。

书中一段代码目的先让旧生代中放置一些对象,然后让某个对象在经历了16此minor GC后仍存活,超过默认的TenuringThreshold值从而进入旧生代

通过jstat观察到的GC结果信息为:

共执行了484次minor GC,耗时为0.528秒,共执行了2次Full GC,耗时为0.107秒,GC时间总共为0.635秒。

调整对象在新生代中的生存周期为16,即让示例代码中 toOldObject在新生代中多存活一次minorGC,从而可以让 toOldObject对象在minor GC阶段被回收,增加参数:-XX: MaxTenuringThreshold=16,执行结果如下:

通过jstat观察到的GC结果信息为:

共执行了484次 minor GC,耗时为0.521秒,共执行了1次Full GC,耗时为0.073秒,GC时间总共为0.593秒。

从上面的调整结果可见,在增大了存活周期后,对象在Minor GC 阶段被回收的机会就增加了,但同时带来的是survivor区被占用,但此值仅在串行GC 和 ParNew GC 时可调整。

GC策略的调优

在讲解了Sun JDK所提供的几种GC策略,串行GC性能太差,因此在实际场景中使用的主要为并行和并发GC

书中通过案例代码来多次触发GC,查看并行GC以及并发GC时对于应用造成的不同的暂停时间。

以-Xms680M -Xmx680M -Xmn80M -XX:+UseConcMarkSweepGC -XX:+PrintGCApplication-StoppedTime-XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=5参数执行以上代码,通过jstat观察到的GC状况如下:

共触发39次minor GC,耗时为1.197秒,共触发21次CMS GC,耗时为0.136秒,GC总耗时为1.333秒。

GC 动作造成应用暂停的时间为:1.74秒。

以-Xms680M -Xmx680M -Xmn80M -XX:+PrintGCApplicationStoppedTime -XX:+UseParallelGC参数执行以上代码,通过jstat观察到的GC状况如下:

共触发119次minor GC,耗时为2.774秒,共触发8次Full GC,耗时为0.243秒,GC总耗时为3.016秒。

GC动作造成应用暂停的时间为:3.11秒。

从上面的结果来看,由于CMS GC 多数动作是和应用并发进行的,确实可以减小GC动作给应用造成的暂停。

大部分Web应用在处理请求时设置了一个最大可同时处理的请求数,当超过此请求数时,会将之后的请求放入等待队列中,而这个等待队列也限制了大小。当等待队列满了后仍然有请求进入,那么这些请求将会直接被丢弃,所有的请求又都是有超时限制的。在这种情况下如触发了造成应用暂停时间较长的Full GC,那么有可能在这次Full GC后,应用中很多请求就超时或被丢弃了。例如请求的超时时间为3秒,在排队时应用中触发了一次Full GC,造成了3秒的暂停,那么之前在此应用上等待处理的请求就全部超时了。从上可看出,Web应用非常需要一个对应用造成暂停时间短的GC,再加上大部分Web应用的瓶颈都不在CPU上。因此对于Web应用而言,在G1还不够成熟的情况下,CMS Gc是不错的选择。

  1. 用到哪些参数

对于代大小的调优,主要是合理调整-Xms、-Xmx、-Xmn 以及

-XX:SurvivorRatio的值,尽可能减少GC所占用的时间。

-Xms、-Xmx适用于调整整个JVM Heap区大小,在内存不够用的情况下可适当加大此值,这个值能调整到多大取决于操作系统位数以及CPU的能力。

-Xmn适用于调整新生代的大小,新生代的大小决定了多少比例的对象有机会在minor GC阶段被回收,但此值相应的也决定了旧生代的大小。新生代越大,通常意味着多数对象能够在minor GC阶段被回收掉,但同时意味着旧生代的空间会变小,可能会造成更频繁的Full GC,甚至是OutOfMemoryError。

-XX:SurvivorRatio适用于调整Eden区和Survivor区的大小, Eden区越大通常也就意味着minor GC发生的频率越低。但有可能会造成Survivor区太小,导致对象在经过minor GC后直接就进入旧生代了,从而更频繁的触发Full GC,这取决于当Eden区满的时候其中存活对象的比例。

在清楚掌握minor GC、Full GC的触发时机以及代大小的调整后,结合应用的状况(例如创建出的对象都可很快被回收掉、缓存对象多等)通常就可较好设置代的大小,减少GC所占用的时间。在调整后可结合jstat、VisualVM等查看GC的变化是否达到了调优的目的。

1、可以使用jmap来查看JVM中各个区域的使用情况。

2、可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁。

3、可以通过jstat命令来查看垃圾回收的情况,特别是fullgc比较频繁,那么就得进行调优了。

4、通过各个命令的结果,或者jvisualvm等工具来进行分析。

  • CPU问题

CPU消耗分析

在Linux中,CPU主要用于中断、内核以及用户进程的任务处理,优先级为中断>内核>用户进程,在分析CPU消耗之前要知道三个概念

·上下文切换

  每个CPU在同一时间只能执行一个线程,Linux采用抢占式调度。即为每个线程分配一定的执行时间,当到达执行时间、线程中IO阻塞或高优先级线程要执行时,Linux将切换执行的线程,在切换时存储目前线程执行状态,并恢复要执行的线程的状态,这个过程称为山下文切换。在Java应用,典型的是在进行文件IO操作、网络IO操作、锁等待或线程Sleep时,当前线程会进入阻塞或休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,应用的响应速度下降。

·运行队列

每个CPU核都维护了一个可运行的线程队列,例如一个4核的CPU,Java应用中启动了8个线程,且这8个线程都处于可运行状态,那么在分配平均的情况下每个CPU的运行队列里就会有两个线程。运行队列值越大,就意味着线程会消耗越长的时间才能执行完。

·利用率

CPU利用率为CPU在用户进程、内核、中断处理、IO等待以及空闲五个部分使用百分比,这五个值是用来分析CPU消耗情况的关键指标。一般建议用户进程的CPU消耗/内存的CPU消耗的比率在65%-70%/30%-35%左右。

在Linux中,可通过top或pidstat方式来查看进程中线程的CPU的消耗状况

  1. top

输入top命令后即可查看CPU的消耗情况

在此需要关注的是第三行的信息,其中 4.0% us表示为用户进程处理所占的百分比;8.9% sy表示为内核线程处理所占的百分比;0.0% ni表示被nice命令改变优先级的任务所占的百分比;87.0%id表示CPU的空闲时间所占的百分比;0.0% wa表示为在执行的过程中等待IO所占的百分比:0.2% hi表示为硬件中断所占的百分比:0.0% si表示为软件中断所占的百分比。

  1. 怎样算CPU问题,CPU问题的特征有哪些?

对于Java应用而言,CPU消耗严重主要体现在us、sy两个值上

1.us

  Java应用造成us高的原因主要是线程一直处于可运行状态,通常是这些线程在执行无阻赛、循环、正则或纯粹的计算等动作造成;另外一个可能也会造成us高的原因是频繁的GC.

  2.sy

  在sy值高时,表示Linux花费了更多的时间在进行进程切换,Java应用造成这种现象的主要原因是启动的线程比较多,且这些线程多数都处于不断的阻塞(例如锁等待、IO等待状态)和执行状态的变化过程,这就导致了操作系统要不断地切换执行的线程,产生大量的上下文切换

  1. 我的解决方案是什么
  1. CPU us高的解决方法

CPU us高的原因主要是执行线程无任何挂起动作,且一直执行,导致CPU没有机会去调度执行其他的线程,造成线程饿死的现象。我们常见的一种优化方法是对这种线程的动作增加Thread.sleep,以释放CPU的执行权,降低CPU的消耗。

  按照这样的思想,书中案例在往集合中增加元素的部分增加sleep,修改如下:

从上结果可见,CPU的消耗大幅度下降,当然,这种修改方式是以损失单次执行性能为代价的,但由于降低了CPU的消耗,对于多线程的应用而言,反而提高了总体的平均性能。在实际的Java应用中会有很多类似的场景,例如多线程的任务执行管理器,它通常要通过扫描任务集合列表来执行任务。对于这些类似的场景,都可通过增加一定的sleep时间来避免消耗过多的CPU。

  还有一种经典的场景是状态的扫描,例如某线程要等其他线程改变了值后才可继续执行。对于这种场景,最佳的方式是改为采用wait/notify机制。

  对于GC频繁造成的CPU us高的现象,则要通过JVM调优或程序调优降低GC的执行次数。

  1. CPU sy 高的解决方法

CPU sy高的原因主要是线程的运行状态要经常切换,对于这种情况,最简单的优化方法是减少线程数。

按照这样的思路,将CPU资源消耗中的例子重新执行,将线程数降低,执行结果:

可见减少线程数是能让sy值下降的,所以不是线程数越多吞吐量就越高,线程数需要设置为合理的值,这要根据应用的情况来具体决定,同时使用线程池避免要不断地创建线程。如应用要支撑大量的并发,在减少线程数的情况下最好是增加一个缓冲队列,避免因为线程数的减少造成系统出错率上升。

 造成sy高还有一个重要原因是线程之间锁竞争激烈,造成了线程状态经常要切换,因此尽可能降低线程间的锁竞争也是常见的优化方法。锁竞争降低后,线程的状态切换的次数也就会下降,sy值会相应下降。但值得注意的是,如果线程数过多,调优后有可能会造成us值过高,所以合理地设置线程数非常关键。锁竞争更有可能造成系统资源消耗不多,但系统性能不足的现象。

协程

除了以上两种情况外,对于分布式Java应用而言,还有一种典型现象是应用中有较多的网络IO操作或确实需要一些锁竞争机制(例如数据库连接池),但为了能够支撑高的并发量,在Java应用中又只能借助启动更多的线程来支撑"。在这样的情况下当并发量增长到一定程度后,可能会造成CPU sy高的现象,对于这种现象,可采用协程(Coroutine"2)来支撑更高的并发量,避免并发量上涨后造成CPU sy消耗严重、系统load迅速上涨和系统性能下降。

在目前的Sun JDK实现中,创建并启动一个Thread对象就意味着运行了一个原生线程,当这个线程中有任何的阻塞动作(例如同步文件 IO、同步网络IO、锁等待、Thread.sleep 等)时,这个线程就会被挂起,但仍然占据着线程的资源。当线程中的阻塞动作完成时,由操作系统来恢复线程的上下文,并调度执行,这是一种标准的遵循目前操作系统的实现方式,这种方式对于Java应用而言,当并发量上涨后,有可能出现的现象是启动的大量线程都处于浪费状态。例如一个线程在等待数据库执行结果的返回,如这个数据库执行操作需要花费②秒,那么就意味着这个线程资源被白白占用了2秒,一方面导致了其他的请求只能是放在缓冲队列中等待执行,性能下降;另一方面是造成系统中线程切换频繁,CPU运行队列过长,协程要改变的就是不浪费相对昂贵的原生线程资源。

采用协程后,能做到当线程等待数据库执行结果时,就立即释放此线程资源给其他请求,等到数据库执行结果返回后才继续执行,在Java中目前主要可用于实现协程的框架为Kilim。在使用Kilim执行一项任务时,并不创建Thread,而是改为创建Task,Task相对于Thread而言就轻量级多了。当此Task要做阻塞动作时,可通过Mailbox.get或Task.pause来阻塞当前Task,Kilim会保存Task之后执行需要的对象信息,并释放Task执行所占用的线程资源;当Task的阻塞动作完成或被唤醒时,此时Kilim会重新载入Task所需的对象信息,恢复Task的执行,相当于Kilim来承担了线程的调度以及上下文切换动作。这种方式相对原生Thread方式更为轻量,且能够更好地利用CPU,因此可做到仅启动CPU核数的线程数,以及大量的Task来支撑高并发量, Kilim带来的是线程使用率的提升,但同时由于要在JVM堆中保存Task上下文信息,因此在采用Kilim的情况下要消耗更多的内存。

在一台linux机器2核Intel(R )Xeon(R)CPU E5410 @ 2.33GHz,2GB内存)上执行,传统方式耗时大概为3077ms,而基于Kilim采用协程方式的耗时大概为277ms。可见在这种高并发的情况下,协程方式对性能提升以及支撑更高的并发量可以起到很大的作用。

目前Kilim版本仅为0.7,而且没有商用的实际例子,如打算在实际的系统中使用,还需要慎重。一方面是0.7中基于object.wait/notify机制实现的Scheduler在高压下会出现bug,可自行基于ThreadPoolExecutor进行改造;另一方面Mailbox.get(timout)是基于Timer实现的,由于Timer在增加task到队列时和运行task队列是互斥的(即使是ScheduledThreadPoolExecutor也同样需要锁整个队列),对于大并发的应用而言这里是个潜在的瓶颈。对于Java应用而言,Timer是一个经常用来实现定时任务的类,但Timer的性能在高并发下是一般的,感兴趣的读者可以尝试基于TimerWheel算法“来提升Timer的性能。

现在要在Java应用中使用Kilim来实现协程方式并没有例子中这么简单,因为协程方式要求所有的操作都不阻塞原生线程,这就要求应用中不能使用目前Java里的同步、锁等机制。除了这些之外,还需要解决同步访问数据库、操作文件等问题,这些都必须改为是异步方式或Kilim中的Task暂停的机制。目前Sun JDK 7中也有一个支持协程方式的实现,感兴趣的读者可进一步阅读1,另外基于JVM的Scala的Actor"也可用于在Java中使用协程。

3、用到哪些参数

1.top

输入top命令后即可查看CPU的消耗情况,CPU的信息在TOP视图的上面几行中

对于多个或多核的CPU,上面的显示则会是多个CPU所占用的百分比的总和,因此会出现160% us这样的现象。如须查看每个核的消耗情况,可在进入 top视图后按1,就会按核来显示消耗情况,

默认情况下,TOP视图中显示的为进程的CPU消耗状况,在TOP视图中按shift+h后,可按线程查看CPU的消耗状况,

此时的PID即为线程ID,其后的%CPU表示该线程所消耗的CPU百分比。

2.pidstat

pidstat是SYSSTAT中的工具,如须使用pidstat,请先安装SYSSTAT

输入pidstat 1 2,在console上将会每隔Ⅰ秒输出目前活动进程的CPU消耗状况,共输出2次,结果如图5.5所示:

其中CPU表示的为当前进程所使用到的CPU个数,如须查看某进程中线程的CPU消耗状况,可输入pidstat -p [PID]-t 15这样的方式来查看,执行后的输出

图中的TID即为线程ID,较之top命令方式而言,pidstat 的好处为可查看每个线程的具体CPU利用率的状况(例如%system)。

  • IO问题

文件IO消耗分析

Linux在操作文件时,将数据放入文件缓存区,直到内存不够或系统要释放内存给用户进程使用,因此在查看Linux内存状况时经常会发现可用(free)的物理内存不多,但cached用了很多,这是Linux提升文件IO速度的一种做法。在这样的做法下,如物理空闲内存够用,通常在Linux上只有写文件和第一次读取文件时会产生真正的文件IO

网络IO消耗分析

对于分布式Java应用而言,网络IO的消耗非常值得关注,尤其要注意网卡中断是不是均衡地分配到各CPU的(可通过cat /proc/interrupts查看)。

  1. 怎样算IO问题,IO问题的特征有哪些?
  1. 文件IO消耗

在使用iostat查看IO的消耗情况时,首先要关注的是CPU中的iowait%所占的百分比,当iowait占据了主要的百分比时,就表示要关注IO方面的消耗状况了。

  Java应用造成文件IO消耗严重主要是多个线程需要进行大量内容写入(例如频繁的日志写入)的动作;或磁盘设备本身的处理速度慢;或文件系统慢;或操作的文件本身已经很大造成的。

书中的例子,通过往一个文件中不断地增加内容,文件越来越大,造成写速度慢,最终IOWait值高

从上面可看出 , iowait 占据了很多的CPU,给iostat 的信息来看,主要是写的消耗,并且花在 await的时间上要远大于svctm的时间。至于具体是什么动作导致了iowait,仍然要通过对应用的线程dump来分析,找出其中IO操作相关的动作。对上面的操作进行线程的dump,可看到如下信息:

从上面的线程堆栈中,可看到线程停留在了FileOutputStream.writeBytes这个 Native方法上,这方法所做的动作为将数据写入文件中,也就是所要寻找的IO操作相关的动作。继续跟踪堆栈往上查找,直到查找到系统中的代码,例如上面的例子中则为IOWaitHighDemo.java:59。

使用pidstat则简单很多,直接通过pidstat 找到IO读写量大的线程ID,然后结合上面的线程dump即可找到相应的耗文件IO多的动作。

  1. 我的解决方案是什么

1.文件IO问题

从程序角度而言,造成文件IO消耗严重的原因主要是多个线程在写大量的数据到同一文件,导致文件很快变得很大,从而写入速度越来越慢,并造成各线程激烈争抢文件锁,对于这类情况,常用的调优方法有:

·异步写文件

将写文件的同步动作改为异步动作,避免应用由于写文件慢而性能下降太多,例如写日志,可以使用log4j提供的AsyncAppender。

·批量读写

频繁的读写操作对IO消耗会很严重,批量操作将大幅度提升IO操作的性能。

·限流

频繁读写的另外一个调优方式是限流,从而将文件IO消耗控制到一个能接受的范围,例如通常在记录日志时会采用如下方式:

如以上方式不做任何处理,在大量出现异常时,会出现所有的线程都在执行log.error(..),此时可采取的一个简单策略为统计一段时间内 log.error 的执行频率。当超过这个频率时,一段时间内不再写log,或塞入一个队列后缓慢地写.

·限制文件大小

操作太大的文件也是造成文件IO效率低的一个原因,因此对于每个输出的文件,都应做大小的限制,在超出最大值后可生成一个新的文件,类似 log4j中 RollingFileAppender的maxFileSize属性的作用。

除了以上这些外,还有就是尽可能采用缓冲区等方式来读取文件内容,避免不断与操作系统交互,具体可参见Sun官方的关于Java文件IO优化的文章'7。

2.网络IO问题

从程序角度而言,造成网络IO消耗严重的原因主要是同时需要发送或接收的包太多。对于这类情况,常用的调优方法为进行限流,限流通常是限制发送packet 的频率,从而在网络IO消耗可接受的情况下来发送 packet。

3、用到哪些参数

在 Linux 中,要跟踪线程的文件IO的消耗,主要方法是通过pidstat 来查找。

·pidstat

输入如 pidstat -d -t-p [pid] 1100类似的命令即可查看线程的IO消耗状况,必须在2.6.20以上版本的内核中执行才有效,执行后的效果如图5.8所示:

其中KB_rd/s表示每秒读取的KB数,KB_wr/s表示每秒写入的KB数。

在没有安装pidstat或内核版本为2.6.20以后的版本的情况下,可通过iostat 来查看,但 iostat 只能查看整个系统的文件IO消耗情况,无法跟踪到进程的文件IO消耗状况。

·iostat

直接输入iostat命令,可查看各个设备的IO历史状况:

在上面的几项指标中,其中 Device表示设备卷标名或分区名; tps是每秒的IO请求数,这也是IO消耗情况中值得关注的数字;Blk_read/s是指每秒读的块数量,通常块的大小为512字节;Blk_wrtn/s是指每秒写的块数量; Blk_read是指总共读取的块数量;Blk_wrtn是指总共写入的块数量。

除了上面的方式外,还可通过输入iostat -x xvda 3 5这样的方式来定时采样查看IO的消耗状况,当使用上面的命令方式时,其输出信息会比直接输入iostat多一些:

其中值得关注的主要有: r/s表示每秒读的请求数; wls表示每秒写的请求数;await 表示平均每次IO操作的等待时间,单位为毫秒;avgqu-sz表示等待请求的队列的平均长度;svctm表示平均每次设备执行IO操作的时间;util表示一秒之中有百分之几用于IO操作。

在Linux中可采用sar来分析网络IO的消耗状况

Sar

输入 sar-n FULL 12,执行后以1秒为频率,总共输出两次网络IO的消耗情况,示例如下。上面的信息中输出的主要有三部分,第一部分为网卡上成功接包和发包的信息,其报告的信息中主要有rxpck/s、txpck/s、rxbyt/s、txbyt/s、rxmcst/s;第二部分为网卡上失败的接包和发包的信息,其报告的信息中主要有rxerr/s、txerr/s、rxdrop/s、txdrop/s;第三部分为sockets 上的统计信息,其报告的信息中主要有tolsck、tcpsck、udpsck、rawsck。关于这些数值的具体含义可通过man sar来进行了解,对于Java应用而言,使用的主要为tcpsck和 udpsck。

如须详细跟踪tcp/ip通信过程的信息,则可通过tcpdump'来进行。

由于没办法分析具体每个线程所消耗的网络IO,因此当网络IO消耗高时,对于Java应用而言只能对线程进行dump,查找产生了大量网络IO操作的线程。这些线程的特征是读取或写入网络流,在用Java '实现网络通信时,通常要将对象序列化为字节流,进行发送,或读取字节流,并反序列化为对象。这个过程要消耗JVM堆内存,JVMJVM 堆的内存大小通常是有限的,因此Java应用一般不会造成网络IO消耗严重。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值