Java程序性能优化与崩溃分析:从GC到RCA的深入探讨
[id_[id_[id_1876014156]083907122]310775053]

最近我在研读 RCA(Root Cause Analysis)方面的内容,不经意间发现了 yCrash 这个东西。它有几段关于问题的小代码,写得很具典型性。我们可以稍微看一看,借此了解 Java 应用程序常见的几种崩溃情形。
1.堆空间溢出
OOM 通常由内存泄漏所导致。在 GC 日志中有所体现,通常表现为 GC 的时间逐渐变长,并且每次回收的成效都较为普通。经过 GC 之后,堆内存的实际占用呈现出上升的态势。
下面的代码呈现为死循环的状态,会持续地向 HashMap 中添加数据。因为 myMap 属于 GCRoots 这一范畴,所以它始终无法被释放。基于此,它最终所导致的结果就是 OOM 。
import java.util.HashMap;
public class OOMDemo {
static HashMap
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恤
商品直购链接