七段小代碼,玩轉(zhuǎn)Java程序常見(jiàn)的崩潰場(chǎng)景!
Java程序是基于GC的,在啟動(dòng)初始,就申請(qǐng)了足量的內(nèi)存池,再加上JIT等編譯器的實(shí)時(shí)優(yōu)化,速度并不比直接用C++語(yǔ)言寫(xiě)的慢。Java語(yǔ)言同時(shí)由于反射和可觀測(cè)等特點(diǎn),再加上JFR這種神器,在發(fā)生問(wèn)題的時(shí)候比二進(jìn)制文件更容易找到它的根源。
最近在看RCA(Root Cause Analysis)的東西,不小心發(fā)現(xiàn)了yCrash這么個(gè)東西。它的幾段問(wèn)題小代碼寫(xiě)的非常典型,我們可以稍微看一下,來(lái)看看Java應(yīng)用程序常見(jiàn)的幾個(gè)崩潰場(chǎng)景。
1. 堆空間溢出
OOM 一般是內(nèi)存泄漏引起的,表現(xiàn)在 GC 日志里,一般情況下就是 GC 的時(shí)間變長(zhǎng)了,而且每次回收的效果都非常一般。GC 后,堆內(nèi)存的實(shí)際占用呈上升趨勢(shì)。
下面的代碼是死循環(huán),持續(xù)向HashMap里塞數(shù)據(jù),由于myMap屬于GCRoots,始終得不到釋放,所以它最終的結(jié)果就是OOM。
import java.util.HashMap;
public class OOMDemo {
static HashMap<Object, Object> myMap = new HashMap<>();
public static void start() throws Exception {
while (true) {
myMap.put("key" + counter, "Large stringgggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ "ggggggggggggggggggggggggggggggggggggggggggggggggggggg"
+ counter);
++counter;
}
}
}
2. 內(nèi)存泄漏
內(nèi)存泄漏和內(nèi)存溢出是一個(gè)道理,不同的是它的語(yǔ)意。
內(nèi)存溢出可能是由于請(qǐng)求量過(guò)高,或者真實(shí)的業(yè)務(wù)需求需要所造成的后果,而內(nèi)存溢出屬于未知的、超出期望的OOM情況。
我們可以使用上面同樣的代碼達(dá)到這個(gè)目的。
在現(xiàn)實(shí)情況中,內(nèi)存泄漏通常都非常的隱蔽,需要借助Mat等工具才能找到根本原因。jmap、pmap等是常用的工具。
比如,如果你忘記了重寫(xiě)對(duì)象的hashCode和equals方法,就會(huì)產(chǎn)生內(nèi)存泄漏。
//leak example : created by 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<Key, Integer> map = new HashMap<>();
map.put(new Key("1"), 1);
map.put(new Key("2"), 2);
map.put(new Key("3"), 2);
Integer integer = map.get(new Key("2"));
System.out.println(integer);
}
}
3. CPU飆升
直接一個(gè)死循環(huán),就可以把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
}
}
}
獲取問(wèn)題代碼通??梢允褂孟旅娴姆椒ǎ?/p>
(1)使用 top 命令,查找到使用 CPU 最多的某個(gè)進(jìn)程,記錄它的 pid。使用 Shift + P 快捷鍵可以按 CPU 的使用率進(jìn)行排序。
(2)再次使用 top 命令,加 -H 參數(shù),查看某個(gè)進(jìn)程中使用 CPU 最多的某個(gè)線程,記錄線程的 ID。
(3)使用 printf 函數(shù),將十進(jìn)制的 tid 轉(zhuǎn)化成十六進(jìn)制。
(4)使用 jstack 命令,查看 Java 進(jìn)程的線程棧。
(5)使用較少 命令查看生成的文件,并查找剛才轉(zhuǎn)化的十六進(jìn)制 tid,找到發(fā)生問(wèn)題的線程上下文。
4. 線程泄漏
線程資源是昂貴的。如果你不停的創(chuàng)建線程,系統(tǒng)資源很快就會(huì)被耗盡。下面的代碼一直不停的創(chuàng)建線程,如果同時(shí)請(qǐng)求壓力比較大的話,多數(shù)能搞死宿主機(jī)。
public class ThreadLeakDemo {
public static void start() {
while (true) {
new ForeverThread().start();
}
}
}
public class ForeverThread extends Thread {
@Override
public void run() {
// Put the thread to sleep forever, so they don't die.
while (true) {
try {
// Sleeping for 10 minutes repeatedly
Thread.sleep(10 * 60 * 1000);
} catch (Exception e) {}
}
}
}
這是暴力啊,這和每個(gè)請(qǐng)求創(chuàng)建一個(gè)線程,或者創(chuàng)建一個(gè)線程池的后果是一樣的。
java.lang.OutOfMemory錯(cuò)誤:無(wú)法創(chuàng)建新的本機(jī)線程
5. 死鎖
死鎖代碼一般不會(huì)發(fā)生,但一旦發(fā)生還是非常嚴(yán)重的,相關(guān)的業(yè)務(wù)可能就跑不動(dòng)了。
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();
}
}
死鎖屬于比較嚴(yán)重的一種情況,jstack 會(huì)以明顯的信息進(jìn)行提示。當(dāng)然,關(guān)于線程的 dump,也有一些線上分析工具可以使用。比如fastthread,但也需要你先了解這些情況發(fā)生的意義。
6. 棧溢出
棧溢出不會(huì)造成 JVM 進(jìn)程死亡,危害“相對(duì)較小”。下面是一個(gè)簡(jiǎn)單的模擬棧溢出的代碼,只需要遞歸調(diào)用就可以了。
public class StackOverflowDemo {
public void start() {
start();
}
}
通過(guò) -Xss 參數(shù)可以設(shè)置虛擬機(jī)棧的大小。比如下面的命令就是設(shè)置棧大小為 128K:
-Xss128K
如果你的應(yīng)用經(jīng)常發(fā)生這種情況,可以試著調(diào)大這個(gè)值。但一般都是因?yàn)槌绦蝈e(cuò)誤引起的,最好檢查一下自己的代碼。
7. 被阻止的線程
BLOCKED是一個(gè)比較嚴(yán)重的線程狀態(tài),當(dāng)后端的服務(wù)處理時(shí)間非常長(zhǎng),請(qǐng)求的線程就會(huì)進(jìn)入等待狀態(tài)。這時(shí)候通過(guò)jstack來(lái)獲取堆棧,就會(huì)發(fā)現(xiàn)線程處于阻塞狀態(tài)。它阻塞在對(duì)鎖的獲取上(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.getSomething();
}
}
public class AppObject {
public static synchronized void getSomething() {
while (true) {
try {
Thread.sleep(10 * 60 * 1000);
} catch (Exception e) {}
}
}
}
一旦頻繁發(fā)生這種情況,就證明你的程序相應(yīng)太慢了。如果CPU資源還有剩余,可以嘗試著增加請(qǐng)求的線程數(shù),比如tomcat的最大線程數(shù)。
結(jié)束
以上就是對(duì)于Java常見(jiàn)故障的幾段小代碼分析,大部分的故障都逃不出這些場(chǎng)景。故障的排查通常都非常耗費(fèi)精力,而且你得有線上權(quán)限。怎樣做一些好用的工具,把這些復(fù)雜性屏蔽在后面,才是我們所想要的。