Java程序性能优化与崩溃分析:从GC到RCA的深入探讨

[id_[id_[id_1876014156]083907122]310775053]

Java程序性能优化与崩溃分析:从GC到RCA的深入探讨

最近我在研读 RCA(Root Cause Analysis)方面的内容,不经意间发现了 yCrash 这个东西。它有几段关于问题的小代码,写得很具典型性。我们可以稍微看一看,借此了解 Java 应用程序常见的几种崩溃情形。

1.堆空间溢出

OOM 通常由内存泄漏所导致。在 GC 日志中有所体现,通常表现为 GC 的时间逐渐变长,并且每次回收的成效都较为普通。经过 GC 之后,堆内存的实际占用呈现出上升的态势。

下面的代码呈现为死循环的状态,会持续地向 HashMap 中添加数据。因为 myMap 属于 GCRoots 这一范畴,所以它始终无法被释放。基于此,它最终所导致的结果就是 OOM 。

import java.util.HashMap;
public class OOMDemo {
   static HashMap myMap = new HashMap<>();
   public static void start() throws Exception { 
      while (true) {
         myMap.put("key" + counter, "有一个很大的字符串"
               + 3. 有着一长串的“g”。 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
               + "ggggggggggggggggggggggggggggggggggggggggggggggggggggg" 
加上“计数器”;
         ++counter;
      }
   }   
}

2.内存泄漏

内存泄漏和内存溢出是一个道理,不同的是它的语意。

内存溢出属于未知的、超出期望的 OOM 情况。

我们可以使用上面同样的代码达到这个目的。

在现实情况里,内存泄漏一般都很隐蔽。要找到其根本原因,往往需要借助工具。其中,Mat 就是这样的工具。此外,jmap、pmap 等也是常用的工具。

如果你忘记重写对象的 equals 方法,也可能会产生内存泄漏。

//示例泄露:由 xjjdog 在 2022 年创建
import java.util.HashMap;
import java.util.Map;
public class HashMapLeakDemo {
    public static class Key {
        String title;
        public Key(String title) {
            this.title = title;
        }
    }
    public static void main(String[] args) {
        Map map = new HashMap<>();
        map.put(new Key("1"), 1);
        map.put(new Key("2"), 2);
        map.put(new Key("3"), 2);
Integer integer;integer = map.get( ;先获取 map 中的值,将其赋值给 integer ;通过 map 获取到的值赋给 integer ;从 map 中获取到的内容赋给 integer ;让 integer 等于从 map 中获取到的值 ;使 integer 被赋值为 map 中的内容 ;把 map 中的值赋给 integer ;将 map 里的值赋予 integer ;让 integer 获取 map 中的值并进行赋值 ;使 integer 从 map 中获取值并赋值 ;把 map 中的值赋予 integer ;让 integer 被 map 中的值所赋值new Key("2"));
将 integer 进行输出。
    }
}

3.CPU飙升

直接一个死循环,就可以把CPU干死。

public class CPUSpikeDemo {
  public static void start() {
    new CPUSpikerThread().start();
    new CPUSpikerThread().start();
    new CPUSpikerThread().start();
    new CPUSpikerThread().start();
    new CPUSpikerThread().start();
    new CPUSpikerThread().start();
    System.out.println("6 threads launched!");
  }
}
public class CPUSpikerThread extends Thread {
  @Override
  public void run() {
    while (true) {
      // Just looping infinitely
    }
  }
}

获取问题代码通常可以使用下面的方法。

使用 top 命令来查找使用 CPU 最多的某个进程,然后记录它的 pid。通过使用 Shift + P 快捷键,能够按照 CPU 的使用率进行排序。再次使用 top 命令,加上 -H 参数,以查看某个进程中使用 CPU 最多的某个线程,接着记录线程的 ID。使用 printf 函数把十进制的 tid 转换为十六进制。使用 jstack 命令来查看 Java 进程的线程栈。使用 less 命令去查看生成的文件,并且查找刚才转化成的十六进制 tid,从而找到发生问题的线程上下文。

4.线程泄漏

线程资源是很昂贵的。如果你持续不断地创建线程,那么系统资源会很快被消耗殆尽。以下的代码会一直不停地创建线程,倘若同时面临较大的请求压力,多数情况下能够使宿主机死机。

public class ThreadLeakDemo {
   public static void start() {
      while (true) {
         new ForeverThread().start();
      }
   }
}
public class ForeverThread extends Thread {
   @Override
   public void run() {
      让线程永远休眠,这样它们就不会死亡。
      while (true) {
         try {
            反复睡眠 10 分钟。
            Thread.sleep(10 * 60 * 1000);
         } catch (Exception e) {}
      }
   }
}

这属于暴力行为。每个请求创建一个线程,或者创建一个线程池,会带来与之相同的后果。xjjdog 这里有两篇与之相关的线程泄漏文章。

Java 语言中的 OutOfMemoryError 表示无法创建新的本地线程。 它表明在 Java 程序运行过程中,内存不足,无法为新的本地线程分配所需的资源。 这种错误通常会导致程序的运行出现异常或崩溃。 当遇到 java.lang.OutOfMemoryError: unable to create new native thread 错误时,需要检查程序的内存使用情况,可能需要优化代码或增加系统的内存资源。

5.死锁

死锁代码通常不会出现。然而,一旦它发生,情况就会非常严重,相关的业务可能会无法正常运行。

public class DeadLockDemo {
   public static void start() {
      new ThreadA().start();
      new ThreadB().start();
   }
}

public class ThreadA extends Thread {
    @Override 
    public void run() {
        CoolObject.method1();
    }
}

public class ThreadB extends Thread {
    @Override 
     public void run() {
HotObject 执行了 method2 方法。
     } 
}

public class CoolObject {
    public static synchronized void method1() {
       try {
     // Sleep for 10 seconds
     Thread.sleep(10 * 1000);
          } catch (Exception e) {}
          HotObject.method2();
     }
}
      
public class HotObject {
   public static synchronized void method2() {
       try {
          // Sleep for 10 seconds
          Thread.sleep(10 * 1000);
       } catch (Exception e) {}
       CoolObject.method1();
   } 
}

死锁是一种较为严重的情况,jstack 会通过明显的信息来进行提示。同时,对于线程的 dump ,有一些线上分析工具是可以使用的,例如 fastthread ,不过你需要先了解这些情况发生的意义。

6.栈溢出

栈溢出不会致使 JVM 进程死亡,其危害是“相对较小”的。以下是一个简单的用于模拟栈溢出的代码,仅通过递归调用便可实现。

public class StackOverflowDemo {
   public void start() {
      start();
   }
}

可以通过 -Xss 参数来设置虚拟机栈的大小。例如,下面的这个命令便是将栈大小设置为 128K 。

-Xss128K

如果你的应用时常出现这种状况,那么可以尝试将这个值调大。然而,通常情况下这是由程序错误所导致的,所以最好对自己的代码进行检查。

7.Blocked线程

BLOCKED 是一种较为严重的线程状态。后端服务处理时间如果特别长,请求的线程就会进入等待状态。此时,通过 jstack 获取堆栈,能够发现线程处于阻塞状态,并且是阻塞在对锁的获取上(wating to lock)。

public class BlockedAppDemo {
   public static void start() {
      for (int counter = 0; counter < 10; ++counter) {
      // Launch 10 threads.
      new AppThread().start();
      }
   }
}
public class AppThread extends Thread {
   @Override
   public void run() {
获取 AppObject 的某个东西。
   }
}
public class AppObject {
   public static synchronized void getSomething() {
      while (true) {
         try {
         Thread.sleep(10 * 60 * 1000);
  } catch (Exception e) {}
      }
  }
}

这种情况频繁发生时,就表明你的程序相应较慢。若 CPU 资源还有剩余,可尝试增加请求的线程数,像 tomcat 的最大线程数。

End

以下是几段关于 Java 常见故障的小代码分析。大部分故障都在这些场景之中。故障排查往往很耗费精力,并且需要有线上权限。我们想要的是做一些好用的工具,将这些复杂性屏蔽在后面。

程序员专属T恤

商品直购链接

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读