Java vm因所有线程忙于String操作而变慢

时间:2013-02-01 16:13:22

标签: java performance tomcat jvm

我遇到了一个非常特殊的问题。我的tomcat以24/7的速度完美地运行在25%左右的CPU上,但是有些日子我的CPU上升到60%并且系统停止运行并且无法恢复。

当我在减速期间进行线程转储时,几乎所有线程都忙于某种String或相关操作。

没有OutOfMemory错误或抛出任何异常,所有请求仍然处理,但响应时间恶化到第n度,即使次秒请求减慢到60秒甚至更长。

我的服务器配置如下:

    Ubuntu 12.04.2 LTS
    Linux 3.2.0-38-virtual #60-Ubuntu SMP x86_64 x86_64 x86_64 GNU/Linux
    java version "1.7.0_13"
    Java(TM) SE Runtime Environment (build 1.7.0_13-b20)
    Java HotSpot(TM) 64-Bit Server VM (build 23.7-b01, mixed mode)
    export JAVA_OPTS='-server
    -Xms18g -Xmx18g
    -XX:MaxPermSize=512m
    -XX:ThreadStackSize=512
    -XX:NewRatio=1
    -XX:SurvivorRatio=4
    -XX:+UseConcMarkSweepGC
    -XX:+UseParNewGC
    -XX:+CMSClassUnloadingEnabled
    -Xloggc:/usr/tomcat/logs/gc.log
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintTenuringDistribution
    -Dcom.sun.management.jmxremote
    -Dcom.sun.management.jmxremote.port=9999
    -Dcom.sun.management.jmxremote.authenticate=false
    -Dcom.sun.management.jmxremote.ssl=false
    -Djava.awt.headless=true'

Click here to download thread dump. I have removed the bulk of the threads and their stackTraces

Click here to download vmstat log

Click here to download gc log

关于这个原因的任何想法? 感谢

9 个答案:

答案 0 :(得分:3)

要尝试查明违规请求,您可以在Tomcat中配置Stuck Thread Detection Valve

  

此阀允许检测需要很长时间处理的请求,这可能表示正在处理它的线程被卡住了。

     

当检测到这样的请求时,其线程的当前堆栈跟踪将写入具有WARN级别的Tomcat日志。

     

卡住线程的ID和名称可通过JMX在stuckThreadIds和stuckThreadNames属性中获得。这些ID可以与标准的Threading JVM MBean(java.lang:type = Threading)一起使用,以检索有关每个卡住线程的其他信息。

答案 1 :(得分:3)

如果CPU利用率低于100%且应用程序已经停止运行,这意味着某些事情阻止了CPU的充分利用。

I / O或过多的上下文切换(例如由锁引起)是通常的罪魁祸首。

你可以在其中一个事件中发布vmsstat 1的输出吗? - 诊断的下一步是消除上下文切换是否是问题。

答案 2 :(得分:3)

这不是内存问题,因为在您的转储中,GC甚至没有忙,并且有足够的可用内存。此外,CPU停留在60%,但如果应用程序忙于计算(GC或其他),它将被卡在100%,如果这是网络攻击,则相同。因此,此问题的根源必须包括一些磁盘IO操作。

众所周知,Tomcat是马车,并且有几个严重的问题。我遇到的一件事是,没有特别的原因,Tomcat突然用无意义的条目淹没了自己的日志文件。这不仅导致磁盘填充到100%,而且还显着减慢了传入的请求。您可以通过查看tomcat日志及其大小来检查这一点。

如果这不是源代码,您应该使用Tomcat check for any strange disk-IO的可用工具并从那里开始。

答案 3 :(得分:3)

我认为你的问题是这个配置决定-XX:PermSize=320m -XX:MaxPermSize=320m不允许你的PemSpace动态改变,当你耗尽它时你会导致死锁 - 记住实习生缓存使用PermSpace。我会尝试将-XX:MaxPermSize=320m更改为-XX:MaxPermSize=512m

答案 4 :(得分:3)

尝试使用以下JVM选项增加代码缓存的最大大小:

-XX:ReservedCodeCacheSize=256m

有关此建议的背景,请参阅my answer to another question

答案 5 :(得分:2)

GC日志中是否有任何异常?看起来你正在运行一个非常大的堆,有一些不寻常的选项,并做了很多String分配的东西。也许你随着时间的推移会受到堆碎片的影响(CMS不紧凑)。还要确保没有进行交换(如果堆太大,可能会发生,因此VM很少访问)

我怀疑这与GC有关,因为显然没有线程受阻。你有没有尝试过更新的JDK?您也可以重试但删除一些不常见的选项-XX:+ CMSScavengeBeforeRemark,因为每个次要JDK版本可能没有太多的测试覆盖率。

另一个怀疑可能是传入请求使用奇怪的字符集(kyrillic或arabic)导致大量的Charset映射开销。 还要检查页面上是否有某个机器人,是否有任何可疑请求进入? 你肯定需要更长的堆栈跟踪来找出字符串操作的根操作。

答案 6 :(得分:1)

您需要使用BTrace诊断方法调用。

写一个像这样的breace脚本:

跟踪com.xx.xx前缀类,它调用String的any方法,并打印调用次数。

@TLS
private static Map<String, Integer> countMap = BTraceUtils.newHashMap();

private static String prefix = "com.xx.xx";// package like com.xx.xx which you want to trace ()

@OnMethod(clazz = "java.lang.String", method = "/.*/") //all method in String
public static void traceMethodInvoke() {
    String str = BTraceUtils.jstackStr();
    for (String currentClass : str.split("\\n")) {
        if (BTraceUtils.Strings.startsWith(currentClass, prefix)) {
            if (!countMap.containsKey(currentClass)) {
                countMap.put(currentClass, 1);
            } else {
                countMap.put(currentClass, countMap.get(currentClass) + 1);
            }
            break;
        }
    }
}

@OnTimer(5000)
public static void print() {
    BTraceUtils.println("========================================");
    for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
        if (entry.getValue() > 100) {// print if cont > 10
            BTraceUtils.println(entry.getValue() + "\t\t" + entry.getKey());
        }
    }
    BTraceUtils.println("===========================================");

}  

结果输出如下:

====================================================
1022                           com.xx.xx.classA#m1
322                            com.xx.xx.classA#m2
2022                           com.xx.xx.classA#m21
422                            com.xx.xx.ccc.classX#m11
522                            com.xx.xx.zz.classS#m44
.........

您可以更改prefix以跟踪另一个包前缀。

根据结果,您可以分析源代码并找出问题。

答案 7 :(得分:1)

通过线程转储扫描查看RUNNABLE线程,有一件事情很突出。您的系统似乎正在处理/尝试同时处理大量请求。除非你有许多核心,否则可能会有很多时间切片。另一方面,我看不到明确的&gt;&gt;证据&lt;&lt;这与GC有关。 (但您没有包含GC日志......)

我建议你看两件事。

  • 查看操作系统的虚拟内存统计信息。灾难性系统减速的一个可能原因是虚拟内存抖动。这是虚拟内存页面的总应用程序需求超过可用物理内存的地方......操作系统花费大量时间在物理内存和交换光盘/页面文件之间交换页面。
  • 查看您获得的请求模式。可能在某些时候,您获得的请求数量/类型只会超出系统的容量。

如果问题是VM颠簸,那么解决方案是减少应用程序内存需求。执行此操作的简单方法是减少 Java堆大小。

如果问题是加载,则更难解决:

  • 您可以尝试增强硬件(或向虚拟机添加更多VCPU)。
  • 您可以尝试将负载分散到服务器的多个实例上。
  • 您可以尝试减少工作线程的数量,以便您的服务器不会尝试一次处理这么多请求。
  • 您可以尝试分析应用程序和/或分析请求统计信息,以查看是否存在可以调整的热点,或者可以关闭的昂贵功能......

最后,您可以看到从CMS切换到并行收集器是否有帮助;请参阅Oracle GC Tuning页面:Available Collectors。但我怀疑这是一个GC问题。

答案 8 :(得分:0)

你要做的第一件事就是找出哪些线程实际上正在消耗CPU。它可以是执行字符串操作的线程,也可以是可以执行GC&amp;的其他VM线程。扫描操作。 The link says how to co-relate the CPU spikes with the thread dump

一旦你能够确定线程的位置,就可以更清楚地知道下一步应该是什么。

希望这有帮助