天天看點

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

摘要:該項目是DAYU平台的資料開發(DLF),資料開發中一個重要的功能就是ETL(資料清洗)。ETL由源端到目的端,中間的業務邏輯一般由使用者自己編寫的SQL模闆實作,velocity是其中涉及的一種模闆語言。

Velocity之OOM

Velocity的基本使用

Velocity模闆語言的基本使用代碼如下:

1. 初始化模闆引擎

2. 擷取模闆檔案

3. 設定變量

4. 輸出

在ETL業務中,Velocity模闆的輸出是使用者的ETL SQL語句集,相當于.sql檔案。這裡官方提供的api需要傳入一個java.io.Writer類的對象用于存儲模闆的生成的SQL語句集。然後,這些語句集會根據我們的業務做SQL語句的拆分,逐個執行。

java.io.Writer類是一個抽象類,在JDK1.8中有多種實作,包括但不僅限于以下幾種:

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

由于雲環境對使用者檔案讀寫建立等權限的安全性要求比較苛刻,是以,我們使用了java.io.StringWriter,其底層是StringBuffer對象,StringBuffer底層是char數組。

簡單模闆Hellovelocity.vm:

#set($iAMVariable = 'good!')
#set($person.password = '123')
Welcome ${name} to velocity.com
today is ${date}
#foreach($one in $list)
    $one
#end
Name:       ${person.name}
Password:   ${person.password}      

HelloVelocity.java

package com.xlf;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class HelloVelocity {

    public static void main(String[] args) {
        // 初始化模闆引擎
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        ve.init();
        // 擷取模闆檔案
        Template template = ve.getTemplate("Hellovelocity.vm");
        VelocityContext ctx = new VelocityContext();

        // 設定變量
        ctx.put("name", "velocity");
        ctx.put("date", (new Date()));

        List temp = new ArrayList();
        temp.add("Hey");
        temp.add("Volecity!");
        ctx.put("list", temp);

        Person person = new Person();
        ctx.put("person", person);
        // 輸出
        StringWriter sw = new StringWriter();
        template.merge(ctx, sw);
        System.out.println(sw.toString());
    }
}      

控制台輸出

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

OOM重制

大模闆檔案BigVelocity.template.vm

(檔案字數超出部落格限制,稍後在附件中給出~~)

模闆檔案本身就379kb不算大,關鍵在于其中定義了一個包含90000多個元素的String數組,數組的每個元素都是”1”,然後寫了79層嵌套循環,循環的每一層都是周遊該String數組;最内層循環調用了一次:

show table;      

這意味着這個模闆将生成包含96372的79次方個SQL語句,其中每一個SQL語句都是:

show table;      

将如此巨大的字元量填充進StringWriter對象裡面,至少需要10的380多次方GB的記憶體空間,這幾乎是不現實的。是以OOM溢出是必然的。

BigVelocity.java

package com.xlf;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.StringWriter;

public class BigVelocity {

    public static void main(String[] args) {
        // 初始化模闆引擎
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        ve.init();
        // 擷取模闆檔案
        Template template = ve.getTemplate("BigVelocity.template.vm");
        VelocityContext ctx = new VelocityContext();
        StringWriter sw = new StringWriter();
        template.merge(ctx, sw);
    }
}      
一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

OOM原因分析

Velocity模闆生成的結果寫入StringWriter對象中,如前面分析,其底層是一個char數組。直接産生OOM的代碼在于java.util.Array.copyOf()函數:

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

StringWriter底層char數組容量極限測試

StringWriterOOMTest.java

package com.xlf;

import java.io.StringWriter;

public class StringWriterOOMTest {
    public static void main(String[] args) {
        System.out.println("The maximum value of Integer is: " + Integer.MAX_VALUE);
        StringWriter sw = new StringWriter();
        int count = 0;
        for (int i = 0; i < 100000; i++) {
            for (int j = 0; j < 100000; j++) {
                sw.write("This will cause OOM\n");
                System.out.println("sw.getBuffer().length(): " + sw.getBuffer().length() + ", count: " + (++count));
            }
        }
    }
}      

Jvm參數設定(參考硬體配置)

環境:JDK8 + Windows10桌上型電腦 + 32GB記憶體 + 1TB SSD + i7-8700

如果你的硬體配置不充分,請勿輕易嘗試!

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

測試結果

StringWriterOOMTest運作時的整個程序記憶體大小在Windows任務管理器中達10300多MB時,程式停止。

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

測試結果分析

char數組元素最大值不會超過Integer.MAX_VALUE,回事非常接近的一個值,我這裡相差20多。網上搜尋了一番,比較靠譜的說法是:确實比Integer.MAX_VALUE小一點,不會等于Integer.MAX_VALUE,是因為char[]對象還有一些别的空間占用,比如對象頭,應該說是這些空間加起來不能超過Integer.MAX_VALUE。如果有讀者感興趣,可以自行探索下别的類型數組的元素個數。我這裡也算是一點拙見,抛磚引玉。

OOM解決方案

原因總結

通過上面一系列重制與分析,我們知道了OOM的根本原因是模闆檔案渲染而成的StringWriter對象過大。具體表現在:

  1. 如果系統沒有足夠大的記憶體空間配置設定給JVM,會導緻OOM,因為這部分記憶體并不是無用記憶體,JVM不能回收
  2. 如果系統有足夠大的記憶體空間配置設定給JVM,char數組中的元素個數在接近于MAX_VALUE會抛出OOM錯誤。

解決方案

前面分析過,出于安全的原因,我們隻能用StringWriter對象去接收模闆渲染結果的輸出。不能用檔案。是以隻能在StringWriter本身去做文章進行改進了:

繼承StringWriter類,重寫其write方法為:

StringWriter sw = new StringWriter() {
    public void write(String str) {
        int length = this.getBuffer().length() + str.length();
        // 限制大小為10MB
        if (length > 10 * 1024 * 1024) {
            this.getBuffer().delete(0, this.getBuffer().length());
            throw new RuntimeException("Velocity template size exceeds limit!");
        }
        this.getBuffer().append(str);
    }
};      

其他代碼保持不變

BigVelocitySolution.java

package com.xlf;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.StringWriter;

public class BigVelocitySolution {

    public static void main(String[] args) {
        // 初始化模闆引擎
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
        ve.init();
        // 擷取模闆檔案
        Template template = ve.getTemplate("BigVelocity.template.vm");
        VelocityContext ctx = new VelocityContext();
        StringWriter sw = new StringWriter() {
            public void write(String str) {
                int length = this.getBuffer().length() + str.length();
                // 限制大小為10MB
                if (length > 10 * 1024 * 1024) {
                    this.getBuffer().delete(0, this.getBuffer().length());
                    throw new RuntimeException("Velocity template size exceeds limit!");
                }
                this.getBuffer().append(str);
            }
        };
        template.merge(ctx, sw);
    }
}      

如果velocity模闆渲染後的sql語句集大小在允許的範圍内,這些語句集會根據我們的業務做SQL語句的拆分,逐句執行。

如何優雅終止線程

在後續逐句執行sql語句的過程中,每一句sql都是調用的周邊服務(DLI,OBS,MySql等)去執行的,結果每次都會傳回給我們的作業開發排程服務(DLF)背景。我們的DLF平台支援及時停止作業的功能,也就是說假如這個作業在排程過程中要執行10000條SQL,我要在中途停止不執行後面的SQL了——這樣的功能是支援的。

在修改上面提到OOM那個bug并通過測試後,測試同學發現我們的作業無法停止下來,換句話說,我們作業所在的java線程無法停止。

線程停止失敗重制

一番debug與代碼深入研讀之後,發現我們項目中确實是調用了對應的線程對象的interrupt方法thread.interrupt();去終止線程的。

那麼為什麼調用了interrupt方法依舊無法終止線程?

TestForInterruptedException.java

package com.xlf;

public class TestForInterruptedException {

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < 10; i++) {
            sb.append("show tables;\n");
        }
        int i = 0;
        for (String str : sb.toString().split("\n")) {
            if (i > 4) {
                Thread.currentThread().interrupt();
                System.out.println(i + " after interrupt");
            }
            System.out.println(str);
            System.out.println(i++);
        }

    }
}      

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

TestForInterruptedException.main函數中做的事情足夠簡單,先産生一個大一點的字元串,拆分成10段小字元串,for循環中逐段列印小字元串;并企圖從第5段(初始段為0)開始,去終止線程。結果發現線程并沒有終止!

這是怎麼回事?為什麼調用了線程的interrupt方法并沒有終止線程?或者說是因為jvm需要一點時間去響應這個方法?其實并非如此,感興趣的同學可以把循環次數加的更大一些,在循環開始幾次就進行interrupt,你會發現結果還是這樣。

經過一番探索,線程終止的方法無外乎兩種:

  • 使用該Thread對象的stop()方法能讓線程馬上停止,但是這種方法太過于暴力,實際上并不會被使用到,詳見JDK1.8的注釋:
    • Deprecated. This method is inherently unsafe. Stopping a thread with Thread.stop causes it to unlock all of the monitors that it has locked (as a natural consequence of the unchecked ThreadDeath exception propagating up the stack). If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to other threads, potentially resulting in arbitrary behavior. Many uses of stop should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), the interrupt method should be used to interrupt the wait…
  • 第二種方法就是上面JDK注釋中提到的設定标志位的做法。這類做法又分為兩種,無論哪一種都需要去被終止的線程本身去“主動”地判斷該标志位的狀态:
  1. 設定一個正常的标志位,比如:boolean類型變量的true/ false, 根據變量的狀态去決定線程是否繼續運作——代碼裡去主動判斷變量狀态。這種一般用在循環中,檢測到相應狀态就break, return或者throw exception。
  2. 使用Thead類的執行個體方法interrupt去終止該thread對象代表的線程。但是interrupt方法本質上也是設定了一個中斷辨別位,而且該标志位一旦被捕獲(讀取),“大部分時候”就會被重置(失效)。是以它并不保證線程一定能夠停止,而且不保證馬上能夠停止,有如下幾類情況:
    1. interrupt方法設定的中斷辨別位後,如果該線程往後的程式執行邏輯中執行了Object類的wait/join/sleep,這3個方法會及時捕獲interrupt标志位,重置并抛出InterruptedException。
    2. 類似于上一點,java.nio.channels包下的InterruptibleChannel類也會去主動捕獲interrupt标志位,即線程處于InterruptibleChannel的I/O阻塞中也會被中斷,之後标志位同樣會被重置,然後channel關閉,抛出java.nio.channels.ClosedByInterruptException;同樣的例子還有java.nio.channels.Selector,詳見JavaDoc
    3. Thread類的執行個體方法isInterrupted()也能去捕獲中斷辨別位并重置辨別位,這個方法用在需要判斷程式終止的地方,可以了解為主動且顯式地去捕獲中斷辨別位。
    4. 值得注意的是:抛出與捕獲InterruptedException并不涉及線程辨別位的捕獲與重置
    5. 怎麼了解我前面說的中斷辨別位一旦被捕獲,“大部分時候”就會被重置?Thread類中有private native boolean isInterrupted(boolean ClearInterrupted);當傳參為false時就能在中斷辨別位被捕獲後不重置。然而一般情況它隻會用于兩個地方
      1. Thread類的static方法:此處會重置中斷辨別位,而且無法指定某個線程對象,隻能是目前線程去判斷
一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?
        1. Thread類的執行個體方法:這個方法也是常用的判斷線程中斷辨別位的方法,而且不會重置辨別位。
一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

小結

要終止線程,目前JDK中可行的做法有:

  1. 自己設定變量去辨別一個線程是否已中斷
  2. 合理利用JDK本身的線程中斷辨別位去判斷線程是否中斷

這兩個做法都需要後續做相應處理比如去break循環,return方法或者抛出異常等等。

線程何時終止?

線程終止原因一般來講有兩種:

  1. 線程執行完他的正常代碼邏輯,自然結束。
  2. 線程執行中抛出Throwable對象且不被顯式捕獲,JVM會終止線程。衆所周知:Throwable類是Exception和Error的父類!

線程異常終止ExplicitlyCatchExceptionAndDoNotThrow.java

package com.xlf;

public class ExplicitlyCatchExceptionAndDoNotThrow {

    public static void main(String[] args) throws Exception {
        boolean flag = true;
        System.out.println("Main started!");
        try {
            throw new InterruptedException();
        } catch (InterruptedException exception) {
            System.out.println("InterruptedException is caught!");
        }
        System.out.println("Main doesn't stop!");
        try {
            throw new Throwable();
        } catch (Throwable throwable) {
            System.out.println("Throwable is caught!");
        }
        System.out.println("Main is still here!");
        if (flag) {
            throw new Exception("Main is dead!");
        }
        System.out.println("You'll never see this!");
    }
}      

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

這個測試驗證了前面關于線程異常終止的結論:

線程執行中抛出Throwable對象且不被顯式捕獲,JVM會終止線程。

優雅手動終止線程

線程執行中需要手動終止,最好的做法就是設定辨別位(可以是interrupt也可以是自己定義的),然後及時捕獲辨別位并抛出異常,在業務邏輯的最後去捕獲異常并做一些收尾的清理動作:比如統計任務執行失敗成功的比例,或者關閉某些流等等。這樣,程式的執行就兼顧到了正常與異常的情況并得到了優雅的處理。

TerminateThreadGracefully.java

package com.xlf;

public class TerminateThreadGracefully {

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < 10; i++) {
            sb.append("show tables;\n");
        }
        int i = 0;
        try {
            for (String str : sb.toString().split("\n")) {
                if (i > 4) {
                    Thread.currentThread().interrupt();
                    if (Thread.currentThread().isInterrupted()) {
                        throw new InterruptedException();
                    }
                    System.out.println(i + " after interrupt");
                }
                System.out.println(str);
                System.out.println(i++);
            }
        } catch (InterruptedException exception) {
            // TODO:此處可能做一些清理工作
            System.out.println(Thread.currentThread().isInterrupted());
        }
        System.out.println("Thread main stops normally!");
    }
}      

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

為何項目中的線程終止失敗?

我們項目中确實是調用了對應的線程對象的interrupt方法thread.interrupt();去終止線程的。

那麼為什麼線程不能相應中斷辨別位并終止呢?

回到我們項目的業務邏輯:

整個job分為模闆讀取、渲染以及SQL執行三個階段,一般而言前兩個階段時間會比較快。在後續逐句執行sql語句的過程中,每一句sql都是調用的周邊服務(DLI,OBS,MySql等)去執行的,結果每次都會傳回給我們的作業開發排程服務(DLF)背景。我們的DLF平台支援及時停止作業的功能,也就是說假如這個作業在排程過程中要執行10000條SQL,我要在中途停止不執行後面的SQL了——這樣的功能是支援的。

是以問題就出在了SQL執行的過程。經過多次debug發現:在SQL執行過程中需要每次都往OBS(華為自研,第三方包)中寫log,該過程不可略去。調用該線程對象的interrupt方法thread.interrupt(),interrupt辨別位最早被OBS底層用到的java.util.concurrent. CountDownLatch類的await()方法捕獲到,重置辨別位并抛出異常,然後在一層層往上抛的時候被轉變成了别的異常類型,而且不能根據最終抛的異常類型去判斷是否是由于我們手動終止job引起的。

對于第三方包OBS根據自己的底層邏輯去處理CountDownLatch抛的異常,這本無可厚非。但是我們的程式終止不了!為了達到終止線程的做法,我在其中加入了一個自定義的标志變量,當調用thread.interrupt()的時候去設定變量的狀态,并在幾個關鍵點比如OBS寫log之後去判斷我的自定義辨別位的狀态,如果狀态改變了就抛出RuntimeException(可以不被捕獲,最小化改動代碼)。并且為了能重用線程池裡的線程對象,在每次job開始的地方去從重置這一自定義辨別位。最終達到了優雅手動終止job的目的。

這一部分的源碼涉及項目細節就不貼出來了,但是相關的邏輯前面已經代碼展示過。

系統記憶體占用較高且不準确

線上程中運作過程中定義的普通的局部變量,非ThreadLocal型,一般而言會随着線程結束而得到回收。我所遇到的現象是上面的那個線程無法停止的bug解決之後,線程停下來了,但是在linux上運作top指令相應程序記憶體占用還是很高。

  1. 首先我用jmap -histo:alive pid指令對jvm進行進行了強制GC,發現此時堆記憶體确實基本上沒用到多少(不關老年帶還是年輕帶都大概是1%左右。)但是top指令看到的占用大概在18% * 7G(linux總記憶體)左右。
  2. 其次,我用了jcmd指令去對對外記憶體進行分析,排斥了堆外記憶體洩露的問題
  3. 然後接下來就是用jstack指令檢視jvm程序的各個線程都是什麼樣的狀态。與job有關的幾個線程全部是waiting on condition狀态(線程結束,線程池将他們挂起的)。
  4. 那麼,現在得到一個初步的結論就是:不管是該jvm程序用到的堆記憶體還是堆外記憶體,都很小(相對于top指令顯式的18% * 8G占用量而言)。是以是否可以猜想:jvm隻是向作業系統申請了這麼多記憶體暫時沒有歸還回去,留待下次線程池有新任務時繼續複用呢?本文最後一部分試驗就圍繞着一點展開。

現象重制

在如下試驗中

設定jvm參數為:

-Xms100m -Xmx200m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps      

其意義在于:

限制jvm初始記憶體為100M,最大堆記憶體為200M。并在jvm發生垃圾回收時及時列印詳細的GC資訊以及時間戳。而我的代碼裡要做的事情就是重制jvm記憶體不夠而不得不發生垃圾回收。同時觀察作業系統層面該java程序的記憶體占用。

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

SystemMemoryOccupiedAndReleaseTest.java

package com.xlf;

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class SystemMemoryOccupiedAndReleaseTest {
    public static void main(String[] args) {

        try {
            System.out.println("start");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3,
            30, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
            new ThreadFactory() {
                public Thread newThread(Runnable r) {
                    return new Thread(r);
                }
            },
            new ThreadPoolExecutor.AbortPolicy());

        try {
            System.out.println("(executor已初始化):");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t1 = new Thread(new Runnable() {
            {
                System.out.println("t1 已經初始化");
            }
            @Override
            public void run() {
                byte[] b = new byte[100 * 1024 * 1024];
                System.out.println("t1配置設定了100M空間給數組");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    throw new RuntimeException("t1 stop");
                }
                System.out.println("t1 stop");
            }
        }, "t1");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t2 = new Thread(new Runnable() {
            {
                System.out.println("t2 已經初始化");
            }
            @Override
            public void run() {
                byte[] b = new byte[100 * 1024 * 1024];
                System.out.println("t2配置設定了100M空間給數組");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    throw new RuntimeException("t2 stop");
                }
                System.out.println("t2 stop");
            }
        }, "t2");


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t3 = new Thread(new Runnable() {
            {
                System.out.println("t3 已經初始化");
            }
            @Override
            public void run() {
                byte[] b = new byte[100 * 1024 * 1024];
                System.out.println("t3配置設定了100M空間給數組");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    throw new RuntimeException("t3 stop");
                }
                System.out.println("t3 stop");
            }
        }, "t3");


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        executor.execute(t1);
        System.out.println("t1 executed!");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();

        }
        executor.execute(t2);
        System.out.println("t2 executed!");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();

        }
        executor.execute(t3);
        System.out.println("t3 executed!");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();

        }

        System.out.println("jmap -histo:live pid by cmd:");
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();

        }
        System.out.println("After jmap!");
        // You may run jmap -heap pid using cmd here
        // executor.shutdown();
    }
}      

上述代碼裡我先定義了三個Thread對象,這三個對象都是在run()方法裡配置設定了100M大小的char[],然後線程休眠(sleep)5秒。然後new一個線程池,并将這三個線程對象依次交給線程池去execute。線程池每兩次execute之間相隔10秒,這是為了給足時間給上一個線程跑完并讓jvm去回收這部分記憶體(200M的最大堆記憶體,一個線程對象要占用100多M,要跑下一個線程必然會發生GC),這樣就能把GC資訊列印下來便于觀察。最後等到三個線程都執行完畢sleep一段時間(大概20秒),讓我有時間手動在cmd執行jmap -histo live pid,該指令會強制觸發FullGC,jmap指令之後你也可以試着執行jmap -heap pid,該指令不會觸發gc,但是可以看下整個jvm堆的占用詳情.

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

在jmp -histo:live執行之前程序在作業系統記憶體占用:

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

執行jmp -histo:live之後

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

執行jmap -heap pid的結果:

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

測試結果分析/win10任務管理器不準确

t1配置設定了100M空間給數組之後,t2結束:

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

記憶體占用:107042K,總可用堆空間大小:166400K

無法給t2配置設定100M,觸發FullGC:

103650K->1036K(98304K)

t2配置設定了100M空間給數組之後,t2結束:

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

記憶體占用:104461K,總可用堆空間大小:166400K

無法給t3配置設定100M,觸發FullGC:

103532K->1037K(123904K)

t3配置設定了100M空間給數組之後,t3結束.

jmap -histo:live pid by cmd:

103565K->997K(123904K)

最後jmap -heap pid結果中堆大小也是123M。

這一過程中,作業系統層面jvm程序記憶體占用不會超過122M,jmap -histo:live pid觸發FullGC之後維持在87M左右(反複幾次試驗都是這個結果)

那麼為什麼jvm的堆棧資訊大小與資料總管對應的不一緻呢?

這個問題在網上搜了一圈,結論如下:

送出記憶體指的是程式要求系統為程式運作的最低大小,如果得不到滿足,就會出現記憶體不足的提示。

工作集記憶體才是程式真正占用的記憶體,而工作集記憶體=可共享記憶體+專用記憶體

可共享記憶體的用處是當你打開更多更大的軟體時,或者進行記憶體整理時,這一部分會被分給其他軟體,是以這一塊算是為程式運作預留下來的記憶體專用記憶體,專用記憶體指的是目前程式運作獨占的記憶體,這一塊和可共享記憶體不一樣,無論目前系統記憶體多麼緊張,這塊專用記憶體是不會主動給其他程式騰出空間的

是以總結一下就是,任務管理器顯示的記憶體,實際上是顯示的程式的專用記憶體而程式真正占用的記憶體,是工作集記憶體

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?
一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

上面兩張圖能對的上:

如下兩張圖“勉強”能對的上:

一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?
一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?

但是和jmap觸發gc之後的堆記憶體123904K還有點差距,這部分差距不大,暫時網上找不到比較靠譜的回答,筆者猜想可能這一部分用的是别的程序的可共享記憶體。我去linux上試了一把,也有這個記憶體算不準的問題。這個問題留待下次填坑吧~~

結論

  1. 線程結束可以是正常結束,也可以是抛出不被catch的Throwable對象而異常終止
  2. 線程結束後,線程所占記憶體空間會在jvm需要空間時進行回收利用,這些空間主要包括:配置設定在堆上的對象,其唯一引用隻存在于該線程中
  3. JVM在進行FullGC後雖然堆空間占用很小,但并不會僅僅向作業系統申請xms大小的記憶體,這部分看似很大的可用記憶體,實際上會在有新的線程任務配置設定時得到利用
  4. JVM程序堆記憶體占用比作業系統層面統計的該程序記憶體占用稍高一些,可能是共享記憶體的原因,這點留待下次填坑!

寫在最後

附上本文中描述的所有代碼以及對應資源檔案,供大家參考學習!也歡迎大家評論提問!

 VelocityExperiment.zip 19.40KB

本文分享自華為雲社群《一個神奇的bug:OOM?優雅終止線程?系統記憶體占用較高?》,原文作者:UnstoppableRock。

點選關注,第一時間了解華為雲新鮮技術~