日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

內(nèi)存詳解

 老匹夫 2014-02-06

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

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

本系列文章共分兩篇,討論不同平臺上的相應話題。本文是其中一篇。在這兩篇文章中,您將了解什么是本機內(nèi)存,Java 運行時如何使用它,本機內(nèi)存耗盡之后會發(fā)生什么情況,以及如何調(diào)試本機 OutOfMemoryError。本文介紹 Windows 和 Linux 平臺上的這一主題,不會介紹任何特定的運行時實現(xiàn)。另一篇 類似的文章 介紹 AIX 上的這一主題,著重介紹 IBM? Developer Kit for Java。(另一篇文章中關(guān)于 IBM 實現(xiàn)的信息也適合于除 AIX 之外的平臺,因此如果您在 Linux 上使用 IBM Developer Kit for Java,或使用 IBM 32-bit Runtime Environment for Windows,您會發(fā)現(xiàn)這篇文章也有用處)。

本機內(nèi)存簡介

我將首先解釋一下操作系統(tǒng)和底層硬件給本機內(nèi)存帶來的限制。如果您熟悉使用 C 等語言管理動態(tài)內(nèi)存,那么您可以直接跳到 下一節(jié)

硬件限制

本機進程遇到的許多限制都是由硬件造成的,而與操作系統(tǒng)沒有關(guān)系。每臺計算機都有一個處理器和一些隨機存取存儲器(RAM),后者也稱為物理內(nèi)存。處理器將數(shù)據(jù)流解釋為要執(zhí)行的指令,它擁有一個或多個處理單元,用于執(zhí)行整數(shù)和浮點運算以及更高級的計算。處理器具有許多寄存器 —— ??焖俚膬?nèi)存元素,用作被執(zhí)行的計算的工作存儲,寄存器大小決定了一次計算可使用的最大數(shù)值。

處理器通過內(nèi)存總線連接到物理內(nèi)存。物理地址(處理器用于索引物理 RAM 的地址)的大小限制了可以尋址的內(nèi)存。例如,一個 16 位物理地址可以尋址 0x0000 到 0xFFFF 的內(nèi)存地址,這個地址范圍包括 2^16 = 65536 個惟一的內(nèi)存位置。如果每個地址引用一個存儲字節(jié),那么一個 16 位物理地址將允許處理器尋址 64KB 內(nèi)存。

處理器被描述為特定數(shù)量的數(shù)據(jù)位。這通常指的是寄存器大小,但是也存在例外,比如 32 位 390 指的是物理地址大小。對于桌面和服務器平臺,這個數(shù)字為 31、32 或 64;對于嵌入式設備和微處理器,這個數(shù)字可能小至 4。物理地址大小可以與寄存器帶寬一樣大,也可以比它大或小。如果在適當?shù)牟僮飨到y(tǒng)上運行,大部分 64 位處理器可以運行 32 位程序。

表 1 列出了一些流行的 Linux 和 Windows 架構(gòu),以及它們的寄存器和物理地址大?。?/p>

表 1. 一些流行處理器架構(gòu)的寄存器和物理地址大小
架構(gòu)寄存器帶寬(位)物理地址大?。ㄎ唬?/th>
(現(xiàn)代)Intel? x863232
36,具有物理地址擴展(Pentium Pro 和更高型號)
x86 6464目前為 48 位(以后將會增大)
PPC6464在 POWER 5 上為 50 位
390 31 位3231
390 64 位6464

操作系統(tǒng)和虛擬內(nèi)存

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

在 Windows 和 Linux 等多任務操作系統(tǒng)中,有多個程序在使用系統(tǒng)資源。需要為每個程序分配物理內(nèi)存區(qū)域來在其中運行??梢栽O計這樣一個操作系統(tǒng):每個程序直接使用物理內(nèi)存,并且可以可靠地僅使用分配給它的內(nèi)存。一些嵌入式操作系統(tǒng)以這種方式工作,但是這在包含多個未經(jīng)過集中測試的應用程序的環(huán)境中是不切實際的,因為任何程序都可能破壞其他程序或者操作系統(tǒng)本身的內(nèi)存。

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

圖 1. 虛擬內(nèi)存將進程地址空間映射到物理資源
虛擬內(nèi)存映射

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

虛擬地址空間大小可能比處理器的物理地址大小更小。32 位 Intel x86 最初擁有的 32 位物理地址僅允許處理器尋址 4GB 存儲空間。后來,添加了一種稱為物理地址擴展(Physical Address Extension,PAE)的特性,將物理地址大小擴大到了 36 位,允許安裝或?qū)ぶ分炼?64GB RAM。PAE 允許操作系統(tǒng)將 32 位的 4GB 虛擬地址空間映射到一個較大的物理地址范圍,但是它不允許每個進程擁有 64GB 虛擬地址空間。這意味著如果您將大于 4GB 的內(nèi)存放入 32 位 Intel 服務器中,您將無法將所有內(nèi)存直接映射到一個單一進程中。

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

內(nèi)核空間和用戶空間

盡管每個進程都有其自己的地址空間,但程序通常無法使用所有這些空間。地址空間被劃分為用戶空間內(nèi)核空間。內(nèi)核是主要的操作系統(tǒng)程序,包含用于連接計算機硬件、調(diào)度程序以及提供聯(lián)網(wǎng)和虛擬內(nèi)存等服務的邏輯。

作為計算機啟動序列的一部分,操作系統(tǒng)內(nèi)核運行并初始化硬件。一旦內(nèi)核配置了硬件及其自己的內(nèi)部狀態(tài),第一個用戶空間進程就會啟動。如果用戶程序需要來自操作系統(tǒng)的服務,它可以執(zhí)行一種稱為系統(tǒng)調(diào)用 的操作與內(nèi)核程序交互,內(nèi)核程序然后執(zhí)行該請求。系統(tǒng)調(diào)用通常是讀取和寫入文件、聯(lián)網(wǎng)和啟動新進程等操作所必需的。

當執(zhí)行系統(tǒng)調(diào)用時,內(nèi)核需要訪問其自己的內(nèi)存和調(diào)用進程的內(nèi)存。因為正在執(zhí)行當前線程的處理器被配置為使用地址空間映射來為當前進程映射虛擬地址,所以大部分操作系統(tǒng)將每個進程地址空間的一部分映射到一個通用的內(nèi)核內(nèi)存區(qū)域。被映射來供內(nèi)核使用的地址空間部分稱為內(nèi)核空間,其余部分稱為用戶空間,可供用戶應用程序使用。

內(nèi)核空間和用戶空間之間的平衡關(guān)系因操作系統(tǒng)的不同而不同,甚至在運行于不同硬件架構(gòu)之上的同一操作系統(tǒng)的各個實例間也有所不同。這種平衡通常是可配置的,可進行調(diào)整來為用戶應用程序或內(nèi)核提供更多空間。縮減內(nèi)核區(qū)域可能導致一些問題,比如能夠同時登錄的用戶數(shù)量限制或能夠運行的進程數(shù)量限制。更小的用戶空間意味著應用程序編程人員只能使用更少的內(nèi)存空間。

默認情況下,32 位 Windows 擁有 2GB 用戶空間和 2GB 內(nèi)核空間。在一些 Windows 版本上,通過向啟動配置添加 /3GB 開關(guān)并使用 /LARGEADDRESSAWARE 開關(guān)重新鏈接應用程序,可以將這種平衡調(diào)整為 3GB 用戶空間和 1GB 內(nèi)核空間。在 32 位 Linux 上,默認設置為 3GB 用戶空間和 1GB 內(nèi)核空間。一些 Linux 分發(fā)版提供了一個 hugemem 內(nèi)核,支持 4GB 用戶空間。為了實現(xiàn)這種配置,將進行系統(tǒng)調(diào)用時使用的地址空間分配給內(nèi)核。通過這種方式增加用戶空間會減慢系統(tǒng)調(diào)用,因為每次進行系統(tǒng)調(diào)用時,操作系統(tǒng)必須在地址空間之間復制數(shù)據(jù)并重置進程地址-空間映射。圖 2 展示了 32 位 Windows 的地址-空間布局:

圖 2. 32 位 Windows 的地址-空間布局
Windows 32 位地址空間

圖 3 顯示了 32 位 Linux 的地址-空間配置:

圖 3. 32 位 Linux 的地址-空間布局
Linux 32 位地址空間

31 位 Linux 390 上還使用了一個獨立的內(nèi)核地址空間,其中較小的 2GB 地址空間使對單個地址空間進行劃分不太合理,但是,390 架構(gòu)可以同時使用多個地址空間,而且不會降低性能。

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

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

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

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

回頁首

Java 運行時如何使用本機內(nèi)存

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

Java 堆和垃圾收集

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

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

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

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

對于維護 Java 堆的內(nèi)存管理系統(tǒng),需要更多本機內(nèi)存來維護它的狀態(tài)。當進行垃圾收集時,必須分配數(shù)據(jù)結(jié)構(gòu)來跟蹤空閑存儲空間和記錄進度。這些數(shù)據(jù)結(jié)構(gòu)的確切大小和性質(zhì)因?qū)崿F(xiàn)的不同而不同,但許多數(shù)據(jù)結(jié)構(gòu)都與堆大小成正比。

即時 (JIT) 編譯器

JIT 編譯器在運行時編譯 Java 字節(jié)碼來優(yōu)化本機可執(zhí)行代碼。這極大地提高了 Java 運行時的速度,并且支持 Java 應用程序以與本機代碼相當?shù)乃俣冗\行。

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

類和類加載器

Java 應用程序由一些類組成,這些類定義對象結(jié)構(gòu)和方法邏輯。Java 應用程序也使用 Java 運行時類庫(比如 java.lang.String)中的類,也可以使用第三方庫。這些類需要存儲在內(nèi)存中以備使用。

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

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

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

  • Java 堆不包含對表示該類加載器的 java.lang.ClassLoader 對象的引用。
  • Java 堆不包含對表示類加載器加載的類的任何 java.lang.Class 對象的引用。
  • 在 Java 堆上,該類加載器加載的任何類的所有對象都不再存活(被引用)。

需要注意的是,Java 運行時為所有 Java 應用程序創(chuàng)建的 3 個默認類加載器( bootstrap、extensionapplication )都不可能滿足這些條件,因此,任何系統(tǒng)類(比如 java.lang.String)或通過應用程序類加載器加載的任何應用程序類都不能在運行時釋放。

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

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

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

當使用 java.lang.reflect API 時,Java 運行時必須將一個反射對象(比如 java.lang.reflect.Field)的方法連接到被反射到的對象或類。這可以通過使用 Java 本機接口(Java Native Interface,JNI)訪問器來完成,這種方法需要的設置很少,但是速度緩慢。也可以在運行時為您想要反射到的每種對象類型動態(tài)構(gòu)建一個類。后一種方法在設置上更慢,但運行速度更快,非常適合于經(jīng)常反射到一個特定類的應用程序。

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

JNI

JNI 支持本機代碼(使用 C 和 C++ 等本機編譯語言編寫的應用程序)調(diào)用 Java 方法,反之亦然。Java 運行時本身極大地依賴于 JNI 代碼來實現(xiàn)類庫功能,比如文件和網(wǎng)絡 I/O。JNI 應用程序可能通過 3 種方式增加 Java 運行時的本機內(nèi)存占用:

  • JNI 應用程序的本機代碼被編譯到共享庫中,或編譯為加載到進程地址空間中的可執(zhí)行文件。大型本機應用程序可能僅僅加載就會占用大量進程地址空間。
  • 本機代碼必須與 Java 運行時共享地址空間。任何本機代碼分配或本機代碼執(zhí)行的內(nèi)存映射都會耗用 Java 運行時的內(nèi)存。
  • 某些 JNI 函數(shù)可能在它們的常規(guī)操作中使用本機內(nèi)存。GetTypeArrayElementsGetTypeArrayRegion 函數(shù)可以將 Java 堆數(shù)據(jù)復制到本機內(nèi)存緩沖區(qū)中,以供本機代碼使用。是否復制數(shù)據(jù)依賴于運行時實現(xiàn)。(IBM Developer Kit for Java 5.0 和更高版本會進行本機復制)。通過這種方式訪問大量 Java 堆數(shù)據(jù)可能會使用大量本機堆。

NIO

Java 1.4 中添加的新 I/O (NIO) 類引入了一種基于通道和緩沖區(qū)來執(zhí)行 I/O 的新方式。就像 Java 堆上的內(nèi)存支持 I/O 緩沖區(qū)一樣,NIO 添加了對直接 ByteBuffer 的支持(使用 java.nio.ByteBuffer.allocateDirect() 方法進行分配), ByteBuffer 受本機內(nèi)存而不是 Java 堆支持。直接 ByteBuffer 可以直接傳遞到本機操作系統(tǒng)庫函數(shù),以執(zhí)行 I/O — 這使這些函數(shù)在一些場景中要快得多,因為它們可以避免在 Java 堆與本機堆之間復制數(shù)據(jù)。

對于在何處存儲直接 ByteBuffer 數(shù)據(jù),很容易產(chǎn)生混淆。應用程序仍然在 Java 堆上使用一個對象來編排 I/O 操作,但持有該數(shù)據(jù)的緩沖區(qū)將保存在本機內(nèi)存中,Java 堆對象僅包含對本機堆緩沖區(qū)的引用。非直接 ByteBuffer 將其數(shù)據(jù)保存在 Java 堆上的 byte[] 數(shù)組中。圖 4 展示了直接與非直接 ByteBuffer 對象之間的區(qū)別:

圖 4. 直接與非直接 java.nio.ByteBuffer 的內(nèi)存拓撲結(jié)構(gòu)
ByteBuffer 內(nèi)存安排

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

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

線程

應用程序中的每個線程都需要內(nèi)存來存儲器堆棧(用于在調(diào)用函數(shù)時持有局部變量并維護狀態(tài)的內(nèi)存區(qū)域)。每個 Java 線程都需要堆棧空間來運行。根據(jù)實現(xiàn)的不同,Java 線程可以分為本機線程和 Java 堆棧。除了堆??臻g,每個線程還需要為線程本地存儲(thread-local storage)和內(nèi)部數(shù)據(jù)結(jié)構(gòu)提供一些本機內(nèi)存。

堆棧大小因 Java 實現(xiàn)和架構(gòu)的不同而不同。一些實現(xiàn)支持為 Java 線程指定堆棧大小,其范圍通常在 256KB 到 756KB 之間。

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

回頁首

本機內(nèi)存耗盡會發(fā)生什么?

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

相反,一旦 Java 運行時開始運行并且應用程序處于穩(wěn)定狀態(tài),它可以在本機堆完全耗盡之后繼續(xù)正常運行。不一定會發(fā)生奇怪的行為,因為需要分配本機內(nèi)存的操作比需要分配 Java 堆的操作少得多。盡管需要本機內(nèi)存的操作因 JVM 實現(xiàn)不同而異,但也有一些操作很常見:啟動線程、加載類以及執(zhí)行某種類型的網(wǎng)絡和文件 I/O。

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

缺乏可預測行為意味著無法確定本機內(nèi)存是否耗盡。相反,您需要使用來自操作系統(tǒng)和 Java 運行時的數(shù)據(jù)執(zhí)行診斷。

回頁首

本機內(nèi)存耗盡示例

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

com.ibm.jtc.demos.NativeMemoryGlutton 類提供了 gobbleMemory() 方法,它在一個循環(huán)中調(diào)用 malloc,直到幾乎所有本機內(nèi)存都已耗盡。完成任務之后,它通過以下方式輸出分配給標準錯誤的字節(jié)數(shù):

Allocated 1953546736 bytes of native memory before running out

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

  • Linux x86
  • Linux PPC 32
  • Linux 390 31
  • Windows x86

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

java version "1.5.0_11"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_11-b03)
Java HotSpot(TM) Client VM (build 1.5.0_11-b03, mixed mode)

使用的 IBM Java 運行時版本為:

java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pwi32devifx-20071025 (SR
6b))
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Windows XP x86-32 j9vmwi3223-2007100
7 (JIT enabled)
J9VM - 20071004_14218_lHdSMR
JIT  - 20070820_1846ifx1_r8
GC   - 200708_10)
JCL  - 20071025

在耗盡本機內(nèi)存時嘗試啟動線程

com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation 類嘗試在耗盡進程地址空間時啟動一個線程。這是發(fā)現(xiàn) Java 進程已耗盡內(nèi)存的一種常用方式,因為許多應用程序都會在其整個生存期啟動線程。

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

Allocated 1019394912 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "systhrow", detail 
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:\Snap0001.20080323.182114.5172.trc'
JVMDUMP010I Snap Dump written to C:\Snap0001.20080323.182114.5172.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080323.182114.5172.phd'
JVMDUMP010I Heap Dump written to C:\heapdump.20080323.182114.5172.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080323.182114.5172.txt'
JVMDUMP010I Java Dump written to C:\javacore.20080323.182114.5172.txt
JVMDUMP013I Processed Dump Event "systhrow", detail "java/lang/OutOfMemoryError".
java.lang.OutOfMemoryError: ZIP006:OutOfMemoryError, ENOMEM error in ZipFile.open
   at java.util.zip.ZipFile.open(Native Method)
   at java.util.zip.ZipFile.<init>(ZipFile.java:238)
   at java.util.jar.JarFile.<init>(JarFile.java:169)
   at java.util.jar.JarFile.<init>(JarFile.java:107)
   at com.ibm.oti.vm.AbstractClassLoader.fillCache(AbstractClassLoader.java:69)
   at com.ibm.oti.vm.AbstractClassLoader.getResourceAsStream(AbstractClassLoader.java:113)
   at java.util.ResourceBundle$1.run(ResourceBundle.java:1101)
   at java.security.AccessController.doPrivileged(AccessController.java:197)
   at java.util.ResourceBundle.loadBundle(ResourceBundle.java:1097)
   at java.util.ResourceBundle.findBundle(ResourceBundle.java:942)
   at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:779)
   at java.util.ResourceBundle.getBundle(ResourceBundle.java:716)
   at com.ibm.oti.vm.MsgHelp.setLocale(MsgHelp.java:103)
   at com.ibm.oti.util.Msg$1.run(Msg.java:44)
   at java.security.AccessController.doPrivileged(AccessController.java:197)
   at com.ibm.oti.util.Msg.<clinit>(Msg.java:41)
   at java.lang.J9VMInternals.initializeImpl(Native Method)
   at java.lang.J9VMInternals.initialize(J9VMInternals.java:194)
   at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:764)
   at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:758)
   at java.lang.Thread.uncaughtException(Thread.java:1315)
K0319java.lang.OutOfMemoryError: Failed to fork OS thread
   at java.lang.Thread.startImpl(Native Method)
   at java.lang.Thread.start(Thread.java:979)
   at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

調(diào)用 java.lang.Thread.start() 來嘗試為一個新的操作系統(tǒng)線程分配內(nèi)存。此嘗試會失敗并拋出 OutOfMemoryErrorJVMDUMP 行通知用戶 Java 運行時已經(jīng)生成了標準的 OutOfMemoryError 調(diào)試數(shù)據(jù)。

嘗試處理第一個 OutOfMemoryError 會導致第二個錯誤 —— :OutOfMemoryError, ENOMEM error in ZipFile.open。當本機進程內(nèi)存耗盡時通常會拋出多個 OutOfMemoryError。Failed to fork OS thread 可能是在耗盡本機內(nèi)存時最常見的消息。

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

在 Sun Java 運行時上執(zhí)行相同的測試案例時,會生成以下控制臺輸出:

Allocated 1953546736 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
   at java.lang.Thread.start0(Native Method)
   at java.lang.Thread.start(Thread.java:574)
   at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

盡管堆棧軌跡和錯誤消息稍有不同,但其行為在本質(zhì)上是一樣的:本機分配失敗并拋出 java.lang.OutOfMemoryError。此場景中拋出的 OutOfMemoryError 與由于 Java 堆耗盡而拋出的錯誤的惟一區(qū)別在于消息。

嘗試在本機內(nèi)存耗盡時分配直接 ByteBuffer

com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation 類嘗試在地址空間耗盡時分配一個直接(也就是受本機支持的)java.nio.ByteBuffer 對象。當在 IBM Java 運行時上運行時,它生成以下輸出:

Allocated 1019481472 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "uncaught", detail
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:\Snap0001.20080324.100721.4232.trc'
JVMDUMP010I Snap Dump written to C:\Snap0001.20080324.100721.4232.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080324.100721.4232.phd'
JVMDUMP010I Heap Dump written to C:\heapdump.20080324.100721.4232.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080324.100721.4232.txt'
JVMDUMP010I Java Dump written to C:\javacore.20080324.100721.4232.txt
JVMDUMP013I Processed Dump Event "uncaught", detail "java/lang/OutOfMemoryError".
Exception in thread "main" java.lang.OutOfMemoryError: 
Unable to allocate 1048576 bytes of direct memory after 5 retries
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:167)
   at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:303)
   at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
   DirectByteBufferUnderNativeStarvation.java:29)
Caused by: java.lang.OutOfMemoryError
   at sun.misc.Unsafe.allocateMemory(Native Method)
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:154)
   ... 2 more

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

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

Allocated 1953546760 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError
   at sun.misc.Unsafe.allocateMemory(Native Method)
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)
   at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
   at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
DirectByteBufferUnderNativeStarvation.java:29)

回頁首

調(diào)試方法和技術(shù)

查閱供應商文檔

本文提供的指南是一般的調(diào)試原則,可用于理解本機內(nèi)存耗盡場景。您的運行時供應商可能提供了自己的調(diào)試說明,供應商期望您按照這些說明與其支持團隊聯(lián)系。如果您要與運行時供應商(包括 IBM)合作解決問題,請始終檢查其調(diào)試和診斷文檔,查看在提交問題報告時應該執(zhí)行哪些步驟。

當出現(xiàn) java.lang.OutOfMemoryError 或看到有關(guān)內(nèi)存不足的錯誤消息時,要做的第一件事是確定哪種類型的內(nèi)存被耗盡。最簡單的方式是首先檢查 Java 堆是否被填滿。如果 Java 堆未導致 OutOfMemory 條件,那么您應該分析本機堆使用情況。

檢查 Java 堆

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

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

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

1STGCHTYPE     GC History  
3STHSTTYPE     09:59:01:632262775 GMT j9mm.80 -   J9AllocateObject() returning NULL!
32 bytes requested for object of class 00147F80

J9AllocateObject() returning NULL! 意味著 Java 堆分配例程未成功完成,并且將拋出 OutOfMemoryError。

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

1STGCHTYPE     GC History  
3STHSTTYPE     09:59:01:632262775 GMT j9mm.83 -     Forcing J9AllocateObject()
to fail due to excessive GC
1STGCHTYPE     GC History  
3STHSTTYPE     09:59:01:632262775 GMT j9mm.84 -     Forcing 
J9AllocateIndexableObject() to fail due to excessive GC

當 Sun 實現(xiàn)耗盡 Java 堆內(nèi)存時,它使用異常消息來顯示它耗盡的是 Java 堆:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

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

測量本機堆使用情況

如果您確定內(nèi)存耗盡情況不是由 Java 堆耗盡引起的,那么下一步就是分析您的本機內(nèi)存使用情況。

Windows 提供的 PerfMon 工具可用于監(jiān)控和記錄許多操作系統(tǒng)和進程指標,包括本機內(nèi)存使用(參見 參考資料)。它允許實時跟蹤計數(shù)器,或?qū)⑵浯鎯υ谌罩疚募幸怨╇x線查看。使用 Private Bytes 計數(shù)器顯示總體地址空間使用情況。如果顯示值接近于用戶空間的限制(前面已經(jīng)討論過,介于 2 到 3GB 之間),您應該會看到本機內(nèi)存耗盡情況。

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

GCMV 最初編寫用于分析冗長的 GC 日志,允許用戶在調(diào)優(yōu)垃圾收集器時查看 Java 堆使用情況和 GC 性能的變化。GCMV 后來進行了擴展,支持分析其他數(shù)據(jù)源,包括 Linux 和 AIX 本機內(nèi)存數(shù)據(jù)。GCMV 是作為 IBM Support Assistant (ISA) 的插件發(fā)布的。

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

  1. 下載并安裝 ISA Version 4(或更高版本),然后安裝 GCMV 工具插件。
  2. 啟動 ISA。
  3. 從菜單欄單擊 Help >> Help Contents,打開 ISA 幫助菜單。
  4. 在左側(cè)窗格的 Tool:IBM 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 本機內(nèi)存說明。

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

圖 5. Linux 本機內(nèi)存數(shù)據(jù)捕獲腳本在 ISA 幫助對話框中的位置
IBM Support Assistant 幫助文件

GCMV 幫助文件中提供的腳本使用的 ps 命令僅適用于最新的 ps 版本。在一些舊的 Linux 分發(fā)版中,幫助文件中的命令將會生成錯誤信息。要查看您的 Linux 分發(fā)版上的行為,可以嘗試運行 ps -o pid,vsz=VSZ,rss=RSS。如果您的 ps 版本支持新的命令行參數(shù)語法,那么得到的輸出將類似于:

  PID    VSZ   RSS
 5826   3772  1960
 5675   2492   760

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

  PID VSZ,rss=RSS
 5826        3772
 5674        2488

如果您在一個較老的 ps 版本上運行,可以修改本機內(nèi)存腳本,將

ps -p $PID -o pid,vsz=VSZ,rss=RSS

行替換為

ps -p $PID -o pid,vsz,rss

將幫助面板中的腳本復制到一個文件中(在本例中名為 memscript.sh),找到您想要監(jiān)控的 Java 進程的進程 ID (PID)(本例中為 1234)并運行:

./memscript.sh 1234 > ps.out

這會把本機內(nèi)存日志寫入到 ps.out 中。要分析內(nèi)存使用情況:

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

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

圖 6. GCMV 的 Linux 本機內(nèi)存使用示例,其中顯示了前期階段
GCMV 本機內(nèi)存使用

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

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

  • 減少本機內(nèi)存使用。縮小 Java 堆大小是一個好的開端。
  • 限制本機內(nèi)存使用。如果您的本機內(nèi)存隨負載增加而增加,可以采取某種方式限制負載或為負載分配的資源。
  • 增加可用地址空間。這可以通過以下方式實現(xiàn):調(diào)優(yōu)您的操作系統(tǒng)(例如,在 Windows 上使用 /3GB 開關(guān)增加用戶空間,或者在 Linux 上使用龐大的內(nèi)核空間),更換平臺(Linux 通常擁有比 Windows 更多的用戶空間),或者 轉(zhuǎn)移到 64 位操作系統(tǒng)。

一種實際的本機內(nèi)存泄漏表現(xiàn)為本機堆的持續(xù)增長,這些內(nèi)存不會在移除負載或運行垃圾收集器時減少。內(nèi)存泄漏程度因負載不同而不同,但泄漏的總內(nèi)存不會下降。泄漏的內(nèi)存不可能被引用,因此它可以被交換出去,并保持被交換出去的狀態(tài)。

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

是什么在使用本機內(nèi)存?

一旦確定本機內(nèi)存被耗盡,下一個邏輯問題是:是什么在使用這些內(nèi)存?這個問題很難回答,因為在默認情況下,Windows 和 Linux 不會存儲關(guān)于分配給特定內(nèi)存塊的代碼路徑的信息。

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

  • Java 堆占用的內(nèi)存至少為 -Xmx 值。
  • 每個 Java 線程需要堆??臻g。堆??臻g因?qū)崿F(xiàn)不同而異,但是如果使用默認設置,每個線程至多會占用 756KB 本機內(nèi)存。
  • 直接 ByteBuffer 至少會占用提供給 allocate() 例程的內(nèi)存值。

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

Microsoft 提供了 UMDH(用戶模式轉(zhuǎn)儲堆)和 LeakDiag 工具來在 Windows 上調(diào)試本機內(nèi)存增長(參見 參考資料)。這兩個工具的機制相同:記錄特定內(nèi)存區(qū)域被分配給了哪個代碼路徑,并提供一種方式來定位所分配的內(nèi)存不會在以后被釋放的代碼部分。我建議您查閱文章 “Umdhtools.exe:如何使用 Umdh.exe 發(fā)現(xiàn) Windows 上的內(nèi)存泄漏”,獲取 UMDH 的使用說明(參見 參考資料)。在本文中,我將主要討論 UMDH 在分析存在泄漏的 JNI 應用程序時的輸出。

本文的 示例包 包含一個名為 LeakyJNIApp 的 Java 應用程序,它循環(huán)調(diào)用一個 JNI 方法來泄漏本機內(nèi)存。UMDH 命令獲取當前的本機堆的快照,以及分配每個內(nèi)存區(qū)域的代碼路徑的本機堆棧軌跡快照。通過獲取兩個快照,并使用 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).
//

+  412192 ( 1031943 - 619751)    963 allocs     BackTrace00468

Total increase == 412192

重要的一行是 + 412192 ( 1031943 - 619751) 963 allocs BackTrace00468。它顯示一個 backtrace 進行了 963 次分配,而且分配的內(nèi)存都沒有釋放 — 總共使用了 412192 字節(jié)內(nèi)存。通過查看一個快照文件,您可以將 BackTrace00468 與有意義的代碼路徑關(guān)聯(lián)起來。在第一個快照文件中搜索 BackTrace00468,可以找到如下信息:

000000AD bytes in 0x1 allocations (@ 0x00000031 + 0x0000001F) by: BackTrace00468
        ntdll!RtlpNtMakeTemporaryKey+000074D0
        ntdll!RtlInitializeSListHead+00010D08
        ntdll!wcsncat+00000224
        leakyjniapp!Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod+000000D6

這顯示內(nèi)存泄漏來自 Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 函數(shù)中的 leakyjniapp.dll 模塊。

在編寫本文時,Linux 沒有類似于 UMDH 或 LeakDiag 的工具。但在 Linux 上仍然可以采用許多方式來調(diào)試本機內(nèi)存泄漏。Linux 上提供的許多內(nèi)存調(diào)試器可分為以下類別:

  • 預處理器級別。這些工具需要將一個頭文件編譯到被測試的源代碼中。可以使用這些工具之一重新編譯您自己的 JNI 庫,以跟蹤您代碼中的本機內(nèi)存泄漏。除非您擁有 Java 運行時本身的源代碼,否則這種方法無法在 JVM 中發(fā)現(xiàn)內(nèi)存泄漏(甚至很難在隨后將這類工具編譯到 JVM 等大型項目中,并且編譯非常耗時)。Dmalloc 就是這類工具的一個例子(參見 參考資料)。
  • 鏈接程序級別。這些工具將被測試的二進制文件鏈接到一個調(diào)試庫。再一次,盡管這對個別 JNI 庫是可行的,但不推薦將其用于整個 Java 運行時,因為運行時供應商不太可能支持您運行修改的二進制文件。Ccmalloc 是這類工具的一個例子(參見 參考資料)。
  • 運行時鏈接程序級別。這些工具使用 LD_PRELOAD 環(huán)境變量預先加載一個庫,這個庫將標準內(nèi)存例程替換為指定的版本。這些工具不需要重新編譯或重新鏈接源代碼,但其中許多工具與 Java 運行時不太兼容。Java 運行時是一個復雜的系統(tǒng),可以以非常規(guī)的方式使用內(nèi)存和線程,這通常會干擾或破壞這類工具。您可以試驗一下,看看是否有一些工具適用于您的場景。NJAMD 是這類工具的一個例子(參見 參考資料)。
  • 基于模擬程序。Valgrind memcheck 工具是這類內(nèi)存調(diào)試器的惟一例子(參見 參考資料)。它模擬底層處理器,與 Java 運行時模擬 JVM 的方式類似。可以在 Valgrind 下運行 Java 應用程序,但是會有嚴重的性能影響(速度會減慢 10 到 30 倍),這意味著難以通過這種方式運行大型、復雜的 Java 應用程序。Valgrind 目前可在 Linux x86、AMD64、PPC 32 和 PPC 64 上使用。如果您使用 Valgrind,請在使用它之前嘗試使用最小的測試案例來將減輕性能問題(如果可能,最好移除整個 Java 運行時)。

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

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

一些 Java 運行時以非常規(guī)的方式使用線程堆棧和處理器寄存器,這可能使一些調(diào)試工具產(chǎn)生混淆,這些工具要求本機程序遵從寄存器使用和堆棧結(jié)構(gòu)的標準約定。當使用 Valgrind 調(diào)試存在內(nèi)存泄漏的 JNI 應用程序時,您可以發(fā)現(xiàn)許多與內(nèi)存使用相關(guān)的警告,并且一些線程堆??雌饋砗芷婀?,這是由 Java 運行時在內(nèi)部構(gòu)造其數(shù)據(jù)的方式所導致的,不用擔心。

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

valgrind --trace-children=yes --leak-check=full 
java -Djava.library.path=. com.ibm.jtc.demos.LeakyJNIApp 10

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

--leak-check=full 選項請求在代碼運行結(jié)束時輸出對泄漏的代碼區(qū)域的完整堆棧軌跡,而不只是匯總內(nèi)存的狀態(tài)。

當該命令運行時,Valgrind 輸出許多警告和錯誤(在此環(huán)境中,其中大部分都是無意義的),最后按泄漏的內(nèi)存量升序輸出存在泄漏的調(diào)用堆棧。在 Linux x86 上,針對 LeakyJNIApp 的 Valgrind 輸出的匯總部分結(jié)尾如下:

==20494== 8,192 bytes in 8 blocks are possibly lost in loss record 36 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main 
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494== 
==20494== 
==20494== 65,536 (63,488 direct, 2,048 indirect) bytes in 62 blocks are definitely 
lost in loss record 42 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin 
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main 
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494== 
==20494== LEAK SUMMARY:
==20494==    definitely lost: 63,957 bytes in 69 blocks.
==20494==    indirectly lost: 2,168 bytes in 12 blocks.
==20494==      possibly lost: 8,600 bytes in 11 blocks.
==20494==    still reachable: 5,156,340 bytes in 980 blocks.
==20494==         suppressed: 0 bytes in 0 blocks.
==20494== Reachable blocks (those to which a pointer was found) are not shown.
==20494== To see them, rerun with: --leak-check=full --show-reachable=yes

堆棧的第二行顯示內(nèi)存是由 com.ibm.jtc.demos.LeakyJNIApp.nativeMethod() 方法泄漏的。

也可以使用一些專用調(diào)試應用程序來調(diào)試本機內(nèi)存泄漏。隨著時間的推移,會有更多工具(包括開源和專用的)被開發(fā)出來,這對于研究當前技術(shù)的發(fā)展現(xiàn)狀很有幫助。

就目前而言,使用免費工具調(diào)試 Linux 上的本機內(nèi)存泄漏比在 Windows 上完成相同的事情更具挑戰(zhàn)性。UMDH 支持就地 調(diào)試 Windows 上本機內(nèi)存泄漏,在 Linux 上,您可能需要進行一些傳統(tǒng)的調(diào)試,而不是依賴工具來解決問題。下面是一些建議的調(diào)試步驟:

  • 提取測試案例。生成一個獨立環(huán)境,您需要能夠在該環(huán)境中再現(xiàn)本機內(nèi)存泄漏。這將使調(diào)試更加簡單。
  • 盡可能縮小測試案例。嘗試禁用函數(shù)來確定是哪些代碼路徑導致了本機內(nèi)存泄漏。如果您擁有自己的 JNI 庫,可以嘗試一次禁用一個來確定是哪個庫導致了內(nèi)存泄漏。
  • 縮小 Java 堆大小。Java 堆可能是進程的虛擬地址空間的最大使用者。通過減小 Java 堆,可以將更多空間提供給本機內(nèi)存的其他使用者。
  • 關(guān)聯(lián)本機進程大小。一旦您獲得了本機內(nèi)存隨時間的使用情況,可以將其與應用程序工作負載和 GC 數(shù)據(jù)比較。如果泄漏程度與負載級別成正比,則意味著泄漏是由每個事務或操作路徑上的某個實體引起的。如果當進行垃圾收集時,本機進程大小顯著減小,這意味著您沒遇到內(nèi)存泄漏,您擁有的是具有本機支持的對象組合(比如直接 ByteBuffer)。通過縮小 Java 堆大小(從而迫使垃圾收集更頻繁地發(fā)生),或者在一個對象緩存中管理對象(而不是依賴于垃圾收集器來清理對象),您可以減少本機支持對象持有的內(nèi)存量。

如果您確定內(nèi)存泄漏或增長來自于 Java 運行時本身,您可能需要聯(lián)系運行時供應商來進一步調(diào)試。

回頁首

消除限制:更改為 64 位

使用 32 位 Java 運行時很容易遇到本機內(nèi)存耗盡的情況,因為地址空間相對較小。32 位操作系統(tǒng)提供的 2 到 4GB 用戶空間通常小于系統(tǒng)附帶的物理內(nèi)存量,而且現(xiàn)代的數(shù)據(jù)密集型應用程序很容易耗盡可用空間。

如果 32 位地址空間不夠您的應用程序使用,您可以通過移動到 64 位 Java 運行時來獲得更多用戶空間。如果您運行的是 64 位操作系統(tǒng),那么 64 位 Java 運行時將能夠滿足海量 Java 堆的需求,還會減少與地址空間相關(guān)的問題。表 2 列出了 64 位操作系統(tǒng)上目前可用的用戶空間。

表 2. 64 位操作系統(tǒng)上的用戶空間大小
操作系統(tǒng)默認用戶空間大小
Windows x86-648192GB
Windows Itanium7152GB
Linux x86-64500GB
Linux PPC641648GB
Linux 390 644EB

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

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

一些現(xiàn)代 Java 運行時包含減輕 64 位 “對象膨脹” 和改善性能的技術(shù)。這些功能在 64 位運行時上使用更短的引用。這在 IBM 實現(xiàn)中稱為壓縮引用,而在 Sun 實現(xiàn)中稱為壓縮 oop。

對 Java 運行時性能的比較研究不屬于本文討論范圍,但是如果您正在考慮移動到 64 位,盡早測試應用程序以理解其執(zhí)行原理會很有幫助。由于更改地址大小會影響到 Java 堆,所以您將需要在新架構(gòu)上重新調(diào)優(yōu)您的 GC 設置,而不是僅僅移植現(xiàn)有設置。

回頁首

結(jié)束語

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

耗盡本機內(nèi)存與耗盡 Java 堆很相似,但它需要不同的工具集來調(diào)試和解決。修復本機內(nèi)存問題的關(guān)鍵在于理解運行您的 Java 應用程序的硬件和操作系統(tǒng)施加的限制,并將其與操作系統(tǒng)工具知識結(jié)合起來,監(jiān)控本機內(nèi)存使用。通過這種方法,您將能夠解決 Java 應用程序產(chǎn)生的一些非常棘手的問題。

回頁首

下載

描述名字大小
本機內(nèi)存示例代碼j-nativememory-linux.zip115KB

參考資料

學習

獲得產(chǎn)品和技術(shù)

  • Valgrind:下載 Valgrind Instrumentation Framework,其中包括內(nèi)存錯誤檢測器。
  • Dmalloc:下載 Debug Malloc 庫。
  • ccmalloc:下載 ccmalloc 內(nèi)存調(diào)試器庫。
  • NJAMD:下載 NJAMD(它不僅僅是另一個 Malloc 調(diào)試器)內(nèi)存調(diào)試器庫。
  • IBM Monitoring and Diagnostic Tools for Java:訪問 IBM Java 工具頁面。
  • IBM Support Assistant (ISA):這個免費支持框架包含 Garbage Collection and Memory Visualizer 和 IBM Guided Activity Assistant 等工具,可用于調(diào)試本機內(nèi)存耗盡情況。

討論

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多