熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

理解JVM如何使用Windows和Linux上的本機內存

2022-06-13   來源: Java核心技術 

  摘要Java&#; 堆耗盡並不是造成 javalangOutOfMemoryError 的惟一原因如果本機內存 耗盡則會發生普通調試技巧無法解決的 OutOfMemoryError本文將討論本機內存的概念Java 運行時如何使用它它被耗盡時會出現什麼情況以及如何在 Windows® 和 Linux® 上調試本機 OutOfMemoryError

  Java 堆(每個 Java 對象在其中分配)是您在編寫 Java 應用程序時使用最頻繁的內存區域JVM 設計用於將我們與主機的特性隔離所以將內存當作堆來考慮再正常不過了您一定遇到過 Java 堆 OutOfMemoryError 它可能是由於對象洩漏造成的也可能是因為堆的大小不足以存儲所有數據您也可能了解這些場景的一些調試技巧但是隨著您的 Java 應用程序處理越來越多的數據和越來越多的並發負載您可能就會遇到無法使用常規技巧進行修復的 OutOfMemoryError在一些場景中即使 java 堆未滿也會拋出錯誤當這類場景發生時您需要理解 Java 運行時環境(Java Runtime EnvironmentJRE)內部到底發生了什麼

  Java 應用程序在 Java 運行時的虛擬化環境中運行但是運行時本身是使用 C 之類的語言編寫的本機程序它也會耗用本機資源包括本機內存本機內存是可用於運行時進程的內存它與 Java 應用程序使用的 java 堆內存不同每種虛擬化資源(包括 Java 堆和 Java 線程)都必須存儲在本機內存中虛擬機在運行時使用的數據也是如此這意味著主機的硬件和操作系統施加在本機內存上的限制會影響到 Java 應用程序的性能

  本機內存簡介

  我將首先解釋一下操作系統和底層硬件給本機內存帶來的限制如果您熟悉使用 C 等語言管理動態內存那麼您可以直接跳到 下一節

  硬件限制

  本機進程遇到的許多限制都是由硬件造成的而與操作系統沒有關系每台計算機都有一個處理器和一些隨機存取存儲器(RAM)後者也稱為物理內存處理器將數據流解釋為要執行的指令它擁有一個或多個處理單元用於執行整數和浮點運算以及更高級的計算處理器具有許多寄存器 —— 常快速的內存元素用作被執行的計算的工作存儲寄存器大小決定了一次計算可使用的最大數值

  處理器通過內存總線連接到物理內存物理地址(處理器用於索引物理 RAM 的地址)的大小限制了可以尋址的內存例如一個 位物理地址可以尋址 xxFFFF 的內存地址這個地址范圍包括 ^ = 個惟一的內存位置如果每個地址引用一個存儲字節那麼一個 位物理地址將允許處理器尋址 KB 內存

  處理器被描述為特定數量的數據位這通常指的是寄存器大小但是也存在例外比如 指的是物理地址大小對於桌面和服務器平台這個數字為 對於嵌入式設備和微處理器這個數字可能小至 物理地址大小可以與寄存器帶寬一樣大也可以比它大或小如果在適當的操作系統上運行大部分 位處理器可以運行 位程序

  表 列出了一些流行的 Linux 和 Windows 架構以及它們的寄存器和物理地址大小

  表 一些流行處理器架構的寄存器和物理地址大小

   架構  寄存器帶寬(位)  物理地址大小(位)  (現代)Intel® x    
具有物理地址擴展(Pentium Pro 和更高型號)  x    目前為 位(以後將會增大)  PPC    在 POWER 上為 位   位       位    

  操作系統和虛擬內存

  如果您編寫無需操作系統直接在處理器上運行的應用程序您可以使用處理器可以尋址的所有內存(假設連接到了足夠的物理 RAM)但是要使用多任務和硬件抽象等特性幾乎所有人都會使用某種類型的操作系統來運行他們的程序

  在 Windows 和 Linux 等多任務操作系統中有多個程序在使用系統資源需要為每個程序分配物理內存區域來在其中運行可以設計這樣一個操作系統每個程序直接使用物理內存並且可以可靠地僅使用分配給它的內存一些嵌入式操作系統以這種方式工作但是這在包含多個未經過集中測試的應用程序的環境中是不切實際的因為任何程序都可能破壞其他程序或者操作系統本身的內存

  虛擬內存 允許多個進程共享物理內存而且不會破壞彼此的數據在具有虛擬內存的操作系統(比如 WindowsLinux 和許多其他操作系統)中每個程序都擁有自己的虛擬地址空間 —— 一個邏輯地址區域其大小由該系統上的地址大小規定(所以桌面和服務器平台的虛擬地址空間為 位)進程的虛擬地址空間中的區域可被映射到物理內存文件或任何其他可尋址存儲當數據未使用時操作系統可以在物理內存與一個交換區域(Windows 上的頁面文件 或者 Linux 上的交換分區)之間移動它以實現對物理內存的最佳利用率當一個程序嘗試使用虛擬地址訪問內存時操作系統連同片上硬件會將該虛擬地址映射到物理位置這個位置可以是物理 RAM一個文件或頁面文件/交換分區如果一個內存區域被移動到交換空間那麼它將在被使用之前加載回物理內存中 展示了虛擬內存如何將進程地址空間區域映射到共享資源

  圖 虛擬內存將進程地址空間映射到物理資源

  虛擬內存映射

  程序的每個實例以進程 的形式運行在 Linux 和 Windows 上進程是一個由受操作系統控制的資源(比如文件和套接字信息)一個典型的虛擬地址空間(在某些架構上不止一個)和至少一個執行線程構成的集合

  虛擬地址空間大小可能比處理器的物理地址大小更小 位 Intel x 最初擁有的 位物理地址僅允許處理器尋址 GB 存儲空間後來添加了一種稱為物理地址擴展(Physical Address ExtensionPAE)的特性將物理地址大小擴大到了 允許安裝或尋址至多 GB RAMPAE 允許操作系統將 位的 GB 虛擬地址空間映射到一個較大的物理地址范圍但是它不允許每個進程擁有 GB 虛擬地址空間這意味著如果您將大於 GB 的內存放入 位 Intel 服務器中您將無法將所有內存直接映射到一個單一進程中

  地址窗口擴展(Address Windowing Extension)特性允許 Windows 進程將其 位地址空間的一部分作為滑動窗口映射到較大的內存區域中Linux 使用類似的技術將內存區域映射到虛擬地址空間中這意味著盡管您無法直接引用大於 GB 的內存但您仍然可以使用較大的內存區域

  內核空間和用戶空間

  盡管每個進程都有其自己的地址空間但程序通常無法使用所有這些空間地址空間被劃分為用戶空間 和內核空間內核是主要的操作系統程序包含用於連接計算機硬件調度程序以及提供聯網和虛擬內存等服務的邏輯

  作為計算機啟動序列的一部分操作系統內核運行並初始化硬件一旦內核配置了硬件及其自己的內部狀態第一個用戶空間進程就會啟動如果用戶程序需要來自操作系統的服務它可以執行一種稱為系統調用 的操作與內核程序交互內核程序然後執行該請求系統調用通常是讀取和寫入文件聯網和啟動新進程等操作所必需的

  當執行系統調用時內核需要訪問其自己的內存和調用進程的內存因為正在執行當前線程的處理器被配置為使用地址空間映射來為當前進程映射虛擬地址所以大部分操作系統將每個進程地址空間的一部分映射到一個通用的內核內存區域被映射來供內核使用的地址空間部分稱為內核空間其余部分稱為用戶空間可供用戶應用程序使用

  內核空間和用戶空間之間的平衡關系因操作系統的不同而不同甚至在運行於不同硬件架構之上的同一操作系統的各個實例間也有所不同這種平衡通常是可配置的可進行調整來為用戶應用程序或內核提供更多空間縮減內核區域可能導致一些問題比如能夠同時登錄的用戶數量限制或能夠運行的進程數量限制更小的用戶空間意味著應用程序編程人員只能使用更少的內存空間

  默認情況下 位 Windows 擁有 GB 用戶空間和 GB 內核空間在一些 Windows 版本上通過向啟動配置添加 /GB 開關並使用 /LARGEADDRESSAWARE 開關重新鏈接應用程序可以將這種平衡調整為 GB 用戶空間和 GB 內核空間 位 Linux 上默認設置為 GB 用戶空間和 GB 內核空間一些 Linux 分發版提供了一個 hugemem 內核支持 GB 用戶空間為了實現這種配置將進行系統調用時使用的地址空間分配給內核通過這種方式增加用戶空間會減慢系統調用因為每次進行系統調用時操作系統必須在地址空間之間復制數據並重置進程地址空間映射 展示了 位 Windows 的地址空間布局

  圖 位 Windows 的地址空間布局

  Windows 32 位地址空間

  圖 顯示了 位 Linux 的地址空間配置

   位 Linux 的地址空間布局
Linux 32 位地址空間

   位 Linux 上還使用了一個獨立的內核地址空間其中較小的 GB 地址空間使對單個地址空間進行劃分不太合理但是 架構可以同時使用多個地址空間而且不會降低性能

  進程空間必須包含程序需要的所有內容包括程序本身和它使用的共享庫(在 Windows 上為 DDL在 Linux 上為 so 文件)共享庫不僅會占據空間使程序無法在其中存儲數據它們還會使地址空間碎片化減少可作為連續內存塊分配的內存這對於在擁有 GB 用戶空間的 Windows x 上運行的程序尤為明顯DLL 在構建時設置了首選的加載地址當加載 DLL 時它被映射到處於特定位置的地址空間除非該位置已經被占用在這種情況下它會加載到別處Windows NT 最初設計時設置了 GB 可用用戶空間這對於要構建來加載接近 GB 區域的系統庫很有用 —— 使大部分用戶區域都可供應用程序自由使用當用戶區域擴展到 GB 時系統共享庫仍然加載接近 GB 數據(約為用戶空間的一半)盡管總體用戶空間為 GB但是不可能分配 GB 大的內存塊因為共享庫無法加載這麼大的內存

  在 Windows 中使用 /GB 開關可以將內核空間減少一半也就是最初設計的大小在一些情形下可能耗盡 GB 內核空間使 I/O 變得緩慢且無法正常創建新的用戶會話盡管 /GB 開關可能對一些應用程序非常有用但任何使用它的環境在部署之前都應該進行徹底的負載測試參見 參考資料獲取關於 /GB 開關及其優缺點的更多信息的鏈接

  本機內存洩漏或過度使用本機內存將導致不同的問題具體取決於您是耗盡了地址空間還是用完了物理內存耗盡地址空間通常只會發生在 位進程上因為最大 GB 的內存很容易分配完 位進程具有數百或數千 GB 的用戶空間即使您特意消耗空間也很難耗盡這麼大的空間如果您確實耗盡了 Java 進程的地址空間那麼 Java 運行時可能會出現一些陌生現象本文稍後將詳細討論當在進程地址空間比物理內存大的系統上運行時內存洩漏或過度使用本機內存會迫使操作系統交換後備存儲器來用作本機進程的虛擬地址空間訪問經過交換的內存地址比讀取駐留(在物理內存中)的地址慢得多因為操作系統必須從硬盤驅動器拉取數據可能會分配大量內存來用完所有物理內存和所有交換內存(頁面空間)在 Linux 上這將觸發內核內存不足(OOM)結束程序強制結束最消耗內存的進程在 Windows 上與地址空間被占滿時一樣內存分配將會失敗

  同時如果嘗試使用比物理內存大的虛擬內存顯然在進程由於消耗內存太大而被結束之前就會遇到問題系統將變得異常緩慢因為它會將大部分時間用於在內存與交換空間之間來回復制數據當發生這種情況時計算機和獨立應用程序的性能將變得非常糟糕從而使用戶意識到出現了問題當 JVM 的 Java 堆被交換出來時垃圾收集器的性能會變得非常差應用程序可能被掛起如果一台機器上同時使用了多個 Java 運行時那麼物理內存必須足夠分配給所有 Java 堆

  Java 運行時如何使用本機內存

  Java 運行時是一個操作系統進程它會受到我在上一節中列出的硬件和操作系統局限性的限制運行時環境提供的功能受一些未知的用戶代碼驅動這使得無法預測在每種情形中運行時環境將需要何種資源Java 應用程序在托管 Java 環境中執行的每個操作都會潛在地影響提供該環境的運行時的需求本節描述 Java 應用程序為什麼和如何使用本機內存

  Java 堆和垃圾收集

  Java 堆是分配了對象的內存區域大多數 Java SE 實現都擁有一個邏輯堆但是一些專家級 Java 運行時擁有多個堆比如實現 Java 實時規范(Real Time Specification for JavaRTSJ)的運行時一個物理堆可被劃分為多個邏輯扇區具體取決於用於管理堆內存的垃圾收集(GC)算法這些扇區通常實現為連續的本機內存塊這些內存塊受 Java 內存管理器(包含垃圾收集器)控制

  堆的大小可以在 Java 命令行使用 Xmx 和 Xms 選項來控制(mx 表示堆的最大大小ms 表示初始大小)盡管邏輯堆(經常被使用的內存區域)可以根據堆上的對象數量和在 GC 上花費的時間而增大和縮小但使用的本機內存大小保持不變而且由 Xmx 值(最大堆大小)指定大部分 GC 算法依賴於被分配為連續的內存塊的堆因此不能在堆需要擴大時分配更多本機內存所有堆內存必須預先保留

  保留本機內存與分配本機內存不同當本機內存被保留時無法使用物理內存或其他存儲器作為備用內存盡管保留地址空間塊不會耗盡物理資源但會阻止內存被用於其他用途由保留從未使用的內存導致的洩漏與洩漏分配的內存一樣嚴重

  當使用的堆區域縮小時一些垃圾收集器會回收堆的一部分(釋放堆的後備存儲空間)從而減少使用的物理內存

  對於維護 Java 堆的內存管理系統需要更多本機內存來維護它的狀態當進行垃圾收集時必須分配數據結構來跟蹤空閒存儲空間和記錄進度這些數據結構的確切大小和性質因實現的不同而不同但許多數據結構都與堆大小成正比

  即時 (JIT) 編譯器

  JIT 編譯器在運行時編譯 Java 字節碼來優化本機可執行代碼這極大地提高了 Java 運行時的速度並且支持 Java 應用程序以與本機代碼相當的速度運行

  字節碼編譯使用本機內存(使用方式與 gcc 等靜態編譯器使用內存來運行一樣)但 JIT 編譯器的輸入(字節碼)和輸出(可執行代碼)必須也存儲在本機內存中包含多個經過 JIT 編譯的方法的 Java 應用程序會使用比小型應用程序更多的本機內存

  類和類加載器

  Java 應用程序由一些類組成這些類定義對象結構和方法邏輯Java 應用程序也使用 Java 運行時類庫(比如 javalangString)中的類也可以使用第三方庫這些類需要存儲在內存中以備使用

  存儲類的方式取決於具體實現Sun JDK 使用永久生成(permanent generationPermGen)堆區域Java 的 IBM 實現會為每個類加載器分配本機內存塊並將類數據存儲在其中現代 Java 運行時擁有類共享等技術這些技術可能需要將共享內存區域映射到地址空間要理解這些分配機制如何影響您 Java 運行時的本機內存占用您需要查閱該實現的技術文檔然而一些普遍的事實會影響所有實現

  從最基本的層面來看使用更多的類將需要使用更多內存(這可能意味著您的本機內存使用量會增加或者您必須明確地重新設置 PermGen 或共享類緩存等區域的大小以裝入所有類)記住不僅您的應用程序需要加載到內存中框架應用服務器第三方庫以及包含類的 Java 運行時也會按需加載並占用空間

  Java 運行時可以卸載類來回收空間但是只有在非常嚴酷的條件下才會這樣做不能卸載單個類而是卸載類加載器隨其加載的所有類都會被卸載只有在以下情況下才能卸載類加載器

  Java 堆不包含對表示該類加載器的 javalangClassLoader 對象的引用
    Java 堆不包含對表示類加載器加載的類的任何 javalangClass 對象的引用
    在 Java 堆上該類加載器加載的任何類的所有對象都不再存活(被引用)

  需要注意的是Java 運行時為所有 Java 應用程序創建的 個默認類加載器( bootstrapextension 和 application )都不可能滿足這些條件因此任何系統類(比如 javalangString)或通過應用程序類加載器加載的任何應用程序類都不能在運行時釋放

  即使類加載器適合進行收集運行時也只會將收集類加載器作為 GC 周期的一部分一些實現只會在某些 GC 周期中卸載類加載器

  也可能在運行時生成類而不用釋放它許多 JEE 應用程序使用 JavaServer Pages (JSP) 技術來生成 Web 頁面使用 JSP 會為執行的每個 jsp 頁面生成一個類並且這些類會在加載它們的類加載器的整個生存期中一直存在 —— 這個生存期通常是 Web 應用程序的生存期

  另一種生成類的常見方法是使用 Java 反射反射的工作方式因 Java 實現的不同而不同但 Sun 和 IBM 實現都使用了這種方法我馬上就會講到

  當使用 javalangreflect API 時Java 運行時必須將一個反射對象(比如 javalangreflectField)的方法連接到被反射到的對象或類這可以通過使用 Java 本機接口(Java Native InterfaceJNI)訪問器來完成這種方法需要的設置很少但是速度緩慢也可以在運行時為您想要反射到的每種對象類型動態構建一個類後一種方法在設置上更慢但運行速度更快非常適合於經常反射到一個特定類的應用程序

  Java 運行時在最初幾次反射到一個類時使用 JNI 方法但當使用了若干次 JNI 方法之後訪問器會膨脹為字節碼訪問器這涉及到構建類並通過新的類加載器進行加載執行多次反射可能導致創建了許多訪問器類和類加載器保持對反射對象的引用會導致這些類一直存活並繼續占用空間因為創建字節碼訪問器非常緩慢所以 Java 運行時可以緩存這些訪問器以備以後使用一些應用程序和框架還會緩存反射對象這進一步增加了它們的本機內存占用

  JNI

  JNI 支持本機代碼(使用 C 和 C++ 等本機編譯語言編寫的應用程序)調用 Java 方法反之亦然Java 運行時本身極大地依賴於 JNI 代碼來實現類庫功能比如文件和網絡 I/OJNI 應用程序可能通過 種方式增加 Java 運行時的本機內存占用

  JNI 應用程序的本機代碼被編譯到共享庫中或編譯為加載到進程地址空間中的可執行文件大型本機應用程序可能僅僅加載就會占用大量進程地址空間

  本機代碼必須與 Java 運行時共享地址空間任何本機代碼分配或本機代碼執行的內存映射都會耗用 Java 運行時的內存

  某些 JNI 函數可能在它們的常規操作中使用本機內存GetTypeArrayElements 和 GetTypeArrayRegion 函數可以將 Java 堆數據復制到本機內存緩沖區中以供本機代碼使用是否復制數據依賴於運行時實現(IBM Developer Kit for Java 和更高版本會進行本機復制)通過這種方式訪問大量 Java 堆數據可能會使用大量本機堆

  NIO

  Java 中添加的新 I/O (NIO) 類引入了一種基於通道和緩沖區來執行 I/O 的新方式就像 Java 堆上的內存支持 I/O 緩沖區一樣NIO 添加了對直接 ByteBuffer 的支持(使用 javanioByteBufferallocateDirect() 方法進行分配) ByteBuffer 受本機內存而不是 Java 堆支持直接 ByteBuffer 可以直接傳遞到本機操作系統庫函數以執行 I/O — 這使這些函數在一些場景中要快得多因為它們可以避免在 Java 堆與本機堆之間復制數據

  對於在何處存儲直接 ByteBuffer 數據很容易產生混淆應用程序仍然在 Java 堆上使用一個對象來編排 I/O 操作但持有該數據的緩沖區將保存在本機內存中Java 堆對象僅包含對本機堆緩沖區的引用非直接 ByteBuffer 將其數據保存在 Java 堆上的 byte[] 數組中 展示了直接與非直接 ByteBuffer 對象之間的區別

  圖 直接與非直接 javanioByteBuffer 的內存拓撲結構

  ByteBuffer 內存安排

  直接 ByteBuffer 對象會自動清理本機緩沖區但這個過程只能作為 Java 堆 GC 的一部分來執行因此它們不會自動響應施加在本機堆上的壓力GC 僅在 Java 堆被填滿以至於無法為堆分配請求提供服務時發生或者在 Java 應用程序中顯式請求它發生(不建議采用這種方式因為這可能導致性能問題)

  發生垃圾收集的情形可能是本機堆被填滿並且一個或多個直接 ByteBuffers 適合於垃圾收集(並且可以被釋放來騰出本機堆的空間)但 Java 堆幾乎總是空的所以不會發生垃圾收集

  線程

  應用程序中的每個線程都需要內存來存儲器堆棧(用於在調用函數時持有局部變量並維護狀態的內存區域)每個 Java 線程都需要堆棧空間來運行根據實現的不同Java 線程可以分為本機線程和 Java 堆棧除了堆棧空間每個線程還需要為線程本地存儲(threadlocal storage)和內部數據結構提供一些本機內存

  堆棧大小因 Java 實現和架構的不同而不同一些實現支持為 Java 線程指定堆棧大小其范圍通常在 KB 到 KB 之間

  盡管每個線程使用的內存量非常小但對於擁有數百個線程的應用程序來說線程堆棧的總內存使用量可能非常大如果運行的應用程序的線程數量比可用於處理它們的處理器數量多效率通常很低並且可能導致糟糕的性能和更高的內存占用

  本機內存耗盡會發生什麼?

  Java 運行時善於以不同的方式來處理 Java 堆的耗盡與本機堆的耗盡但這兩種情形具有類似的症狀當 Java 堆耗盡時Java 應用程序很難正常運行因為 Java 應用程序必須通過分配對象來完成工作只要 Java 堆被填滿就會出現糟糕的 GC 性能並拋出表示 Java 堆被填滿的 OutOfMemoryError

  相反一旦 Java 運行時開始運行並且應用程序處於穩定狀態它可以在本機堆完全耗盡之後繼續正常運行不一定會發生奇怪的行為因為需要分配本機內存的操作比需要分配 Java 堆的操作少得多盡管需要本機內存的操作因 JVM 實現不同而異但也有一些操作很常見啟動線程加載類以及執行某種類型的網絡和文件 I/O

  本機內存不足行為與 Java 堆內存不足行為也不太一樣因為無法對本機堆分配進行單點控制盡管所有 Java 堆分配都在 Java 內存管理系統控制之下但任何本機代碼(無論其位於 JVMJava 類庫還是應用程序代碼中)都可能執行本機內存分配而且會失敗嘗試進行分配的代碼然後會處理這種情況無論設計人員的意圖是什麼它可能通過 JNI 接口拋出一個 OutOfMemoryError在屏幕上輸出一條消息發生無提示失敗並在稍後再試一次或者執行其他操作

  缺乏可預測行為意味著無法確定本機內存是否耗盡相反您需要使用來自操作系統和 Java 運行時的數據執行診斷

  本機內存耗盡示例

  為了幫助您了解本機內存耗盡如何影響您正使用的 Java 實現本文的示例代碼(參見 下載)中包含了一些 Java 程序用於以不同方式觸發本機堆耗盡這些示例使用通過 C 語言編寫的本機庫來消耗所有本機地址空間然後嘗試執行一些使用本機內存的操作提供的示例已經過編譯編譯它們的指令包含在示例包的頂級目錄下的 l 文件中

  comibmjtcdemosNativeMemoryGlutton 類提供了 gobbleMemory() 方法它在一個循環中調用 malloc直到幾乎所有本機內存都已耗盡完成任務之後它通過以下方式輸出分配給標准錯誤的字節數

   Allocated bytes of native memory before running out

  針對在 位 Windows 上運行的 Sun 和 IBM Java 運行時的每次演示其輸出都已被捕獲提供的二進制文件已在以下操作系統上進行了測試

  Linux x
    Linux PPC
    Linux
    Windows x

  使用以下 Sun Java 運行時版本捕獲輸出

   java version _
Java(TM) Runtime Environment Standard Edition (build _b)
Java HotSpot(TM) Client VM (build _b mixed mode)

  使用的 IBM Java 運行時版本為

   java version
Java(TM) Runtime Environment Standard Edition (build pwidevifx (SR
b))
IBM J VM (build JRE IBM J Windows XP x jvmwi
(JIT enabled)
JVM __lHdSMR
JIT  _ifx_r
GC   _)
JCL 

  在耗盡本機內存時嘗試啟動線程

  comibmjtcdemosStartingAThreadUnderNativeStarvation 類嘗試在耗盡進程地址空間時啟動一個線程這是發現 Java 進程已耗盡內存的一種常用方式因為許多應用程序都會在其整個生存期啟動線程

  當在 IBM Java 運行時上運行時StartingAThreadUnderNativeStarvation 演示的輸出如下

   Allocated bytes of native memory before running out
JVMDUMPI Processing Dump Event systhrow detail
java/lang/OutOfMemoryError Please Wait
JVMDUMPI JVM Requesting Snap Dump using C:\Snaptrc
JVMDUMPI Snap Dump written to C:\Snaptrc
JVMDUMPI JVM Requesting Heap Dump using C:\heapdumpphd
JVMDUMPI Heap Dump written to C:\heapdumpphd
JVMDUMPI JVM Requesting Java Dump using C:\javacoretxt
JVMDUMPI Java Dump written to C:\javacoretxt
JVMDUMPI Processed Dump Event systhrow detail java/lang/OutOfMemoryError
javalangOutOfMemoryError: ZIP:OutOfMemoryError ENOMEM error in ZipFileopen
   at javautilzipZipFileopen(Native Method)
   at javautilzipZipFile<init>(ZipFilejava:)
   at javautiljarJarFile<init>(JarFilejava:)
   at javautiljarJarFile<init>(JarFilejava:)
   at comibmotivmAbstractClassLoaderfillCache(AbstractClassLoaderjava:)
   at comibmotivmAbstractClassLoadergetResourceAsStream(AbstractClassLoaderjava:)
   at javautilResourceBundle$run(ResourceBundlejava:)
   at javasecurityAccessControllerdoPrivileged(AccessControllerjava:)
   at javautilResourceBundleloadBundle(ResourceBundlejava:)
   at javautilResourceBundlefindBundle(ResourceBundlejava:)
   at javautilResourceBundlegetBundleImpl(ResourceBundlejava:)
   at javautilResourceBundlegetBundle(ResourceBundlejava:)
   at comibmotivmMsgHelpsetLocale(MsgHelpjava:)
   at comibmotiutilMsg$run(Msgjava:)
   at javasecurityAccessControllerdoPrivileged(AccessControllerjava:)
   at comibmotiutilMsg<clinit>(Msgjava:)
   at javalangJVMInternalsinitializeImpl(Native Method)
   at javalangJVMInternalsinitialize(JVMInternalsjava:)
   at javalangThreadGroupuncaughtException(ThreadGroupjava:)
   at javalangThreadGroupuncaughtException(ThreadGroupjava:)
   at javalangThreaduncaughtException(Threadjava:)
KjavalangOutOfMemoryError: Failed to fork OS thread
   at javalangThreadstartImpl(Native Method)
   at javalangThreadstart(Threadjava:)
   at comibmjtcdemosStartingAThreadUnderNativeStarvationmain(
StartingAThreadUnderNativeStarvationjava:)

  調用 javalangThreadstart() 來嘗試為一個新的操作系統線程分配內存此嘗試會失敗並拋出 OutOfMemoryErrorJVMDUMP 行通知用戶 Java 運行時已經生成了標准的 OutOfMemoryError 調試數據

  嘗試處理第一個 OutOfMemoryError 會導致第二個錯誤 —— OutOfMemoryError ENOMEM error in ZipFileopen當本機進程內存耗盡時通常會拋出多個 OutOfMemoryErrorFailed to fork OS thread 可能是在耗盡本機內存時最常見的消息

  本文提供的示例會觸發一個 OutOfMemoryError 集群這比您在自己的應用程序中看到的情況要嚴重得多這一定程度上是因為幾乎所有本機內存都已被使用與實際的應用程序不同使用的內存不會在以後被釋放在實際應用程序中當拋出 OutOfMemoryError 時線程會關閉並且可能會釋放一部分本機內存以讓運行時處理錯誤測試案例的這個細微特性還意味著類庫的許多部分(比如安全系統)未被初始化而且它們的初始化受嘗試處理內存耗盡情形的運行時驅動在實際應用程序中您可能會看到顯示了很多錯誤但您不太可能在一個位置看到所有這些錯誤

  在 Sun Java 運行時上執行相同的測試案例時會生成以下控制台輸出

   Allocated bytes of native memory before running out
Exception in thread main javalangOutOfMemoryError: unable to create new native thread
   at javalangThreadstart(Native Method)
   at javalangThreadstart(Threadjava:)
   at comibmjtcdemosStartingAThreadUnderNativeStarvationmain(
StartingAThreadUnderNativeStarvationjava:)

  盡管堆棧軌跡和錯誤消息稍有不同但其行為在本質上是一樣的本機分配失敗並拋出 javalangOutOfMemoryError此場景中拋出的 OutOfMemoryError 與由於 Java 堆耗盡而拋出的錯誤的惟一區別在於消息

  嘗試在本機內存耗盡時分配直接 ByteBuffer

  comibmjtcdemosDirectByteBufferUnderNativeStarvation 類嘗試在地址空間耗盡時分配一個直接(也就是受本機支持的)javanioByteBuffer 對象當在 IBM Java 運行時上運行時它生成以下輸出

   Allocated bytes of native memory before running out
JVMDUMPI Processing Dump Event uncaught detail
java/lang/OutOfMemoryError Please Wait
JVMDUMPI JVM Requesting Snap Dump using C:\Snaptrc
JVMDUMPI Snap Dump written to C:\Snaptrc
JVMDUMPI JVM Requesting Heap Dump using C:\heapdumpphd
JVMDUMPI Heap Dump written to C:\heapdumpphd
JVMDUMPI JVM Requesting Java Dump using C:\javacoretxt
JVMDUMPI Java Dump written to C:\javacoretxt
JVMDUMPI Processed Dump Event uncaught detail java/lang/OutOfMemoryError
Exception in thread main javalangOutOfMemoryError:
Unable to allocate bytes of direct memory after retries
   at javanioDirectByteBuffer<init>(DirectByteBufferjava:)
   at javanioByteBufferallocateDirect(ByteBufferjava:)
   at comibmjtcdemosDirectByteBufferUnderNativeStarvationmain(
   DirectByteBufferUnderNativeStarvationjava:)
Caused by: javalangOutOfMemoryError
   at sunmiscUnsafeallocateMemory(Native Method)
   at javanioDirectByteBuffer<init>(DirectByteBufferjava:)
   more

  在此場景中拋出了 OutOfMemoryError它會觸發默認的錯誤文檔OutOfMemoryError 到達主線程堆棧的頂部並在 stderr 上輸出

  當在 Sun Java 運行時上運行時此測試案例生成以下控制台輸出

   Allocated bytes of native memory before running out
Exception in thread main javalangOutOfMemoryError
   at sunmiscUnsafeallocateMemory(Native Method)
   at javanioDirectByteBuffer<init>(DirectByteBufferjava:)
   at javanioByteBufferallocateDirect(ByteBufferjava:)
   at comibmjtcdemosDirectByteBufferUnderNativeStarvationmain(
DirectByteBufferUnderNativeStarvationjava:)

  調試方法和技術

  當出現 javalangOutOfMemoryError 或看到有關內存不足的錯誤消息時要做的第一件事是確定哪種類型的內存被耗盡最簡單的方式是首先檢查 Java 堆是否被填滿如果 Java 堆未導致 OutOfMemory 條件那麼您應該分析本機堆使用情況

  檢查 Java 堆

  檢查堆使用情況的方法因 Java 實現不同而異在 Java 的 IBM 實現上當拋出 OutOfMemoryError 時會生成一個 javacore 文件來告訴您javacore 文件通常在 Java 進程的工作目錄中生成以 javacore日期時間pidtxt 的形式命名如果您在文本編輯器中打開該文件可以看到以下信息

   SECTION       MEMINFO subcomponent dump routine
NULL           =================================
STHEAPFREE    Bytes of Heap Space Free:
STHEAPALLOC   Bytes of Heap Space Allocated:

  這部分信息顯示在生成 javacore 時有多少空閒的 Java 堆注意顯示的值為十六進制格式如果因為分配條件不滿足而拋出了 OutOfMemoryError 異常則 GC 軌跡部分會顯示如下信息

   STGCHTYPE     GC History 
STHSTTYPE     ::: GMT jmm    JAllocateObject() returning NULL!
bytes requested for object of class F

  JAllocateObject() returning NULL! 意味著 Java 堆分配例程未成功完成並且將拋出 OutOfMemoryError

  也可能由於垃圾收集器運行太頻繁(意味著堆被填滿了並且 Java 應用程序的運行速度將很慢或停止運行)而拋出 OutOfMemoryError在這種情況下您可能想要 Heap Space Free 值非常小GC 軌跡將顯示以下消息之一

   STGCHTYPE     GC History 
STHSTTYPE     ::: GMT jmm      Forcing JAllocateObject()
to fail due to excessive GC

   STGCHTYPE     GC History 
STHSTTYPE     ::: GMT jmm      Forcing
JAllocateIndexableObject() to fail due to excessive GC

  當 Sun 實現耗盡 Java 堆內存時它使用異常消息來顯示它耗盡的是 Java 堆

   Exception in thread main javalangOutOfMemoryError: Java heap space

  IBM 和 Sun 實現都擁有一個詳細的 GC 選項用於在每個 GC 周期生成顯示堆填充情況的跟蹤數據此信息可使用工具(比如 IBM Monitoring and Diagnostic Tools for Java Garbage Collection and Memory Visualizer (GCMV))來分析以顯示 Java 堆是否在增長

  測量本機堆使用情況

  如果您確定內存耗盡情況不是由 Java 堆耗盡引起的那麼下一步就是分析您的本機內存使用情況

  Windows 提供的 PerfMon 工具可用於監控和記錄許多操作系統和進程指標包括本機內存使用(參見 參考資料)它允許實時跟蹤計數器或將其存儲在日志文件中以供離線查看使用 Private Bytes 計數器顯示總體地址空間使用情況如果顯示值接近於用戶空間的限制(前面已經討論過介於 GB 之間)您應該會看到本機內存耗盡情況

  Linux 沒有類似於 PerfMon 的工具但是它提供了幾個替代工具命令行工具(比如 pstop 和 pmap)能夠顯示應用程序的本機內存占用情況盡管獲取進程內存使用情況的實時快照非常有用但通過記錄內存隨時間的使用情況您能夠更好地理解本機內存是如何被使用的為此能夠采取的一種方式是使用 GCMV

  GCMV 最初編寫用於分析冗長的 GC 日志允許用戶在調優垃圾收集器時查看 Java 堆使用情況和 GC 性能的變化GCMV 後來進行了擴展支持分析其他數據源包括 Linux 和 AIX 本機內存數據GCMV 是作為 IBM Support Assistant (ISA) 的插件發布的

  要使用 GCMV 分析 Linux 本機內存配置文件您首先必須使用腳本收集本機內存數據GCMV 的 Linux 本機內存分析器通過根據時間戳隔行掃描的方式讀取 Linux ps 命令的輸出GCMV 提供了一個腳本來幫助以正確形式記錄收集數據要找到該腳本

  下載並安裝 ISA Version (或更高版本)然後安裝 GCMV 工具插件

  啟動 ISA

  從菜單欄單擊 Help >> Help Contents打開 ISA 幫助菜單

  在左側窗格的 ToolIBM Monitoring and Diagnostic Tools for Java Garbage Collection and Memory Visualizer >> Using the Garbage Collection and Memory Visualizer >> Supported Data Types >> Native memory >> Linux native memory 下找到 Linux 本機內存說明

  圖 顯示了該腳本在 ISA 幫助文件中的位置如果您的幫助文件中沒有 GCMV Tool 條目很可能是因為您沒有安裝 GCMV 插件

  圖 Linux 本機內存數據捕獲腳本在 ISA 幫助對話框中的位置

  IBM Support Assistant 幫助文件

  GCMV 幫助文件中提供的腳本使用的 ps 命令僅適用於最新的 ps 版本在一些舊的 Linux 分發版中幫助文件中的命令將會生成錯誤信息要查看您的 Linux 分發版上的行為可以嘗試運行 ps o pidvsz=VSZrss=RSS如果您的 ps 版本支持新的命令行參數語法那麼得到的輸出將類似於

     PID    VSZ   RSS
     
      

  如果您的 ps 版本不支持新語法得到的輸出將類似於

     PID VSZrss=RSS
        
        

  如果您在一個較老的 ps 版本上運行可以修改本機內存腳本

   ps p $PID o pidvsz=VSZrss=RSS

  行替換為

   ps p $PID o pidvszrss

  將幫助面板中的腳本復制到一個文件中(在本例中名為 memscriptsh)找到您想要監控的 Java 進程的進程 ID (PID)(本例中為 )並運行

   /memscriptsh > psout

  這會把本機內存日志寫入到 psout 中要分析內存使用情況

  在 ISA 中從 Launch Activity 下拉菜單選擇 Analyze Problem
    選擇接近 Analyze Problem 面板頂部的 Tools 標簽
    選擇 IBM Monitoring and Diagnostic Tools for Java Garbage Collection and Memory Visualizer
    單擊接近工具面板底部的 Launch 按鈕
    單擊 Browse 按鈕並找到日志文件單擊 OK 啟動 GCMV

    一旦您擁有了本機內存隨時間的使用情況的配置文件您需要確定是存在本機內存洩漏還是在嘗試在可用空間中做太多事情即使對於運行良好的 Java 應用程序其本機內存占用也不是從啟動開始就一成不變的一些 Java 運行時系統(尤其是 JIT 編譯器和類加載器)會不斷初始化這會消耗本機內存初始化增加的內存將高居不下但是如果初始本機內存占用接近於地址空間的限制那麼僅這個前期階段就足以導致本機內存耗盡 給出了一個 Java 壓力測試示例中的 GCMV 本機內存使用情況其中突出顯示了前期階段

  圖 GCMV 的 Linux 本機內存使用示例其中顯示了前期階段

  GCMV 本機內存使用

  本機內存占用也可能應工作負載不同而異如果您的應用程序創建了較多進程來處理傳入的工作負載或者根據應用於系統的負載量按比例分配本機存儲(比如直接 ByteBuffer)則可能由於負載過高而耗盡本機內存

  由於 JVM 前期階段的本機內存增長而耗盡本機內存以及內存使用隨負載增加而增加這些都是嘗試在可用空間中做太多事情的例子在這些場景中您的選擇是

  減少本機內存使用縮小 Java 堆大小是一個好的開端

  限制本機內存使用如果您的本機內存隨負載增加而增加可以采取某種方式限制負載或為負載分配的資源

  增加可用地址空間這可以通過以下方式實現調優您的操作系統(例如在 Windows 上使用 /GB 開關增加用戶空間或者在 Linux 上使用龐大的內核空間)更換平台(Linux 通常擁有比 Windows 更多的用戶空間)或者 轉移到 位操作系統

  一種實際的本機內存洩漏表現為本機堆的持續增長這些內存不會在移除負載或運行垃圾收集器時減少內存洩漏程度因負載不同而不同但洩漏的總內存不會下降洩漏的內存不可能被引用因此它可以被交換出去並保持被交換出去的狀態

  當遇到內存洩漏時您的選擇很有限您可以增加用戶空間(這樣就會有更多的空間供洩漏)但這僅能延緩最終的內存耗盡如果您擁有足夠的物理內存和地址空間並且會在進程地址空間耗盡之前重啟應用程序那麼可以允許地址空間繼續洩漏

  是什麼在使用本機內存?

  一旦確定本機內存被耗盡下一個邏輯問題是是什麼在使用這些內存?這個問題很難回答因為在默認情況下Windows 和 Linux 不會存儲關於分配給特定內存塊的代碼路徑的信息

  當嘗試理解本機內存都到哪裡去了時您的第一步是粗略估算一下根據您的 Java 設置將會使用多少本機內存如果沒有對 JVM 工作機制的深入知識很難得出精確的值但您可以根據以下指南粗略估算一下

  Java 堆占用的內存至少為 Xmx 值

  每個 Java 線程需要堆棧空間堆棧空間因實現不同而異但是如果使用默認設置每個線程至多會占用 KB 本機內存

  直接 ByteBuffer 至少會占用提供給 allocate() 例程的內存值

  如果總數比您的最大用戶空間少得多那麼您很可能不安全Java 運行時中的許多其他組件可能會分配大量內存進而引起問題但是如果您的初步估算值與最大用戶空間很接近則可能存在本機內存問題如果您懷疑存在本機內存洩漏或者想要准確了解內存都到哪裡去了使用一些工具將有所幫助

  Microsoft 提供了 UMDH(用戶模式轉儲堆)和 LeakDiag 工具來在 Windows 上調試本機內存增長(參見 參考資料)這兩個工具的機制相同記錄特定內存區域被分配給了哪個代碼路徑並提供一種方式來定位所分配的內存不會在以後被釋放的代碼部分我建議您查閱文章 Umdhtoolsexe如何使用 Umdhexe 發現 Windows 上的內存洩漏獲取 UMDH 的使用說明(參見 參考資料)在本文中我將主要討論 UMDH 在分析存在洩漏的 JNI 應用程序時的輸出

  本文的 示例包 包含一個名為 LeakyJNIApp 的 Java 應用程序它循環調用一個 JNI 方法來洩漏本機內存UMDH 命令獲取當前的本機堆的快照以及分配每個內存區域的代碼路徑的本機堆棧軌跡快照通過獲取兩個快照並使用 UMDH 來分析差異您會得到兩個快照之間的堆增長報告

  對於 LeakyJNIApp差異文件包含以下信息

   // _NT_SYMBOL_PATH set by default to C:\WINDOWS\symbols
//
// Each log entry has the following syntax:
//
// + BYTES_DELTA (NEW_BYTES OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID
// + COUNT_DELTA (NEW_COUNT OLD_COUNT) BackTrace TRACEID allocations
//     stack trace
//
// where:
//
//     BYTES_DELTA increase in bytes between before and after log
//     NEW_BYTES bytes in after log
//     OLD_BYTES bytes in before log
//     COUNT_DELTA increase in allocations between before and after log
//     NEW_COUNT number of allocations in after log
//     OLD_COUNT number of allocations in before log
//     TRACEID decimal index of the stack trace in the trace database
//         (can be used to search for allocation instances in the original
//         UMDH logs)
//

( )    allocs     BackTrace

Total increase ==

  重要的一行是 + allocs BackTrace它顯示一個 backtrace 進行了 次分配而且分配的內存都沒有釋放 — 總共使用了 字節內存通過查看一個快照文件您可以將 BackTrace 與有意義的代碼路徑關聯起來在第一個快照文件中搜索 BackTrace可以找到如下信息

   AD bytes in x allocations (@ x + xF) by: BackTrace
        ntdll!RtlpNtMakeTemporaryKey+D
        ntdll!RtlInitializeSListHead+D
        ntdll!wcsncat+
        leakyjniapp!Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod+D

  這顯示內存洩漏來自 Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 函數中的 leakyjniappdll 模塊

  在編寫本文時Linux 沒有類似於 UMDH 或 LeakDiag 的工具但在 Linux 上仍然可以采用許多方式來調試本機內存洩漏Linux 上提供的許多內存調試器可分為以下類別

  預處理器級別這些工具需要將一個頭文件編譯到被測試的源代碼中可以使用這些工具之一重新編譯您自己的 JNI 庫以跟蹤您代碼中的本機內存洩漏除非您擁有 Java 運行時本身的源代碼否則這種方法無法在 JVM 中發現內存洩漏(甚至很難在隨後將這類工具編譯到 JVM 等大型項目中並且編譯非常耗時)Dmalloc 就是這類工具的一個例子(參見 參考資料)

  鏈接程序級別這些工具將被測試的二進制文件鏈接到一個調試庫再一次盡管這對個別 JNI 庫是可行的但不推薦將其用於整個 Java 運行時因為運行時供應商不太可能支持您運行修改的二進制文件Ccmalloc 是這類工具的一個例子(參見 參考資料)

  運行時鏈接程序級別這些工具使用 LD_PRELOAD 環境變量預先加載一個庫這個庫將標准內存例程替換為指定的版本這些工具不需要重新編譯或重新鏈接源代碼但其中許多工具與 Java 運行時不太兼容Java 運行時是一個復雜的系統可以以非常規的方式使用內存和線程這通常會干擾或破壞這類工具您可以試驗一下看看是否有一些工具適用於您的場景NJAMD 是這類工具的一個例子

  基於模擬程序Valgrind memcheck 工具是這類內存調試器的惟一例子它模擬底層處理器與 Java 運行時模擬 JVM 的方式類似可以在 Valgrind 下運行 Java 應用程序但是會有嚴重的性能影響(速度會減慢 倍)這意味著難以通過這種方式運行大型復雜的 Java 應用程序Valgrind 目前可在 Linux xAMDPPC 和 PPC 上使用如果您使用 Valgrind請在使用它之前嘗試使用最小的測試案例來將減輕性能問題(如果可能最好移除整個 Java 運行時)

  對於能夠容忍這種性能開銷的簡單場景Valgrind memcheck 是最簡單且用戶友好的免費工具它能夠為洩漏內存的代碼路徑提供完整的堆棧軌跡提供方式與 Windows 上的 UMDH 相同

  LeakyJNIApp 非常簡單能夠在 Valgrind 下運行當模擬的程序結束時Valgrind memcheck 工具能夠輸出洩漏的內存的匯總信息默認情況下LeakyJNIApp 程序會一直運行要使其在固定時期之後關閉可以將運行時間(以秒為單位)作為惟一的命令行參數進行傳遞

  一些 Java 運行時以非常規的方式使用線程堆棧和處理器寄存器這可能使一些調試工具產生混淆這些工具要求本機程序遵從寄存器使用和堆棧結構的標准約定當使用 Valgrind 調試存在內存洩漏的 JNI 應用程序時您可以發現許多與內存使用相關的警告並且一些線程堆棧看起來很奇怪這是由 Java 運行時在內部構造其數據的方式所導致的不用擔心

  要使用 Valgrind memcheck 工具跟蹤 LeakyJNIApp(在一行上)使用以下命令

   valgrind tracechildren=yes leakcheck=full
java Djavalibrarypath= comibmjtcdemosLeakyJNIApp

  ——tracechildren=yes 選項使 Valgrind 跟蹤由 Java 啟動器啟動的任何進程一些 Java 啟動器版本會重新執行其本身(它們從頭重新啟動其本身再次設置環境變量來改變行為)如果您未指定 ——tracechildren您將不能跟蹤實際的 Java 運行時

  ——leakcheck=full 選項請求在代碼運行結束時輸出對洩漏的代碼區域的完整堆棧軌跡而不只是匯總內存的狀態

  當該命令運行時Valgrind 輸出許多警告和錯誤(在此環境中其中大部分都是無意義的)最後按洩漏的內存量升序輸出存在洩漏的調用堆棧在 Linux x針對 LeakyJNIApp 的 Valgrind 輸出的匯總部分結尾如下

   ==== bytes in blocks are possibly lost in loss record of
====    at xAB: malloc (vg_replace_mallocc:)
====    by xED: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniappso)
====    by xCF: ???
====    by xCB: gpProtectedRunCallInMethod
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xCF: signalProtectAndRunGlue
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xED: jsig_protect
(in /usr/local/ibmjavai/jre/bin/libjprtso)
====    by xFD: gpProtectAndRun
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xA: gpCheckCallin
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xC: callStaticVoidMethod
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xD: main
(in /usr/local/ibmjavai/jre/bin/java)
====
====
==== ( direct indirect) bytes in blocks are definitely
lost in loss record of
====    at xAB: malloc (vg_replace_mallocc:)
====    by xED: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniappso)
====    by xCF: ???
====    by xCB: gpProtectedRunCallInMethod
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xCF: signalProtectAndRunGlue
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xED: jsig_protect
(in /usr/local/ibmjavai/jre/bin/libjprtso)
====    by xFD: gpProtectAndRun
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xA: gpCheckCallin
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xC: callStaticVoidMethod
(in /usr/local/ibmjavai/jre/bin/libjvmso)
====    by xD: main
(in /usr/local/ibmjavai/jre/bin/java)
====
==== LEAK SUMMARY:
====    definitely lost: bytes in blocks
====    indirectly lost: bytes in blocks
====      possibly lost: bytes in blocks
====    still reachable: bytes in blocks
====         suppressed: bytes in blocks
==== Reachable blocks (those to which a pointer was found) are not shown
==== To see them rerun with: leakcheck=full showreachable=yes

  堆棧的第二行顯示內存是由 comibmjtcdemosLeakyJNIAppnativeMethod() 方法洩漏的

  也可以使用一些專用調試應用程序來調試本機內存洩漏隨著時間的推移會有更多工具(包括開源和專用的)被開發出來這對於研究當前技術的發展現狀很有幫助

  就目前而言使用免費工具調試 Linux 上的本機內存洩漏比在 Windows 上完成相同的事情更具挑戰性UMDH 支持就地 調試 Windows 上本機內存洩漏在 Linux 上您可能需要進行一些傳統的調試而不是依賴工具來解決問題下面是一些建議的調試步驟

  提取測試案例生成一個獨立環境您需要能夠在該環境中再現本機內存洩漏這將使調試更加簡單

  盡可能縮小測試案例嘗試禁用函數來確定是哪些代碼路徑導致了本機內存洩漏如果您擁有自己的 JNI 庫可以嘗試一次禁用一個來確定是哪個庫導致了內存洩漏

  縮小 Java 堆大小Java 堆可能是進程的虛擬地址空間的最大使用者通過減小 Java 堆可以將更多空間提供給本機內存的其他使用者

  關聯本機進程大小一旦您獲得了本機內存隨時間的使用情況可以將其與應用程序工作負載和 GC 數據比較如果洩漏程度與負載級別成正比則意味著洩漏是由每個事務或操作路徑上的某個實體引起的如果當進行垃圾收集時本機進程大小顯著減小這意味著您沒遇到內存洩漏您擁有的是具有本機支持的對象組合(比如直接 ByteBuffer)通過縮小 Java 堆大小(從而迫使垃圾收集更頻繁地發生)或者在一個對象緩存中管理對象(而不是依賴於垃圾收集器來清理對象)您可以減少本機支持對象持有的內存量

  如果您確定內存洩漏或增長來自於 Java 運行時本身您可能需要聯系運行時供應商來進一步調試

  消除限制更改為

  使用 位 Java 運行時很容易遇到本機內存耗盡的情況因為地址空間相對較小 位操作系統提供的 GB 用戶空間通常小於系統附帶的物理內存量而且現代的數據密集型應用程序很容易耗盡可用空間

  如果 位地址空間不夠您的應用程序使用您可以通過移動到 位 Java 運行時來獲得更多用戶空間如果您運行的是 位操作系統那麼 位 Java 運行時將能夠滿足海量 Java 堆的需求還會減少與地址空間相關的問題 列出了 位操作系統上目前可用的用戶空間

  表 位操作系統上的用戶空間大小

   操作系統  默認用戶空間大小  Windows x  GB  Windows Itanium  GB  Linux x  GB  Linux PPC  GB  Linux  EB

  然而移動到 位並不是所有本機內存問題的通用解決方案您仍然需要足夠的物理內存來持有所有數據如果物理內存不夠 Java 運行時使用運行時性能將變得非常糟因為操作系統不得不在內存與交換空間之間來回復制 Java 運行時數據出於相同原因移動到 位也不是內存洩漏永恆的解決方案您只是提供了更多空間來供洩漏這只會延緩您不得不重啟應用程序的時間

  無法在 位運行時中使用 位本機代碼任何本機代碼(JNI 庫JVM Tool Interface [JVMTI]JVM Profiling Interface [JVMPI] 以及 JVM Debug Interface [JVMDI] 代理)都必須編譯為 位運行時的性能也可能比相同硬件上對應的 位運行時更慢 位運行時使用 位指針(本機地址引用)因此 位運行時上的 Java 對象會占用比 位運行時上包含相同數據的對象更多的空間更大的對象意味著要使用更大的堆來持有相同的數據量同時保持類似的 GC 性能這使操作系統和硬件緩存效率更低令人驚訝的是更大的 Java 堆並不一定意味著更長的 GC 暫停時間因為堆上的活動數據量可能不會增加並且一些 GC 算法在使用更大的堆時效率更高

  一些現代 Java 運行時包含減輕 對象膨脹 和改善性能的技術這些功能在 位運行時上使用更短的引用這在 IBM 實現中稱為壓縮引用而在 Sun 實現中稱為壓縮 oop

  對 Java 運行時性能的比較研究不屬於本文討論范圍但是如果您正在考慮移動到 盡早測試應用程序以理解其執行原理會很有幫助由於更改地址大小會影響到 Java 堆所以您將需要在新架構上重新調優您的 GC 設置而不是僅僅移植現有設置

  結束語

  在設計和運行大型 Java 應用程序時理解本機內存至關重要但是這一點通常被忽略因為它與復雜的硬件和操作系統細節密切相關Java 運行時的目的正是幫助我們規避這些細節JRE 是一個本機進程它必須在由這些紛繁復雜的細節定義的環境中工作要從 Java 應用程序中獲得最佳的性能您必須理解應用程序如何影響 Java 運行時的本機內存使用

  耗盡本機內存與耗盡 Java 堆很相似但它需要不同的工具集來調試和解決修復本機內存問題的關鍵在於理解運行您的 Java 應用程序的硬件和操作系統施加的限制並將其與操作系統工具知識結合起來監控本機內存使用通過這種方法您將能夠解決 Java 應用程序產生的一些非常棘手的問題

  下載

   描述 名字  大小  下載方法  本機內存示例代碼  jnativememorylinuxzip KB   linuxzip target=_blank>FTP


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26811.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.