天天看點

性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解

作者:Hu先生Linux背景開發

介紹

什麼是記憶體洩漏

含義:内層洩露是程式中己動态配置設定的堆記憶體由于某種原因程式未釋放或無法釋放,造成系統記憶體的浪費。(換言之,GC回收不了這些不再被使用的對象,這些對象的生命周期太長) 危害:當應用程式長時間連續運作時,會導緻嚴重的性能下降;OOM;偶爾會耗盡連接配接對象;可能導緻頻繁GC。(大量Full GC發生也可推測系統可能發生記憶體溢出)

什麼是記憶體溢出

含義:内層溢出通俗了解就是記憶體不夠,程式要求的記憶體超出了系統所能配置設定的範圍。 危害:記憶體溢出錯誤會導緻處理資料的任務失敗,甚至會引發平台崩潰等嚴重後果。

什麼是CPU飙升

應用程式CPU使用率高,甚至超過100%

什麼是死鎖

死鎖是指兩個或兩個以上的程序在執行過程中,由于競争資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都将無法推進下去。此時稱系統處于死鎖狀态或系統産生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。

什麼是棧溢出

Java 裡的 StackOverflowError。抛出這個錯誤表明應用程式因為深遞歸導緻棧被耗盡了。每當java程式啟動一個新的線程時,java虛拟機會為他配置設定一個棧,java棧以幀為機關保持線程運作狀态;當線程調用一個方法是,jvm壓入一個新的棧幀到這個線程的棧中,隻要這個方法還沒傳回,這個棧幀就存在。 如果方法的嵌套調用層次太多(如遞歸調用),随着java棧中的幀的增多,最終導緻這個線程的棧中的所有棧幀的大小的總和大于-Xss設定的值,而産生StackOverflowError溢出異常。

記憶體洩漏、記憶體溢出、CPU飙升三者之間的關系

記憶體洩露可能會導緻記憶體溢出。 記憶體溢出會抛出異常,記憶體洩露不會抛出異常,大多數時候程式看起來是正常運作的。 記憶體洩露的程式,JVM頻繁進行FullGC嘗試釋放記憶體空間,進而會導緻CPU飙升 記憶體洩露過多,造成可回收記憶體不足,程式申請記憶體失敗,結果就是記憶體溢出。

基本指令

首先了解各個基本指令、工具的使用,用它們去分析JVM參數,後文案例均是基于以下指令/工具解決。

top free df jps

# 先掌控全局,分别擷取執行中的程式程序情況、顯示記憶體的使用情況、檢視磁盤剩餘空間
top free df 

# 擷取java程序的PID
jps 或者ps -ef|grep java           

jinfo

可以列印一些目前jvm的各種參數,比如jvm的一些啟動參數,jvm中的一些屬性k-v等。

jinfo [option] pid           

jmap(記憶體溢出解決方案)

這個指令可以檢視JVM記憶體的一些相關資料

  1. 堆曆史:可以看到目前JVM中所有已加載内的類建立對象的數量,占用記憶體等,可以導入檔案中檢視;
jmap -histo[:live] <pid> [ > ./xx.log]           
  1. 堆資訊:可以檢視java程式新生代和老年代的占比即使用情況。
jmap -heap <pid>           
  1. 堆轉儲:可以dump堆日志(儲存堆現場),再使用visualVM檢視jmap生成的堆轉儲快照。
jmap -dump:live,format=b,file=heap.hprof <pid>           

3.1 HeapDump檔案 HeapDump 檔案是一個二進制檔案,它儲存了某一時刻JVM堆中對象使用情況(指定時刻Java堆棧的快照),是一種鏡像檔案。jhat可分析heapdump檔案,但是jhat指令在JDK9、JDK10中已經被删除,官方建議用VisualVM代替。 自動導出dump檔案:通過JVM參數HeapDumpOnOutOfMemoryError,可以讓JVM在出現記憶體溢出時候Dump出目前的記憶體轉儲快照。

# 在IDE中VM option中添加了以下環境變量,程式OOM後生成檔案,字尾名為hprof
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./           

3.2 VisualVM工具

VisualVM 能夠監控線程狀态、記憶體使用情況、CPU 使用情況

jstack(cpu占用高解決方案)

這個指令可以檢視線程的堆棧資訊,定位到簡單的死鎖,常用的是通過jstack定位CPU高的問題,具體步驟是:

  1. 檢視目前占用cpu最高的程序pid(COMMAND列);
top           
  1. 擷取目前程序中所有線程占CPU的情況(也可top -p再按 H);
top -Hp <pid>           
  1. 将占用最高的tid轉換為16進制
printf "%x\n" <tid>;           
  1. 檢視占用最高的線程的堆棧狀态。通過這個流程可以直接定位到哪個線程正在執行占用了大量的cpu。其中A10 就是過濾到關鍵詞之後(A:after)10行資訊。
jstack <pid> | grep -A10 <16進制tid>           
  1. 前面的步驟已經擷取了堆棧資訊,我們也可以儲存線程棧現場到指定檔案裡分析。
jstack <pid> > jstack.log            

jstat(FullGC頻繁解決方案)

這個指令可以檢視堆的各個部分的詳細的使用情況,可以通過jstat --help檢視幫助;

jstat -gc <pid> [1000 10]           

檢視gc情況,每1秒列印一次總共列印10次(可選),可以檢視各個帶的使用總大小和使用大小對于jvm的優化就是要去優化它的FullGC次數,FullGC越少越好,最好控制在FullGC幾個小時甚至幾天一次,具體看業務的情況。

jstat參數說明:

S0C:第一個幸存區的大小(From Survivor區),以下幾個容量的機關都是KB 
S1C:第二個幸存區的大小 (To Survivor區)
S0U:第一個幸存區的使用大小
S1U:第二個幸存區的使用大小 
EC:伊甸園區的大小 (Eden區)
EU:伊甸園區的使用大小
OC:老年代大小 
OU:老年代使用大小 
MC:方法區大小(元空間)
MU:方法區使用大小 
CCSC:壓縮類空間大小 
CCSU:壓縮類空間使用大小 
YGC:年輕代垃圾回收次數 
YGCT:年輕代垃圾回收消耗時間,機關s 
FGC:老年代垃圾回收次數 
FGCT:老年代垃圾回收消耗時間,機關s 
GCT:垃圾回收消耗總時間,機關s           

根據jstat檢視出來的gc情況,我們可能需要以下幾個主要名額:

各記憶體區域大小是否合理;
觀察Eden區的對象增長,如每秒有多少對象建立;
每次YoungGC後有多少對象存活下來、有多少對象進入了老年代;
YoungGC的耗時;
FullGC觸發頻率及耗時;           

GC分析

性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解
  • Minor GC/Young GC: 指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
  • Major GC/Full GC: 一般會回收老年代 ,年輕代,方法區的垃圾,Major GC的速度一般會比Minor GC的慢 10倍以上。

大量的對象在Eden區配置設定,YoungGC之後存活的對象經過S0、S1,大對象與長期存活的對象可能會到Old區。Eden與Survivor區預設8:1:1。

  • 觸發YoungGC時機: Eden區域滿了
  • 觸發FullGC時機:

1.System.gc() 顯式觸發Full GC

2.老年代空間不足,晉升到老年代的對象大小大于老年代的可用記憶體。進入到老年代有多種情況:

1)Survivor區的對象滿足晉升到老年代的條件,即對象年齡達到了MaxTenuringThreshold,這是一般情況;

2)根據對象動态年齡判斷機制:在YoungGC後判斷,Survivor區中年齡 1 到 N 的對象大小是否超過 Survivor 的 50% ,這會讓大于等于年齡 N 的對象放入老年代(-XX:TargetSurvivorRatio),如果此時老年代沒有足夠的空間來放置這些對象也會引起Full GC;

3)堆中産生大對象超過門檻值(-XX:PretenureSizeThreshold):很長的字元串或者數組在被建立後會直接進入老年代

3.元空間空間不足(-XX:MetaspaceSize)

4.老年代空間配置設定擔保失敗:在YoungGC前判斷,YoungGC後晉升到Old區的曆史平均大小是否大于本次Old區剩餘空間大小(XX:-HandlePromotionFailure)

更多C++背景開發技術點知識内容包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒體,音視訊開發,Linux核心,TCP/IP,協程,DPDK多個進階知識點。

C/C++背景開發架構師免費學習位址:C/C++Linux鏈嶅姟鍣ㄥ紑鍙�/鍚庡彴鏋舵瀯甯堛€愰浂澹版暀鑲層€�-瀛︿範瑙嗛鏁欑▼-鑵捐璇懼爞

【文章福利】另外還整理一些C++背景開發架構師 相關學習資料,面試題,教學視訊,以及學習路線圖,免費分享有需要的可以點選 「連結」 免費領取

性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解

Arthas

官方文檔:https://arthas.aliyun.com

使用略

  • 指令合并

下文部分指令是多個指令的合并,與拆開輸入等效

# 例如,當程式名為StaticTest時
java -jar arthas-boot.jar  `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'`
jstat -gc `jps|grep StaticTest |grep -v grep|awk '{print $1}'` 500 1000
jvisualvm --openpid `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'`           

記憶體洩漏案例分析

介紹

JAVA在記憶體管理上有着獨一無二的優勢,它取消了指針,引入垃圾回收機制,由垃圾收集器(GC)來自動管理記憶體回收;GC隐式地負責配置設定和釋放記憶體,是以能夠處理大多數記憶體洩漏問題。雖然GC有效地處理了相當一部分記憶體,但它不能保證對記憶體洩漏提供萬無一失的解決方案。即使在認真的開發人員的應用程式中,記憶體洩漏仍然可能悄悄發生。是以我們有必要了解記憶體洩漏的潛在原因是什麼,如何在運作時識别它們,以及如何在應用程式中處理它們。

案例一、通過靜态字段的記憶體洩漏

第一種可能導緻潛在記憶體洩漏的情況是大量使用靜态變量。靜态字段的生命周期與正在運作的應用程式一緻。

import java.util.ArrayList;
import java.util.List;

public class StaticTest {
    public static List<Double> list = new ArrayList<>(); //靜态集合

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        System.out.println("Debug Point 2");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000);
        System.out.println("Debug Point 1");
        new StaticTest().populateList();
        System.out.println("Debug Point 3");
        Thread.sleep(10000);
        System.gc(); //有修改,在此處顯示觸發Full GC
        Thread.sleep(Integer.MAX_VALUE);
    }
}           

如果我們分析這個程式執行期間的堆記憶體,那麼我們将看到在調試點1和2之間,堆記憶體如預期的那樣增加了。但是當我們在調試點3離開populateList()方法時,堆記憶體還沒有被垃圾回收。

性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解

​然而,如果我們在上面的程式删除了關鍵字static,那麼它将給記憶體使用帶來巨大的變化。

性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解

靜态集合類持有短生命周期對象的引用,盡管短生命周期的對象不再使用,但是因為長生命周期對象持有它的引用而導緻不能被回收。

#檢視jvm預設具體參數
java -XX:+PrintCommandLineFlags -version 
java -XX:+PrintGCDetails -version           

比較關鍵字static帶來堆空間大小回收的差異,兩者差距接近300M;10000000個Double對象沒有被回收,再根據對齊填充8的倍數,反推出來一個Double包裝對象占用32位元組的空間。

結論:我們需要密切關注靜态變量的使用。靜态的集合或大型對象在整個應用程式的生命周期中都被保留在記憶體中,這些可在其他地方使用的重要記憶體空間就被浪費掉了。

建議:盡量減少靜态變量的使用;單例對象懶加載,需要用對象的時候再建立,而不是初始化時就建立好了對象。

案例一變種

import java.util.ArrayList;
import java.util.List;

/**
 * JVM參數預設
 * @author luke
 * @date 2022/11/11
 */
public class StaticTest {
    public static List<Integer> list = new ArrayList<>(100000000);
    public void populateList() throws InterruptedException {
        for (int i = 1; i <= 100000000; i++) {
            list.add(i);
            if(i % 100000 == 0){
                Thread.sleep(1000);
                //System.out.println(list.size());
            }
        }
        System.out.println("running......");
    }
    public static void main(String[] args) throws InterruptedException {
        System.out.println("before......");
        new StaticTest().populateList();
        System.out.println("after......");
    }
}
// 代碼最終因記憶體洩露,回收不了可用空間而OOM。
// Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded           

先使用jstat指令統計垃圾回收,間隔時間500毫秒列印一次。根據一個Integer對象占用16位元組,每1秒鐘向list添加100000個整數,16*100000位元組大約是1.5M,與統計圖eden區平均每秒産生對象的大小接近。 再觀察jvisualvm中堆記憶體的曲線圖,每分鐘平均生産100M對象,資料相符合。

使用指令如下:

jstat -gc `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'` 500 1000
jvisualvm --openpid `ps -ef|grep StaticTest |grep -v grep|awk '{print $2}'`           
性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解

案例二、連接配接資源未關閉

import java.io.File;
import java.io.IOException;

/**
 * @author luke
 * @date 2022/10/27
 */
public class FileTest {
    public static void main(String[] args) throws IOException {
        File f = new File("C:\\Users\\lzyxx\\Desktop\\a.txt");
        System.out.println(f.exists());
        System.out.println(f.isDirectory());
    }
}           

各種連接配接,如資料庫連接配接、網絡連接配接和IO連接配接等,如果不顯性地關閉連接配接資源,将會造成大量的對象無法被回收,進而引起記憶體洩漏。

案例三、equals()和hashCode()方法使用不當

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 線程池通過submit方式送出任務,會把Runnable封裝成FutureTask。
 * 直接導緻了Runnable重寫的toString方法在afterExecute統計的時候沒有起到我們想要的作用(重寫toString以用于統計任務數),
 * 最終導緻幾乎每一個任務(除非hashCode相同)就按照一類任務進行統計。是以這個metricsMap會越來越大,調用metrics接口的時候,會把該map轉成一個字元傳回。
 * 改成execute方式送出任務即可
 */
public class GCTest {
    /**
     * 統計各類任務已經執行的數量, 此處為了簡化代碼,隻用map來代替metrics統計
     */
    private static final Map<String, AtomicInteger> metricsMap = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) {
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);
                metricsMap.compute(r.toString(), (s, atomicInteger) -> new AtomicInteger(atomicInteger == null ? 1 : atomicInteger.incrementAndGet()));
            }
        };
        /**
         * 線程池執行兩類任務
         */
        for (int i = 0; i < 500; i++) {
            executor.submit(new SimpleRunnable()); // 錯誤方式
            executor.submit(new SimpleRunnable2());
//            executor.execute(new SimpleRunnable()); // 正确方式
//            executor.execute(new SimpleRunnable2());
        }
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.DAYS);
        System.out.println(metricsMap);
    }
    static class SimpleRunnable implements Runnable{
        @Override
        public void run() {}
        @Override
        public String toString(){
            return this.getClass().getSimpleName();
        }
    }

    static class SimpleRunnable2 implements Runnable{
        @Override
        public void run() {}
        @Override
        public String toString(){
            return this.getClass().getSimpleName();
        }
    }
}           

案例四、ThreadLocal的錯誤使用

如果任何類建立了ThreadLocal變量,但沒有顯式删除它,那麼即使在web應用程式停止後,該對象的副本也将保留在工作線程中,進而防止對象被垃圾收集。

案例:記憶體的最大大小為1m,while循環每隔100ms申請30kb大小的空間

import java.lang.reflect.Field;

/**
 * jvm運作參數 -Xmx1m -Xms1m -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
 *
 * 使用以下指令觀察:jstat -gc `ps -ef|grep MemoryLeakExample |grep -v grep|awk '{print $2}'` 1000 1000
 */
public class MemoryLeakExample{

    public static int i = 0;

    public static void main(String[] args) throws Exception{
        while (true){
            ThreadLocal<Object> threadLocal = new ThreadLocal<>();
            try {
                objectThreadLocal.set(new byte[10 * 1024]);
                printEntriesSize();
                Thread.sleep(100);
                i++;
            }catch (Throwable e){
                System.out.println(i);
                throw e;
            }finally {
                //threadLocal.remove(); // 正确使用
            }
        }
    }

    /**
     * 列印ThreadLocal.entry的個數
     */
    public static void printEntriesSize() throws NoSuchFieldException, IllegalAccessException{
        Thread thread = Thread.currentThread();
        Class<? extends Thread> aClass = thread.getClass();
        Field threadLocals = aClass.getDeclaredField("threadLocals");
        threadLocals.setAccessible(true);
        Object threadLocalMap  = threadLocals.get(thread);
        Class<?> tlmClass = threadLocalMap.getClass();
        Field entriesSize = tlmClass.getDeclaredField("size");
        entriesSize.setAccessible(true);
        System.out.println(entriesSize.get(threadLocalMap));
    }
}           

為什麼程式沒有因可用空間越來越少而oom,ThreadLocal.entry的個數會周期性的由少變多? 解釋:ThreadLocalMap底層使用數組來儲存元素,利用線性探測法解決哈希沖突,但是調用ThreadLocal#set,周遊Entry數組過程中會清理key為null的value,盡量保證不出現記憶體洩漏的情況。

  • 如何預防?
  1. 當我們不再使用ThreadLocal時,記得清理它們。ThreadLocals提供了remove()方法,該方法将删除此變量的目前線程值。
  2. 不要使用ThreadLocal#set(null)以清除該值。它實際上不會清除該值,而是會查找與目前線程關聯的Map,并将鍵值對分别設定為目前線程和null。
  3. 最好将ThreadLocal 視為需要在finally塊中關閉的資源。
try {
    threadLocal.set(System.nanoTime());
    //... further processing
} finally {
    threadLocal.remove();
}           

案例五、緩存洩漏

記憶體洩漏的另一個常見來源是緩存,一旦你把對象引用放入到緩存中,就很容易遺忘。

  • 如何預防?

使用WeakHashMap 緩存對象,這個map 的特點是當除了自身有對key的引用外,此key沒有其他引用那麼此map會自動丢棄此值

案例六、 内部類持有外部類

嵌套類分為兩類:非靜态類和靜态類。非靜态嵌套類稱為内部類。聲明為靜态的嵌套類稱為靜态内部類。 按照嵌嵌套類的文法限定,非靜态内部類(InnerClass)可以通路其封閉類(OuterClass)的成員,即使這些成員是私有的。而靜态内部類沒有權限通路OuterClass的成員(當然靜态類成員除外)。

預設情況下,每個非靜态内部類(InnerClass)都有對其包含類(OuterClass)的隐式引用。如果我們在應用程式中使用這個内部類對象,那麼即使在我們的包含類對象超出範圍之後,它也不會被垃圾收集。

  • 如何預防?

如果内部類不需要通路包含的類成員,請考慮将其轉換為靜态類。

public class OuterClass {
    class InnerClass {
    }
    static class StaticClass {
    }
}           

cpu占用高案例分析

CPU占用飙升甚至超過100%的原因分析:

  1. 記憶體消耗過大,導緻Full GC次數過多

多個線程的CPU都超過了100%,通過jstack指令可以看到這些線程主要是垃圾回收線程(VM Thread); 通過jstat指令監控GC情況,可以看到Full GC次數非常多,并且次數在不斷增加。

  1. 代碼中有大量消耗CPU的操作,導緻CPU過高,系統運作緩慢

例如某些複雜算法,甚至算法BUG,無限循環遞歸等等。jstack指令可直接定位到代碼行。

  1. 由于鎖使用不當,導緻死鎖

死鎖不會直接導緻 cpu 資源占用過高,synchronize 和 AQS中鎖的設計是線程擷取鎖失敗時,會主動挂起線程,而不會自旋循環檢測鎖是否被釋放。 如果因為死鎖,阻塞線程越來越多,記憶體占用也越來越高且無法釋放,導緻不停的 gc,會造成CPU占用飙升。

  1. 線程由于某種原因而進入TIMED_WAITING、WAITING狀态
性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解

使用 synchronized 會讓等待鎖的線程處于 Blocked 狀态;

使用 AQS 相關的鎖則會讓等待鎖的線程處于 TIMED_WAITING、 WAITING 狀态,因為底層基于 LockSupport;

記憶體溢出案例分析

一般來說記憶體溢出主要分為以下幾類:

  • 堆溢出(java.lang.OutOfMemoryError: Java heap space) 最常見最複雜情況
  • 棧深度不夠( java.lang.StackOverflowError) 需關注配置項 -Xss大小
  • 棧線程數不夠(java.lang.OutOfMemoryError: unable to create new native thread)
  • 元空間溢出(java.lang.OutOfMemoryError: Metaspace) 需關注配置項 -XX:MaxMetaspaceSize大小、jstat 名額參數 MC、MU 如果發現元空間大小是持續上漲的,則需要檢查代碼是否存在大量的反射類加載、動态代理生成的類加載等導緻。可以通過-XX:+TraceClassLoading -XX:+TraceClassUnloading記錄下類的加載和解除安裝情況,反推具體問題代碼。
性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出詳解

原文連結:性能優化-記憶體洩漏、記憶體溢出、cpu占用高、死鎖、棧溢出、FullGC頻繁檢測手段-總結與分享 - Luke! - 部落格園

繼續閱讀