最近,網站主機常觸發記憶體空間不足告警,調查發現有個 Java 程式默默吃掉 1.2GB 的記憶體,高居工作管理員(Task Manager)記憶體用量榜首。

工作管理員只看得出是 java.exe 不知來歷,使用 wmic process where "Name='java.exe'" get CommandLine 由 Java 啟動參數查出是最近安裝的資安監控軟體。比對安裝日期,與記憶體空間不足發生次數開始暴增的時間相近,推測該軟體涉有重嫌。

有趣的是該軟體所耗用的記憶體比主機上任一個 ASP.NET 網站都多,頓時有乞丐趕廟公的感覺。前陣子才剛發生防毒軟體 CrowdStrike 更新搞掛 Windows,再碰上此案例,不禁讓人感嘆:這個年,頭都變了,壞人沒來倒是先被保鏢揍了一頓。噗~

研究 Java 參數時發現它有設定 "-Xms1024m -Xmx1024m",指定起始及最大 Heap 記憶體大小都是 1GB,可解釋為什麼會吃到 1GB 的記憶體。但普查幾台機器,發現在部分主機該程式只吃了 880MB,低於 "-Xms1024m" 設定的 1GB。

鬼打牆一陣子(鬼月鬼打牆,特別應景),想起這問題之前我就研究過了(只是沒存在大腦 Cache 區)。一般工作管理員說的記憶體使用量,準確名稱是 Working Set - Private,指的是該程序專用的實體記憶體(扣除與其他程式共用部分) 參考

補充 .NET 記憶體管理探索(3) - 為什麼程式爆記憶體,用工作管理員卻看不出來?
一般我們在工作管理員常說的「記憶體用量」,正式術語為「 Memory - Active Private Working Set / 記憶體(使用中的私人工作集)」,嚴謹定義是「程序 Commit 取得且不與其他程序共用的實體記憶體量 (不含 Suspended UWP 程序)」。Working Set 是指 Committed 中已配置實體記憶體的部分(換言之,Committed = Working Set + Pagefile),Private 是指 Committed 中該程序專用不與其他程序共用的部分,Active 則指不含 Suspended UWP 暫停狀態的部分。

意思是,即便 "-Xms1024m -Xmx1024m" 配置了 1GB 的 Heap,它佔用的實體記億體量 (Working Set - Private,工作管理員看到的記憶體數字)需依資料使用狀態而定,若使用到或資料被 GC 回收,數字便會降下來,有可能低於 1GB。而會高於 1GB 則是除了 Heap 記憶體,Java 程式碼本身、靜態資料區、Stack 也要耗用記憶體,故 Heap 1GB 再加上這些空間,大於 1GB 也是合理的。

疑惑有了答案,但沒寫到程式心不爽,決定寫個程式實測驗證。

這個 Java 小程式先宣告 String[1024],再用 for 迴圈填入 1024 個 512K byte[] 大小的字串,過程每 128 個顯示一次記憶體現況,包含 JVM 可用記憶體 Runtime.getRuntime().totalMemory()、 已使用記憶體 Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()、Windows 觀點 Java 程序已取用記憶體 PrivateBytes 及 Java 程序耗用實體記憶體 WorkingSetPrivate。PrivateBytes 及 WorkingSetPrivate 靠呼叫 wmic path Win32_PerfFormattedData_PerfProc_Process where "IDProcess=<PID>" get PrivateBytes,WorkingSetPrivate /value 查詢 Win32_PerfRawData_PerfProc_Process 取得,再次證明呼叫外部程式永遠是最簡單粗暴的手段,噗。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {

    static String formatMemSize(long size) {
        if (size < 1024) {
            return String.format("%,d B", size);
        } else if (size < 1024 * 1024) {
            return String.format("%,d KB", size / 1024);
        } else {
            return String.format("%,d MB", size / (1024 * 1024));
        }
    }
    static String formatMemSize(String size) {
        return formatMemSize(Long.parseLong(size));
    }

    public static void showMemUsage() {
        showMemUsage("");
    }
    
    public static void showMemUsage(String title) {
        // https://learn.microsoft.com/en-us/previous-versions//aa394323(v=vs.85)?redirectedfrom=MSDN
        // wmic path Win32_PerfFormattedData_PerfProc_Process where "IDProcess=<PID>" get PrivateBytes,WorkingSetPrivate /value
        String privBytes = "", workSetPriv = "";
        try {
            Process proc = Runtime.getRuntime().exec(
                "wmic path Win32_PerfFormattedData_PerfProc_Process where \"IDProcess=" + 
                ProcessHandle.current().pid() + 
                "\" get PrivateBytes,WorkingSetPrivate /Value");
            proc.waitFor();
            java.io.InputStream is = proc.getInputStream();
            java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
            String val = s.hasNext() ? s.next() : "";
            Pattern p = Pattern.compile("PrivateBytes=(\\d+).*WorkingSetPrivate=(\\d+)", Pattern.DOTALL);
            Matcher m = p.matcher(val);
            if (m.find()) {
                privBytes = formatMemSize(m.group(1));
                workSetPriv = formatMemSize(m.group(2));
            }
            s.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        String jvmTotal = formatMemSize(Runtime.getRuntime().totalMemory());
        String jvmUsed = formatMemSize(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
        System.out.println(String.format("%10s %10s %10s %14s %s", jvmTotal, jvmUsed, privBytes, workSetPriv, title));
    }
    
    public static void main(String[] args) {
        System.out.println("JVM Total  JVM Used   PrivBytes  WorkingSetPriv");
        System.out.println("========== ========== ========== ==============");
        showMemUsage("Start");

        String[] strings = new String[1024];

        for (int i = 0; i < strings.length; i++) {
            strings[i] = new String(new byte[512 * 1024]);
            if (i % 128 == 0)
                showMemUsage(String.format("String[%d]", i));
        }
        showMemUsage(String.format("String[%d]", strings.length));
        strings = null;
        System.gc();
        showMemUsage("After GC");
    }
}

參考網路文章

  • -Xms:Heap 記憶體的最小值,預設為實體記憶體的1/64,但小於 1G。預設空閒 Heap 大於指定閾值時,JVM 會減少 Heap 到 -Xms 指定的大小。
  • -Xmx:Heap 記憶體的最大值,預設為實體記憶體的1/4。預設當空閒 Heap 小於指定閾值時,JVM 會增加 Heap 到 -Xmx 指定的大小。
    (正式環境可考量將 -Xms 與 -Xmx 設定相同值,以減少 GC 發生機率改善效能。Oracle 官方文件如是說:Oracle recommends setting the minimum heap size (-Xms) equal to the maximum heap size (-Xmx) to minimize garbage collections.)

我的工作機有 64G RAM,故未加參數時,JVM Total 1GB 起跳,而 GC 後,JVM Total 降到 96MB,而 WorkingSetPriv 也降到 106MB:

設定 -Xms4096m -Xmx4096m 後,JVM Total 變成 4GB, 但由於上下限相同。GC 後,JVM Total 維持 4GB,WorkingSetPriv 也停在最高值 810MB 沒有回降:

設定 -Xms128m -Xmx768m,JVM Total 由 128MB 起跳,上升到 657MB 後,下一輪前踩到 OutOfMemoryError: Java heap space 錯誤,無法配置更多記憶體:

結論: -Xms / -Xmx 參數主要影響 Committed Memory (Private Bytes),而其中只有部分會用到實體記憶體,相當於工作管理員看到的記憶體數字(Wokring Set - Private)。Private Bytes 還包含程式碼、靜態資料、Stack,故可能大於 -Xms / -Xmx 指定值;而資料使用狀況決定耗用的實體記憶體大小,如:建立 String() 物件後增加,GC 後減少。故工作管理員看到的記憶體用量不管大於或小於 -Xms / -Xmx 指定值,都是合理的。

The blog post examines a memory issue caused by a Java application on a server, detailing the investigation process, findings, and a practical experiment to understand Java’s memory usage parameters (-Xms and -Xmx). It concludes that memory usage in Task Manager may differ from specified Java heap sizes due to actual memory usage and garbage collection behavior.


Comments

# by Anonymous

所以到底是哪個防毒軟體(歪樓 到底為什麼不用微軟內建的防毒軟體?為什麼平時這麼挺微軟的現在每個都龜縮了…

# by Jeffrey

to Anonymous,資安軟體種類五花八門,可不止防毒,還有防火牆、IDS/IPS、SIEM、DLP、SCM... 裝愈多理論上會更安全,只靠作業系統內建服務是不夠的(一看就是不專業不用心不肯花錢呀),這是企業普遍採取的資安防護策略,就算是勇者欣梅爾一定也會這麼做的...

# by yoyo

裝愈多"理論上"會更安全 再多資安軟體,管制再嚴格,也比不上提升User的資安觀念..

# by Jeffrey

to yoyo, 人往往是資安最大的破口 <= 不能同意更多了。但遺憾沒法以此當藉口在資安軟體規劃上不作為。

# by Anonymous

好吧我誤入叢林了 先不討論為什麼其他作業系統不用裝防毒軟體,而又為什麼一套作業系統出來一定要裝防毒軟體才能享有安全這種被洗腦很奇怪的思維 我服務過的地方關防火牆的關防火牆,最高管理權限任意亂開,使用者得以裝任何程式 這些基本防護全部都關掉,再來說因為很危險,所以要裝防毒軟體,就會很安全? 也要怪台灣對電腦的教育就只有寫程式,根本沒有去教這些東西 因為不會設定權限,就所有程式全部放 C 槽 有時候去深究這些真的會想不透,又例如為何要定時換密碼 大型科技公司的帳號全部都不用,就只有公家機關,和被公司家機關要求的地方(如金融網頁)

Post a comment