<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          GraalVM Native Image 原生內存跟蹤【譯】

          共 12447字,需瀏覽 25分鐘

           ·

          2024-05-29 08:30

          GraalVM Native Image 最近新增了對原生內存跟蹤(Native Memory Tracking, NMT)的初步支持。這項功能目前可在早期訪問構建中使用,并將在未來的 GraalVM JDK 23 版本中提供。NMT 的加入將允許 Native Image 的用戶更好地理解他們的應用程序是如何使用堆外內存的。

          一、背景

          “原生內存”這個術語有時與“堆外內存”或“非管理內存”交替使用。當 Java 應用程序在 JVM 上運行時,“原生內存”指的是 Java 堆上未分配的任何內存。由于 JVM 是用 C++ 實現的,這包括 JVM 用于其內部操作的內存,以及使用 Unsafe#allocateMemory(long) 或從外部庫請求的內存。

          相比之下,當相同的應用程序作為 Native Image 可執(zhí)行文件部署時,VM(SubstrateVM)是用 Java 實現的,而不是 C++。與 Hotspot 不同,這意味著 VM 內部使用的大部分內存是與應用程序級別的請求內存一起在“Java 堆”上分配的。然而,SubstrateVM 中仍然有地方使用了不在“Java 堆”上的非管理內存。Native Image 中 NMT 的目標是跟蹤這些使用情況。

          圖 1 展示了一個高層次的說明,說明了在哪里進行分配。

          二、用例

          NMT 之所以有用,一個主要原因是它暴露了 Java 堆轉儲無法看到的內存信息。例如,假設你的應用程序的常駐集大小(RSS)遠高于預期值。你生成并檢查了堆轉儲,但沒有發(fā)現任何可以解釋高 RSS 的內容。這可能表明原生內存是罪魁禍首。但是,是什么導致了意外的原生內存使用?對你的應用程序代碼的更改是否影響了原生內存使用?或者 GraalVM 的 SubstrateVM 運行時組件的更改?沒有 NMT,很難確定發(fā)生了什么。

          由于 Native Image 的運行時組件 SubstrateVM 在“Java 堆”上使用內存,VM 級別的大部分內存使用可以通過堆轉儲來暴露。這使得 Native Image 中的堆轉儲在某些方面比普通 Java 更具信息量。然而,正如前面提到的,SubstrateVM 仍然為其大量操作使用非管理內存。一些例子包括垃圾回收和 JDK Flight Recorder。

          相比之下,Native Image 構建時間較慢,導致大多數開發(fā)人員在常規(guī) Java 中進行大部分開發(fā)和測試。但是,如果切換到原生模式時性能有顯著差異怎么辦?NMT 在比較在常規(guī) Java 中運行應用程序和 Native Image 之間的差異時可能特別有用。這是因為 Native Image 中的底層 VM 與 Hotspot 大不相同,即使 Java 應用程序代碼相同,也可能導致完全不同的資源消耗。NMT 可以幫助你在這種情況下了解發(fā)生了什么,以便你可以為原生執(zhí)行優(yōu)化你的代碼。

          三、在 Native Image 中使用

          你可以通過在構建時包含 --enable-monitoring=nmt 功能來在你的 Native Image 可執(zhí)行文件中使用 NMT。默認情況下,NMT 不包含在鏡像構建中。一旦在構建時包含了 NMT,它將在運行時始終啟用。

          $ native-image --enable-monitoring=nmt MyApp

          在運行時添加 -XX:+PrintNMTStatistics 將導致在程序關閉時將 NMT 報告轉儲到標準輸出。

          $ ./myapp -XX:+PrintNMTStatistics

          轉儲的報告如下所示:

          Native memory tracking
            Peak total used memory: 13632010 bytes
            Total alive allocations at peak usage: 1414
            Total used memory: 2125896 bytes
            Total alive allocations: 48
            Compiler peak used memory: 0 bytes
            Compiler alive allocations at peak: 0
            Compiler currently used memory: 0 bytes
            Compiler currently alive allocations: 0
            Code peak used memory: 0 bytes
            Code alive allocations at peak: 0
            Code currently used memory: 0 bytes
            Code currently alive allocations: 0
            GC peak used memory: 0 bytes
            GC alive allocations at peak: 0
            GC currently used memory: 0 bytes
            GC currently alive allocations: 0
            Heap Dump peak used memory: 0 bytes
            Heap Dump alive allocations at peak: 0
            Heap Dump currently used memory: 0 bytes
            Heap Dump currently alive allocations: 0
            JFR peak used memory: 11295082 bytes
            JFR alive allocations at peak: 1327
            JFR currently used memory: 24720 bytes
            JFR currently alive allocations: 3
            JNI peak used memory: 80 bytes
            JNI alive allocations at peak: 1
            JNI currently used memory: 80 bytes
            JNI currently alive allocations: 1
            jvmstat peak used memory: 0 bytes
            jvmstat alive allocations at peak: 0
            jvmstat currently used memory: 0 bytes
            jvmstat currently alive allocations: 0
            Native Memory Tracking peak used memory: 23584 bytes
            Native Memory Tracking alive allocations at peak: 1474
            Native Memory Tracking currently used memory: 768 bytes
            Native Memory Tracking currently alive allocations: 48
            PGO peak used memory: 0 bytes
            PGO alive allocations at peak: 0
            PGO currently used memory: 0 bytes
            PGO currently alive allocations: 0
            Threading peak used memory: 3960 bytes
            Threading alive allocations at peak: 30
            Threading currently used memory: 1088 bytes
            Threading currently alive allocations: 8
            Unsafe peak used memory: 2310280 bytes
            Unsafe alive allocations at peak: 57
            Unsafe currently used memory: 2099240 bytes
            Unsafe currently alive allocations: 36
            Internal peak used memory: 1024 bytes
            Internal alive allocations at peak: 1
            Internal currently used memory: 0 bytes
            Internal currently alive allocations: 0

          報告顯示了按類別組織的瞬時內存使用情況、瞬時計數、峰值使用情況和峰值計數。類別與 Hotspot 中的類別不完全相同。這是因為 Hotspot 中有許多類別不適用于 SubstrateVM。還有一些類別在 SubstrateVM 中有用,但在 Hotspot 中不存在。

          此報告是單個瞬間的快照,因此僅反映生成時的原生內存使用情況。這就是為什么建議通過 JFR 消費 NMT 數據的方式,如下所述。

          四、NMT JDK Flight Recorder (JFR) 事件

          Native Image 支持 OpenJDK JFR 事件 jdk.NativeMemoryUsagejdk.NativeMemoryUsageTotaljdk.NativeMemoryUsage 按類別公開使用情況,而 jdk.NativeMemoryUsageTotal 公開總體使用情況。兩個事件都不包括計數信息。

          還有兩個特定的 Native Image JFR 事件可以訪問:jdk.NativeMemoryUsagePeakjdk.NativeMemoryUsageTotalPeak。這些自定義事件已被創(chuàng)建,以公開 JFR 事件中未公開的峰值使用數據,這些事件是從 OpenJDK 移植過來的。在常規(guī) Java 中,用戶將使用 jcmd 來獲取峰值使用信息。然而,jcmd 在 Native Image 中不受支持。

          4.1 啟用 JFR 和 NMT

          為了訪問這些公開 NMT 數據的 JFR 事件,只需在構建時包含 JFR 和 NMT:

          $ native-image --enable-monitoring=nmt,jfr MyApp

          然后在啟動應用程序時啟用 JFR。如果包含在構建時,NMT 將在運行時自動啟用。

          $ ./myapp -XX:StartFlightRecording=filename=recording.jfr

          4.2 示例

          以下是使用 jfr 工具查看 jdk.NativeMemoryUsage 內容的示例。

          $ jfr print --events jdk.NativeMemoryUsage recording.jfr 
          jdk.NativeMemoryUsage {
            startTime = 15:47:33.392 (2024-04-25)
            type = "JFR"
            reserved = 10.1 MB
            committed = 10.1 MB
          }
          jdk.NativeMemoryUsage {
            startTime = 15:47:33.392 (2024-04-25)
            type = "Threading"
            reserved = 272 bytes
            committed = 272 bytes
          }
          ...

          圖 2 是使用 JDK Mission Control 查看 jdk.NativeMemoryUsageTotal 內容的示例。下面是使用該工具查看的一個示例。類似于NMT報告轉儲,值以Bytes.jdk.NativeMemoryUsagePeakjfr顯示。

          $jfr print --events jdk.NativeMemoryUsagePeak recording.jfr 
          jdk.NativeMemoryUsagePeak {
            startTime = 15:47:35.221 (2024-04-25)
            type = "Threading"
            peakReserved = 424
            peakCommitted = 424
            countAtPeak = 4
            eventThread = "JFR Shutdown Hook" (javaThreadId = 64)
          }
          jdk.NativeMemoryUsagePeak {
            startTime = 15:47:35.221 (2024-04-25)
            type = "Unsafe"
            peakReserved = 14336
            peakCommitted = 14336
            countAtPeak = 2
            eventThread = "JFR Shutdown Hook" (javaThreadId = 64)
          }
          ...

          4.3 顯示實驗性 JFR 事件

          需要注意的是,這些事件被標記為實驗性。這意味著像 VisualVM 和 JDK Mission Control 這樣的軟件默認會隱藏它們。

          jdk.NativeMemoryUsagePeakjdk.NativeMemoryUsageTotalPeak 是 JDK 中的兩個事件,它們用于跟蹤Java虛擬機(JVM)使用的本地內存的峰值。

          要在 VisualVM 中顯示這些事件,請在 Browser 標簽頁中選中 Display experimental items 復選框,如圖3所示。

          圖3:VisualVM中顯示的實驗性事件。

          要在 JDK Mission Control 中顯示這些事件,在 Preferences > JDK Mission Control > Flight Recorder,選擇 Include experimental events... ,如圖4所示。圖4:使用“首選項”菜單來切換顯示實驗性事件。

          五、實現細節(jié)

          Native Image 中的 NMT 與 Hotspot 中的對應功能非常相似。具體來說,它通過插樁 malloc/calloc/realloc 調用點以及 mmap 調用點來工作。總體上,這種插樁被分為 malloc 跟蹤和虛擬內存跟蹤。虛擬內存跟蹤組件尚未集成(在下一節(jié)中閱讀更多)。malloc 跟蹤利用在每個 malloc 分配上添加小標題。這些標題存儲有關分配的元數據,并允許 NMT 系統(tǒng)保持對現有原生內存塊的準確理解。實際的內存使用記錄本質上是一系列連續(xù)更新的集中計數器。這意味著在生成 NMT 報告時,只報告原生內存使用的瞬時快照。

          六、性能影響

          與在即時編譯模式下運行 Java 應用程序的 JVM 類似,Native Image NMT 在大多數場景中的性能影響非常小。當前實現每個分配有 16B 的開銷,以容納 malloc 標題。可能會有對用于跟蹤使用量的原子計數器的爭用,但在大多數情況下,預計沒有足夠的并發(fā)原生內存分配來產生任何影響。

          使用基于基本入門快速啟動的簡單 Quarkus 應用程序,運行了一個測試以收集下表中的性能指標。Hyperfoil 被用來驅動對 Quarkus 原生鏡像可執(zhí)行文件的請求。每秒發(fā)送 50 個請求,持續(xù) 5 秒,最多同時發(fā)送 25 個請求。每個請求在生成響應之前都會導致 1000 個連續(xù)的 1KB 原生分配。這是最壞情況的例子,因為有大量的小分配而不是較少的大分配。malloc 標題開銷與分配計數成比例,而不是分配大小。因此,這應該被解釋為夸大的案例,大多數情況下的性能會更好。

          這些結果是 10 次運行的平均值。表中還包括了 Java JIT 執(zhí)行的結果進行比較。

          Measurement With NMT (NI) Without NMT (NI) With NMT (Java) Without NMT (Java)
          RSS (KB) 53,030 53,390 151,460 148,952
          啟動時間 (ms) 147 144 1,378 1,364
          平均響應時間 (us) 4,040 3,766 4,881 4,613
          P50 響應時間 (us) 3,846 3,615 4,695 4,440
          P90 響應時間 (us) 4,925 4,544 6,337 5,924
          P99 響應時間 (us) 13,772 11,347 12,497 12,602
          Image size (B) 47,292,872 47,290,704 N/A N/A

          從結果表中我們可以看到,NMT 對 RSS(在啟動時測量)和啟動時間的影響微乎其微。這是意料之中的,因為 NMT 不需要任何特別的設置或預先分配(與 JFR 不同)。NMT 內存開銷與本地分配的數量成比例。鏡像大小的增加也非常小 (增加 <3KB)。盡管示例夸大了,但響應延遲也受到了最小的影響。

          后來進行了第二次測試,以確定 NMT 和 JFR 的綜合影響。結果如下。

          Measurement With NMT and JFR (NI) Without NMT or JFR (NI) With NMT and JFR (Java) Without NMT or JFR (Java)
          RSS (KB) 72,366 52,388 191,504 149,756
          啟動時間 (ms) 193 138 1,920 1,315
          平均響應時間 (us) 5,038 4,451 5,990 4,452
          P50 響應時間 (us) 4,882 3,548 4,793 4,325
          P90 響應時間 (us) 6,704 4,662 9,525 5,826
          P99 響應時間 (us) 12,320 9,591 30,644 10,623
          Image size (B) 50,938,336 47,290,272 N/A N/A

          我們可以看到,一旦 JFR 出現,性能影響就大得多。實際上,JFR 的影響在所有測量類別中都大大超過了 NMT 的影響。

          我們還可以使用 NMT 本身來確定正在使用的資源數量。下面顯示了 NMT 報告的相關部分:

          JFR peak used memory: 11295082 bytes
          JFR alive allocations at peak: 1327
          Native Memory Tracking peak used memory: 23584 bytes
          Native Memory Tracking alive allocations at peak: 1474

          從這些信息中我們可以看到,JFR 使用的原生內存完全超過了 NMT 所需的內存。這是因為 JFR 要復雜得多。例如,JFR 需要為每個平臺線程提供線程本地緩沖區(qū),并為內部數據提供存儲表。你還可以看到,NMT 的峰值分配計數乘以 malloc 頭的大小等于峰值使用大小。

          七、限制和規(guī)劃

          Native Image 中 NMT 的當前實現僅跟蹤 malloc/calloc/realloc,缺少虛擬內存跟蹤。虛擬內存會計組件的初始迭代已完成,但仍在審查過程中。然而,malloc 跟蹤可能是大多數情況下發(fā)生大多數有趣分配的地方。當前的 SubstrateVM 實現僅使用虛擬內存操作,如 mmap,來支持“Java 堆”并映射“鏡像堆”(Native Image 的“Java 堆”的只讀部分)。缺少虛擬內存跟蹤意味著像 jdk.NativeMemoryUsage 這樣的 JFR 事件將報告保留和提交的內存相等。一旦集成了虛擬內存跟蹤,并且我們除了 malloc 之外還跟蹤個別的保留/提交,某些類別中的保留和提交值可能會有所不同。

          Native Image NMT 與 OpenJDK 共享的一個限制是,它只能跟蹤 VM 級別的分配和使用 Unsafe#allocateMemory(long) 進行的分配。例如,如果庫代碼或應用程序代碼直接調用 malloc,則該調用將繞過 NMT 會計并且不會被跟蹤。

          Native Image 特有的一個限制是,如果沒有 JFR,就沒有辦法在程序執(zhí)行期間的任意點收集 NMT 報告數據。你必須等到程序完成才能轉儲報告。正在研究的一個解決方案是能夠在接收到信號時轉儲報告(類似于堆轉儲)。

          在 OpenJDK 中,NMT 有兩種不同的模式。“摘要”模式類似于目前在 Native Image 中實現的,而“詳細”模式還跟蹤分配調用站點。“詳細”模式尚未在 Native Image 中支持,但未來可能會添加。

          還需要知道的是,每個內部 NMT 計數器都是獨立且非原子地與其他計數器更新的。這與 OpenJDK 中的情況相同。然而,這意味著“總計”或“峰值時計數”測量桶的報告可能會因請求報告的時間而暫時與其他計數器不同步。

          八、總結

          添加到 Native Image 的新 NMT 功能應該有助于用戶了解他們的可執(zhí)行文件是如何使用原生內存的。NMT 加入了 JFR、JMX、堆轉儲和調試信息的行列,成為 Native Image 可觀測性和調試工具箱中的另一個組件。我希望你將嘗試這個新功能,并報告你擁有的任何建議或你發(fā)現的任何問題。你可以在 GraalVM GitHub 的 Issues 中反饋。干杯!

          瀏覽 91
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩欧美亚州小说图文视频 | 香蕉大伊人 | 免费观看黄色成人网站 | 人操人摸| 精品人妻人伦 |