Java 堆(每個(gè) Java 對象在其中分配)是您在編寫 Java 應(yīng)用程序時(shí)使用最頻繁的內(nèi)存區(qū)域。JVM 設(shè)計(jì)用于將我們與主機(jī)的特性隔離,所以將內(nèi)存當(dāng)作堆來考慮再正常不過了。您一定遇到過 Java 堆 OutOfMemoryError
,它可能是由于對象泄漏造成的,也可能是因?yàn)槎训拇笮〔蛔阋源鎯λ袛?shù)據(jù),您也可能了解這些場景的一些調(diào)試技巧。但是隨著您的 Java 應(yīng)用程序處理越來越多的數(shù)據(jù)和越來越多的并發(fā)負(fù)載,您可能就會遇到無法使用常規(guī)技巧進(jìn)行修復(fù)的 OutOfMemoryError
。在一些場景中,即使 java 堆未滿,也會拋出錯(cuò)誤。當(dāng)這類場景發(fā)生時(shí),您需要理解 Java 運(yùn)行時(shí)環(huán)境(Java Runtime Environment,JRE)內(nèi)部到底發(fā)生了什么。
Java 應(yīng)用程序在 Java 運(yùn)行時(shí)的虛擬化環(huán)境中運(yùn)行,但是運(yùn)行時(shí)本身是使用 C 之類的語言編寫的本機(jī)程序,它也會耗用本機(jī)資源,包括本機(jī)內(nèi)存。本機(jī)內(nèi)存是可用于運(yùn)行時(shí)進(jìn)程的內(nèi)存,它與 Java 應(yīng)用程序使用的 java 堆內(nèi)存不同。每種虛擬化資源(包括 Java 堆和 Java 線程)都必須存儲在本機(jī)內(nèi)存中,虛擬機(jī)在運(yùn)行時(shí)使用的數(shù)據(jù)也是如此。這意味著主機(jī)的硬件和操作系統(tǒng)施加在本機(jī)內(nèi)存上的限制會影響到 Java 應(yīng)用程序的性能。
本系列文章共分兩篇,討論不同平臺上的相應(yīng)話題。本文是其中一篇。在這兩篇文章中,您將了解什么是本機(jī)內(nèi)存,Java 運(yùn)行時(shí)如何使用它,本機(jī)內(nèi)存耗盡之后會發(fā)生什么情況,以及如何調(diào)試本機(jī) OutOfMemoryError
。本文介紹 Windows 和 Linux 平臺上的這一主題,不會介紹任何特定的運(yùn)行時(shí)實(shí)現(xiàn)。另一篇 類似的文章 介紹 AIX 上的這一主題,著重介紹 IBM? Developer Kit for Java。(另一篇文章中關(guān)于 IBM 實(shí)現(xiàn)的信息也適合于除 AIX 之外的平臺,因此如果您在 Linux 上使用 IBM Developer Kit for Java,或使用 IBM 32-bit Runtime Environment for Windows,您會發(fā)現(xiàn)這篇文章也有用處)。
我將首先解釋一下操作系統(tǒng)和底層硬件給本機(jī)內(nèi)存帶來的限制。如果您熟悉使用 C 等語言管理動(dòng)態(tài)內(nèi)存,那么您可以直接跳到 下一節(jié)。
本機(jī)進(jìn)程遇到的許多限制都是由硬件造成的,而與操作系統(tǒng)沒有關(guān)系。每臺計(jì)算機(jī)都有一個(gè)處理器和一些隨機(jī)存取存儲器(RAM),后者也稱為物理內(nèi)存。處理器將數(shù)據(jù)流解釋為要執(zhí)行的指令,它擁有一個(gè)或多個(gè)處理單元,用于執(zhí)行整數(shù)和浮點(diǎn)運(yùn)算以及更高級的計(jì)算。處理器具有許多寄存器 —— ??焖俚膬?nèi)存元素,用作被執(zhí)行的計(jì)算的工作存儲,寄存器大小決定了一次計(jì)算可使用的最大數(shù)值。
處理器通過內(nèi)存總線連接到物理內(nèi)存。物理地址(處理器用于索引物理 RAM 的地址)的大小限制了可以尋址的內(nèi)存。例如,一個(gè) 16 位物理地址可以尋址 0x0000 到 0xFFFF 的內(nèi)存地址,這個(gè)地址范圍包括 2^16 = 65536 個(gè)惟一的內(nèi)存位置。如果每個(gè)地址引用一個(gè)存儲字節(jié),那么一個(gè) 16 位物理地址將允許處理器尋址 64KB 內(nèi)存。
處理器被描述為特定數(shù)量的數(shù)據(jù)位。這通常指的是寄存器大小,但是也存在例外,比如 32 位 390 指的是物理地址大小。對于桌面和服務(wù)器平臺,這個(gè)數(shù)字為 31、32 或 64;對于嵌入式設(shè)備和微處理器,這個(gè)數(shù)字可能小至 4。物理地址大小可以與寄存器帶寬一樣大,也可以比它大或小。如果在適當(dāng)?shù)牟僮飨到y(tǒng)上運(yùn)行,大部分 64 位處理器可以運(yùn)行 32 位程序。
表 1 列出了一些流行的 Linux 和 Windows 架構(gòu),以及它們的寄存器和物理地址大?。?/p>
架構(gòu) | 寄存器帶寬(位) | 物理地址大小(位) |
---|---|---|
(現(xiàn)代)Intel? x86 | 32 | 32 36,具有物理地址擴(kuò)展(Pentium Pro 和更高型號) |
x86 64 | 64 | 目前為 48 位(以后將會增大) |
PPC64 | 64 | 在 POWER 5 上為 50 位 |
390 31 位 | 32 | 31 |
390 64 位 | 64 | 64 |
如果您編寫無需操作系統(tǒng),直接在處理器上運(yùn)行的應(yīng)用程序,您可以使用處理器可以尋址的所有內(nèi)存(假設(shè)連接到了足夠的物理 RAM)。但是要使用多任務(wù)和硬件抽象等特性,幾乎所有人都會使用某種類型的操作系統(tǒng)來運(yùn)行他們的程序。
在 Windows 和 Linux 等多任務(wù)操作系統(tǒng)中,有多個(gè)程序在使用系統(tǒng)資源。需要為每個(gè)程序分配物理內(nèi)存區(qū)域來在其中運(yùn)行??梢栽O(shè)計(jì)這樣一個(gè)操作系統(tǒng):每個(gè)程序直接使用物理內(nèi)存,并且可以可靠地僅使用分配給它的內(nèi)存。一些嵌入式操作系統(tǒng)以這種方式工作,但是這在包含多個(gè)未經(jīng)過集中測試的應(yīng)用程序的環(huán)境中是不切實(shí)際的,因?yàn)槿魏纬绦蚨伎赡芷茐钠渌绦蚧蛘卟僮飨到y(tǒng)本身的內(nèi)存。
虛擬內(nèi)存 允許多個(gè)進(jìn)程共享物理內(nèi)存,而且不會破壞彼此的數(shù)據(jù)。在具有虛擬內(nèi)存的操作系統(tǒng)(比如 Windows、Linux 和許多其他操作系統(tǒng))中,每個(gè)程序都擁有自己的虛擬地址空間 —— 一個(gè)邏輯地址區(qū)域,其大小由該系統(tǒng)上的地址大小規(guī)定(所以,桌面和服務(wù)器平臺的虛擬地址空間為 31、32 或 64 位)。進(jìn)程的虛擬地址空間中的區(qū)域可被映射到物理內(nèi)存、文件或任何其他可尋址存儲。當(dāng)數(shù)據(jù)未使用時(shí),操作系統(tǒng)可以在物理內(nèi)存與一個(gè)交換區(qū)域(Windows 上的頁面文件 或者 Linux 上的交換分區(qū))之間移動(dòng)它,以實(shí)現(xiàn)對物理內(nèi)存的最佳利用率。當(dāng)一個(gè)程序嘗試使用虛擬地址訪問內(nèi)存時(shí),操作系統(tǒng)連同片上硬件會將該虛擬地址映射到物理位置,這個(gè)位置可以是物理 RAM、一個(gè)文件或頁面文件/交換分區(qū)。如果一個(gè)內(nèi)存區(qū)域被移動(dòng)到交換空間,那么它將在被使用之前加載回物理內(nèi)存中。圖 1 展示了虛擬內(nèi)存如何將進(jìn)程地址空間區(qū)域映射到共享資源:
程序的每個(gè)實(shí)例以進(jìn)程 的形式運(yùn)行。在 Linux 和 Windows 上,進(jìn)程是一個(gè)由受操作系統(tǒng)控制的資源(比如文件和套接字信息)、一個(gè)典型的虛擬地址空間(在某些架構(gòu)上不止一個(gè))和至少一個(gè)執(zhí)行線程構(gòu)成的集合。
虛擬地址空間大小可能比處理器的物理地址大小更小。32 位 Intel x86 最初擁有的 32 位物理地址僅允許處理器尋址 4GB 存儲空間。后來,添加了一種稱為物理地址擴(kuò)展(Physical Address Extension,PAE)的特性,將物理地址大小擴(kuò)大到了 36 位,允許安裝或?qū)ぶ分炼?64GB RAM。PAE 允許操作系統(tǒng)將 32 位的 4GB 虛擬地址空間映射到一個(gè)較大的物理地址范圍,但是它不允許每個(gè)進(jìn)程擁有 64GB 虛擬地址空間。這意味著如果您將大于 4GB 的內(nèi)存放入 32 位 Intel 服務(wù)器中,您將無法將所有內(nèi)存直接映射到一個(gè)單一進(jìn)程中。
地址窗口擴(kuò)展(Address Windowing Extension)特性允許 Windows 進(jìn)程將其 32 位地址空間的一部分作為滑動(dòng)窗口映射到較大的內(nèi)存區(qū)域中。Linux 使用類似的技術(shù)將內(nèi)存區(qū)域映射到虛擬地址空間中。這意味著盡管您無法直接引用大于 4GB 的內(nèi)存,但您仍然可以使用較大的內(nèi)存區(qū)域。
盡管每個(gè)進(jìn)程都有其自己的地址空間,但程序通常無法使用所有這些空間。地址空間被劃分為用戶空間 和內(nèi)核空間。內(nèi)核是主要的操作系統(tǒng)程序,包含用于連接計(jì)算機(jī)硬件、調(diào)度程序以及提供聯(lián)網(wǎng)和虛擬內(nèi)存等服務(wù)的邏輯。
作為計(jì)算機(jī)啟動(dòng)序列的一部分,操作系統(tǒng)內(nèi)核運(yùn)行并初始化硬件。一旦內(nèi)核配置了硬件及其自己的內(nèi)部狀態(tài),第一個(gè)用戶空間進(jìn)程就會啟動(dòng)。如果用戶程序需要來自操作系統(tǒng)的服務(wù),它可以執(zhí)行一種稱為系統(tǒng)調(diào)用 的操作與內(nèi)核程序交互,內(nèi)核程序然后執(zhí)行該請求。系統(tǒng)調(diào)用通常是讀取和寫入文件、聯(lián)網(wǎng)和啟動(dòng)新進(jìn)程等操作所必需的。
當(dāng)執(zhí)行系統(tǒng)調(diào)用時(shí),內(nèi)核需要訪問其自己的內(nèi)存和調(diào)用進(jìn)程的內(nèi)存。因?yàn)檎趫?zhí)行當(dāng)前線程的處理器被配置為使用地址空間映射來為當(dāng)前進(jìn)程映射虛擬地址,所以大部分操作系統(tǒng)將每個(gè)進(jìn)程地址空間的一部分映射到一個(gè)通用的內(nèi)核內(nèi)存區(qū)域。被映射來供內(nèi)核使用的地址空間部分稱為內(nèi)核空間,其余部分稱為用戶空間,可供用戶應(yīng)用程序使用。
內(nèi)核空間和用戶空間之間的平衡關(guān)系因操作系統(tǒng)的不同而不同,甚至在運(yùn)行于不同硬件架構(gòu)之上的同一操作系統(tǒng)的各個(gè)實(shí)例間也有所不同。這種平衡通常是可配置的,可進(jìn)行調(diào)整來為用戶應(yīng)用程序或內(nèi)核提供更多空間。縮減內(nèi)核區(qū)域可能導(dǎo)致一些問題,比如能夠同時(shí)登錄的用戶數(shù)量限制或能夠運(yùn)行的進(jìn)程數(shù)量限制。更小的用戶空間意味著應(yīng)用程序編程人員只能使用更少的內(nèi)存空間。
默認(rèn)情況下,32 位 Windows 擁有 2GB 用戶空間和 2GB 內(nèi)核空間。在一些 Windows 版本上,通過向啟動(dòng)配置添加 /3GB
開關(guān)并使用 /LARGEADDRESSAWARE
開關(guān)重新鏈接應(yīng)用程序,可以將這種平衡調(diào)整為 3GB 用戶空間和 1GB 內(nèi)核空間。在 32 位 Linux 上,默認(rèn)設(shè)置為 3GB 用戶空間和 1GB 內(nèi)核空間。一些 Linux 分發(fā)版提供了一個(gè) hugemem 內(nèi)核,支持 4GB 用戶空間。為了實(shí)現(xiàn)這種配置,將進(jìn)行系統(tǒng)調(diào)用時(shí)使用的地址空間分配給內(nèi)核。通過這種方式增加用戶空間會減慢系統(tǒng)調(diào)用,因?yàn)槊看芜M(jìn)行系統(tǒng)調(diào)用時(shí),操作系統(tǒng)必須在地址空間之間復(fù)制數(shù)據(jù)并重置進(jìn)程地址-空間映射。圖 2 展示了 32 位 Windows 的地址-空間布局:
圖 3 顯示了 32 位 Linux 的地址-空間配置:
31 位 Linux 390 上還使用了一個(gè)獨(dú)立的內(nèi)核地址空間,其中較小的 2GB 地址空間使對單個(gè)地址空間進(jìn)行劃分不太合理,但是,390 架構(gòu)可以同時(shí)使用多個(gè)地址空間,而且不會降低性能。
進(jìn)程空間必須包含程序需要的所有內(nèi)容,包括程序本身和它使用的共享庫(在 Windows 上為 DDL,在 Linux 上為 .so 文件)。共享庫不僅會占據(jù)空間,使程序無法在其中存儲數(shù)據(jù),它們還會使地址空間碎片化,減少可作為連續(xù)內(nèi)存塊分配的內(nèi)存。這對于在擁有 3GB 用戶空間的 Windows x86 上運(yùn)行的程序尤為明顯。DLL 在構(gòu)建時(shí)設(shè)置了首選的加載地址:當(dāng)加載 DLL 時(shí),它被映射到處于特定位置的地址空間,除非該位置已經(jīng)被占用,在這種情況下,它會加載到別處。Windows NT 最初設(shè)計(jì)時(shí)設(shè)置了 2GB 可用用戶空間,這對于要構(gòu)建來加載接近 2GB 區(qū)域的系統(tǒng)庫很有用 —— 使大部分用戶區(qū)域都可供應(yīng)用程序自由使用。當(dāng)用戶區(qū)域擴(kuò)展到 3GB 時(shí),系統(tǒng)共享庫仍然加載接近 2GB 數(shù)據(jù)(約為用戶空間的一半)。盡管總體用戶空間為 3GB,但是不可能分配 3GB 大的內(nèi)存塊,因?yàn)楣蚕韼鞜o法加載這么大的內(nèi)存。
在 Windows 中使用 /3GB
開關(guān),可以將內(nèi)核空間減少一半,也就是最初設(shè)計(jì)的大小。在一些情形下,可能耗盡 1GB 內(nèi)核空間,使 I/O 變得緩慢,且無法正常創(chuàng)建新的用戶會話。盡管 /3GB
開關(guān)可能對一些應(yīng)用程序非常有用,但任何使用它的環(huán)境在部署之前都應(yīng)該進(jìn)行徹底的負(fù)載測試。參見 參考資料,獲取關(guān)于 /3GB
開關(guān)及其優(yōu)缺點(diǎn)的更多信息的鏈接。
本機(jī)內(nèi)存泄漏或過度使用本機(jī)內(nèi)存將導(dǎo)致不同的問題,具體取決于您是耗盡了地址空間還是用完了物理內(nèi)存。耗盡地址空間通常只會發(fā)生在 32 位進(jìn)程上,因?yàn)樽畲?4GB 的內(nèi)存很容易分配完。64 位進(jìn)程具有數(shù)百或數(shù)千 GB 的用戶空間,即使您特意消耗空間也很難耗盡這么大的空間。如果您確實(shí)耗盡了 Java 進(jìn)程的地址空間,那么 Java 運(yùn)行時(shí)可能會出現(xiàn)一些陌生現(xiàn)象,本文稍后將詳細(xì)討論。當(dāng)在進(jìn)程地址空間比物理內(nèi)存大的系統(tǒng)上運(yùn)行時(shí),內(nèi)存泄漏或過度使用本機(jī)內(nèi)存會迫使操作系統(tǒng)交換后備存儲器來用作本機(jī)進(jìn)程的虛擬地址空間。訪問經(jīng)過交換的內(nèi)存地址比讀取駐留(在物理內(nèi)存中)的地址慢得多,因?yàn)椴僮飨到y(tǒng)必須從硬盤驅(qū)動(dòng)器拉取數(shù)據(jù)??赡軙峙浯罅績?nèi)存來用完所有物理內(nèi)存和所有交換內(nèi)存(頁面空間),在 Linux 上,這將觸發(fā)內(nèi)核內(nèi)存不足(OOM)結(jié)束程序,強(qiáng)制結(jié)束最消耗內(nèi)存的進(jìn)程。在 Windows 上,與地址空間被占滿時(shí)一樣,內(nèi)存分配將會失敗。
同時(shí),如果嘗試使用比物理內(nèi)存大的虛擬內(nèi)存,顯然在進(jìn)程由于消耗內(nèi)存太大而被結(jié)束之前就會遇到問題。系統(tǒng)將變得異常緩慢,因?yàn)樗鼤⒋蟛糠謺r(shí)間用于在內(nèi)存與交換空間之間來回復(fù)制數(shù)據(jù)。當(dāng)發(fā)生這種情況時(shí),計(jì)算機(jī)和獨(dú)立應(yīng)用程序的性能將變得非常糟糕,從而使用戶意識到出現(xiàn)了問題。當(dāng) JVM 的 Java 堆被交換出來時(shí),垃圾收集器的性能會變得非常差,應(yīng)用程序可能被掛起。如果一臺機(jī)器上同時(shí)使用了多個(gè) Java 運(yùn)行時(shí),那么物理內(nèi)存必須足夠分配給所有 Java 堆。
Java 運(yùn)行時(shí)是一個(gè)操作系統(tǒng)進(jìn)程,它會受到我在上一節(jié)中列出的硬件和操作系統(tǒng)局限性的限制。運(yùn)行時(shí)環(huán)境提供的功能受一些未知的用戶代碼驅(qū)動(dòng),這使得無法預(yù)測在每種情形中運(yùn)行時(shí)環(huán)境將需要何種資源。Java 應(yīng)用程序在托管 Java 環(huán)境中執(zhí)行的每個(gè)操作都會潛在地影響提供該環(huán)境的運(yùn)行時(shí)的需求。本節(jié)描述 Java 應(yīng)用程序?yàn)槭裁春腿绾问褂帽緳C(jī)內(nèi)存。
Java 堆是分配了對象的內(nèi)存區(qū)域。大多數(shù) Java SE 實(shí)現(xiàn)都擁有一個(gè)邏輯堆,但是一些專家級 Java 運(yùn)行時(shí)擁有多個(gè)堆,比如實(shí)現(xiàn) Java 實(shí)時(shí)規(guī)范(Real Time Specification for Java,RTSJ)的運(yùn)行時(shí)。一個(gè)物理堆可被劃分為多個(gè)邏輯扇區(qū),具體取決于用于管理堆內(nèi)存的垃圾收集(GC)算法。這些扇區(qū)通常實(shí)現(xiàn)為連續(xù)的本機(jī)內(nèi)存塊,這些內(nèi)存塊受 Java 內(nèi)存管理器(包含垃圾收集器)控制。
堆的大小可以在 Java 命令行使用 -Xmx
和 -Xms
選項(xiàng)來控制(mx
表示堆的最大大小,ms
表示初始大?。?。盡管邏輯堆(經(jīng)常被使用的內(nèi)存區(qū)域)可以根據(jù)堆上的對象數(shù)量和在 GC 上花費(fèi)的時(shí)間而增大和縮小,但使用的本機(jī)內(nèi)存大小保持不變,而且由 -Xmx
值(最大堆大?。┲付?。大部分 GC 算法依賴于被分配為連續(xù)的內(nèi)存塊的堆,因此不能在堆需要擴(kuò)大時(shí)分配更多本機(jī)內(nèi)存。所有堆內(nèi)存必須預(yù)先保留。
保留本機(jī)內(nèi)存與分配本機(jī)內(nèi)存不同。當(dāng)本機(jī)內(nèi)存被保留時(shí),無法使用物理內(nèi)存或其他存儲器作為備用內(nèi)存。盡管保留地址空間塊不會耗盡物理資源,但會阻止內(nèi)存被用于其他用途。由保留從未使用的內(nèi)存導(dǎo)致的泄漏與泄漏分配的內(nèi)存一樣嚴(yán)重。
當(dāng)使用的堆區(qū)域縮小時(shí),一些垃圾收集器會回收堆的一部分(釋放堆的后備存儲空間),從而減少使用的物理內(nèi)存。
對于維護(hù) Java 堆的內(nèi)存管理系統(tǒng),需要更多本機(jī)內(nèi)存來維護(hù)它的狀態(tài)。當(dāng)進(jìn)行垃圾收集時(shí),必須分配數(shù)據(jù)結(jié)構(gòu)來跟蹤空閑存儲空間和記錄進(jìn)度。這些數(shù)據(jù)結(jié)構(gòu)的確切大小和性質(zhì)因?qū)崿F(xiàn)的不同而不同,但許多數(shù)據(jù)結(jié)構(gòu)都與堆大小成正比。
JIT 編譯器在運(yùn)行時(shí)編譯 Java 字節(jié)碼來優(yōu)化本機(jī)可執(zhí)行代碼。這極大地提高了 Java 運(yùn)行時(shí)的速度,并且支持 Java 應(yīng)用程序以與本機(jī)代碼相當(dāng)?shù)乃俣冗\(yùn)行。
字節(jié)碼編譯使用本機(jī)內(nèi)存(使用方式與 gcc
等靜態(tài)編譯器使用內(nèi)存來運(yùn)行一樣),但 JIT 編譯器的輸入(字節(jié)碼)和輸出(可執(zhí)行代碼)必須也存儲在本機(jī)內(nèi)存中。包含多個(gè)經(jīng)過 JIT 編譯的方法的 Java 應(yīng)用程序會使用比小型應(yīng)用程序更多的本機(jī)內(nèi)存。
Java 應(yīng)用程序由一些類組成,這些類定義對象結(jié)構(gòu)和方法邏輯。Java 應(yīng)用程序也使用 Java 運(yùn)行時(shí)類庫(比如 java.lang.String
)中的類,也可以使用第三方庫。這些類需要存儲在內(nèi)存中以備使用。
存儲類的方式取決于具體實(shí)現(xiàn)。Sun JDK 使用永久生成(permanent generation,PermGen)堆區(qū)域。Java 5 的 IBM 實(shí)現(xiàn)會為每個(gè)類加載器分配本機(jī)內(nèi)存塊,并將類數(shù)據(jù)存儲在其中?,F(xiàn)代 Java 運(yùn)行時(shí)擁有類共享等技術(shù),這些技術(shù)可能需要將共享內(nèi)存區(qū)域映射到地址空間。要理解這些分配機(jī)制如何影響您 Java 運(yùn)行時(shí)的本機(jī)內(nèi)存占用,您需要查閱該實(shí)現(xiàn)的技術(shù)文檔。然而,一些普遍的事實(shí)會影響所有實(shí)現(xiàn)。
從最基本的層面來看,使用更多的類將需要使用更多內(nèi)存。(這可能意味著您的本機(jī)內(nèi)存使用量會增加,或者您必須明確地重新設(shè)置 PermGen 或共享類緩存等區(qū)域的大小,以裝入所有類)。記住,不僅您的應(yīng)用程序需要加載到內(nèi)存中,框架、應(yīng)用服務(wù)器、第三方庫以及包含類的 Java 運(yùn)行時(shí)也會按需加載并占用空間。
Java 運(yùn)行時(shí)可以卸載類來回收空間,但是只有在非常嚴(yán)酷的條件下才會這樣做。不能卸載單個(gè)類,而是卸載類加載器,隨其加載的所有類都會被卸載。只有在以下情況下才能卸載類加載器:
java.lang.ClassLoader
對象的引用。java.lang.Class
對象的引用。需要注意的是,Java 運(yùn)行時(shí)為所有 Java 應(yīng)用程序創(chuàng)建的 3 個(gè)默認(rèn)類加載器( bootstrap、extension 和 application )都不可能滿足這些條件,因此,任何系統(tǒng)類(比如 java.lang.String
)或通過應(yīng)用程序類加載器加載的任何應(yīng)用程序類都不能在運(yùn)行時(shí)釋放。
即使類加載器適合進(jìn)行收集,運(yùn)行時(shí)也只會將收集類加載器作為 GC 周期的一部分。一些實(shí)現(xiàn)只會在某些 GC 周期中卸載類加載器。
也可能在運(yùn)行時(shí)生成類,而不用釋放它。許多 JEE 應(yīng)用程序使用 JavaServer Pages (JSP) 技術(shù)來生成 Web 頁面。使用 JSP 會為執(zhí)行的每個(gè) .jsp 頁面生成一個(gè)類,并且這些類會在加載它們的類加載器的整個(gè)生存期中一直存在 —— 這個(gè)生存期通常是 Web 應(yīng)用程序的生存期。
另一種生成類的常見方法是使用 Java 反射。反射的工作方式因 Java 實(shí)現(xiàn)的不同而不同,但 Sun 和 IBM 實(shí)現(xiàn)都使用了這種方法,我馬上就會講到。
當(dāng)使用 java.lang.reflect
API 時(shí),Java 運(yùn)行時(shí)必須將一個(gè)反射對象(比如 java.lang.reflect.Field
)的方法連接到被反射到的對象或類。這可以通過使用 Java 本機(jī)接口(Java Native Interface,JNI)訪問器來完成,這種方法需要的設(shè)置很少,但是速度緩慢。也可以在運(yùn)行時(shí)為您想要反射到的每種對象類型動(dòng)態(tài)構(gòu)建一個(gè)類。后一種方法在設(shè)置上更慢,但運(yùn)行速度更快,非常適合于經(jīng)常反射到一個(gè)特定類的應(yīng)用程序。
Java 運(yùn)行時(shí)在最初幾次反射到一個(gè)類時(shí)使用 JNI 方法,但當(dāng)使用了若干次 JNI 方法之后,訪問器會膨脹為字節(jié)碼訪問器,這涉及到構(gòu)建類并通過新的類加載器進(jìn)行加載。執(zhí)行多次反射可能導(dǎo)致創(chuàng)建了許多訪問器類和類加載器。保持對反射對象的引用會導(dǎo)致這些類一直存活,并繼續(xù)占用空間。因?yàn)閯?chuàng)建字節(jié)碼訪問器非常緩慢,所以 Java 運(yùn)行時(shí)可以緩存這些訪問器以備以后使用。一些應(yīng)用程序和框架還會緩存反射對象,這進(jìn)一步增加了它們的本機(jī)內(nèi)存占用。
JNI 支持本機(jī)代碼(使用 C 和 C++ 等本機(jī)編譯語言編寫的應(yīng)用程序)調(diào)用 Java 方法,反之亦然。Java 運(yùn)行時(shí)本身極大地依賴于 JNI 代碼來實(shí)現(xiàn)類庫功能,比如文件和網(wǎng)絡(luò) I/O。JNI 應(yīng)用程序可能通過 3 種方式增加 Java 運(yùn)行時(shí)的本機(jī)內(nèi)存占用:
GetTypeArrayElements
和 GetTypeArrayRegion
函數(shù)可以將 Java 堆數(shù)據(jù)復(fù)制到本機(jī)內(nèi)存緩沖區(qū)中,以供本機(jī)代碼使用。是否復(fù)制數(shù)據(jù)依賴于運(yùn)行時(shí)實(shí)現(xiàn)。(IBM Developer Kit for Java 5.0 和更高版本會進(jìn)行本機(jī)復(fù)制)。通過這種方式訪問大量 Java 堆數(shù)據(jù)可能會使用大量本機(jī)堆。Java 1.4 中添加的新 I/O (NIO) 類引入了一種基于通道和緩沖區(qū)來執(zhí)行 I/O 的新方式。就像 Java 堆上的內(nèi)存支持 I/O 緩沖區(qū)一樣,NIO 添加了對直接 ByteBuffer
的支持(使用 java.nio.ByteBuffer.allocateDirect()
方法進(jìn)行分配), ByteBuffer
受本機(jī)內(nèi)存而不是 Java 堆支持。直接 ByteBuffer
可以直接傳遞到本機(jī)操作系統(tǒng)庫函數(shù),以執(zhí)行 I/O — 這使這些函數(shù)在一些場景中要快得多,因?yàn)樗鼈兛梢员苊庠?Java 堆與本機(jī)堆之間復(fù)制數(shù)據(jù)。
對于在何處存儲直接 ByteBuffer
數(shù)據(jù),很容易產(chǎn)生混淆。應(yīng)用程序仍然在 Java 堆上使用一個(gè)對象來編排 I/O 操作,但持有該數(shù)據(jù)的緩沖區(qū)將保存在本機(jī)內(nèi)存中,Java 堆對象僅包含對本機(jī)堆緩沖區(qū)的引用。非直接 ByteBuffer
將其數(shù)據(jù)保存在 Java 堆上的 byte[]
數(shù)組中。圖 4 展示了直接與非直接 ByteBuffer
對象之間的區(qū)別:
java.nio.ByteBuffer
的內(nèi)存拓?fù)浣Y(jié)構(gòu)直接 ByteBuffer
對象會自動(dòng)清理本機(jī)緩沖區(qū),但這個(gè)過程只能作為 Java 堆 GC 的一部分來執(zhí)行,因此它們不會自動(dòng)響應(yīng)施加在本機(jī)堆上的壓力。GC 僅在 Java 堆被填滿,以至于無法為堆分配請求提供服務(wù)時(shí)發(fā)生,或者在 Java 應(yīng)用程序中顯式請求它發(fā)生(不建議采用這種方式,因?yàn)檫@可能導(dǎo)致性能問題)。
發(fā)生垃圾收集的情形可能是,本機(jī)堆被填滿,并且一個(gè)或多個(gè)直接 ByteBuffers
適合于垃圾收集(并且可以被釋放來騰出本機(jī)堆的空間),但 Java 堆幾乎總是空的,所以不會發(fā)生垃圾收集。
應(yīng)用程序中的每個(gè)線程都需要內(nèi)存來存儲器堆棧(用于在調(diào)用函數(shù)時(shí)持有局部變量并維護(hù)狀態(tài)的內(nèi)存區(qū)域)。每個(gè) Java 線程都需要堆??臻g來運(yùn)行。根據(jù)實(shí)現(xiàn)的不同,Java 線程可以分為本機(jī)線程和 Java 堆棧。除了堆棧空間,每個(gè)線程還需要為線程本地存儲(thread-local storage)和內(nèi)部數(shù)據(jù)結(jié)構(gòu)提供一些本機(jī)內(nèi)存。
堆棧大小因 Java 實(shí)現(xiàn)和架構(gòu)的不同而不同。一些實(shí)現(xiàn)支持為 Java 線程指定堆棧大小,其范圍通常在 256KB 到 756KB 之間。
盡管每個(gè)線程使用的內(nèi)存量非常小,但對于擁有數(shù)百個(gè)線程的應(yīng)用程序來說,線程堆棧的總內(nèi)存使用量可能非常大。如果運(yùn)行的應(yīng)用程序的線程數(shù)量比可用于處理它們的處理器數(shù)量多,效率通常很低,并且可能導(dǎo)致糟糕的性能和更高的內(nèi)存占用。
Java 運(yùn)行時(shí)善于以不同的方式來處理 Java 堆的耗盡與本機(jī)堆的耗盡,但這兩種情形具有類似的癥狀。當(dāng) Java 堆耗盡時(shí),Java 應(yīng)用程序很難正常運(yùn)行,因?yàn)?Java 應(yīng)用程序必須通過分配對象來完成工作。只要 Java 堆被填滿,就會出現(xiàn)糟糕的 GC 性能并拋出表示 Java 堆被填滿的 OutOfMemoryError
。
相反,一旦 Java 運(yùn)行時(shí)開始運(yùn)行并且應(yīng)用程序處于穩(wěn)定狀態(tài),它可以在本機(jī)堆完全耗盡之后繼續(xù)正常運(yùn)行。不一定會發(fā)生奇怪的行為,因?yàn)樾枰峙浔緳C(jī)內(nèi)存的操作比需要分配 Java 堆的操作少得多。盡管需要本機(jī)內(nèi)存的操作因 JVM 實(shí)現(xiàn)不同而異,但也有一些操作很常見:啟動(dòng)線程、加載類以及執(zhí)行某種類型的網(wǎng)絡(luò)和文件 I/O。
本機(jī)內(nèi)存不足行為與 Java 堆內(nèi)存不足行為也不太一樣,因?yàn)闊o法對本機(jī)堆分配進(jìn)行單點(diǎn)控制。盡管所有 Java 堆分配都在 Java 內(nèi)存管理系統(tǒng)控制之下,但任何本機(jī)代碼(無論其位于 JVM、Java 類庫還是應(yīng)用程序代碼中)都可能執(zhí)行本機(jī)內(nèi)存分配,而且會失敗。嘗試進(jìn)行分配的代碼然后會處理這種情況,無論設(shè)計(jì)人員的意圖是什么:它可能通過 JNI 接口拋出一個(gè) OutOfMemoryError
,在屏幕上輸出一條消息,發(fā)生無提示失敗并在稍后再試一次,或者執(zhí)行其他操作。
缺乏可預(yù)測行為意味著無法確定本機(jī)內(nèi)存是否耗盡。相反,您需要使用來自操作系統(tǒng)和 Java 運(yùn)行時(shí)的數(shù)據(jù)執(zhí)行診斷。
為了幫助您了解本機(jī)內(nèi)存耗盡如何影響您正使用的 Java 實(shí)現(xiàn),本文的示例代碼(參見 下載)中包含了一些 Java 程序,用于以不同方式觸發(fā)本機(jī)堆耗盡。這些示例使用通過 C 語言編寫的本機(jī)庫來消耗所有本機(jī)地址空間,然后嘗試執(zhí)行一些使用本機(jī)內(nèi)存的操作。提供的示例已經(jīng)過編譯,編譯它們的指令包含在示例包的頂級目錄下的 README.html 文件中。
com.ibm.jtc.demos.NativeMemoryGlutton
類提供了 gobbleMemory()
方法,它在一個(gè)循環(huán)中調(diào)用 malloc
,直到幾乎所有本機(jī)內(nèi)存都已耗盡。完成任務(wù)之后,它通過以下方式輸出分配給標(biāo)準(zhǔn)錯(cuò)誤的字節(jié)數(shù):
Allocated 1953546736 bytes of native memory before running out
針對在 32 位 Windows 上運(yùn)行的 Sun 和 IBM Java 運(yùn)行時(shí)的每次演示,其輸出都已被捕獲。提供的二進(jìn)制文件已在以下操作系統(tǒng)上進(jìn)行了測試:
使用以下 Sun Java 運(yùn)行時(shí)版本捕獲輸出:
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 運(yùn)行時(shí)版本為:
java version "1.5.0"Java(TM) 2 Runtime Environment, Standard Edition (build pwi32devifx-20071025 (SR6b))IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Windows XP x86-32 j9vmwi3223-20071007 (JIT enabled)J9VM - 20071004_14218_lHdSMRJIT - 20070820_1846ifx1_r8GC - 200708_10)JCL - 20071025
com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation
類嘗試在耗盡進(jìn)程地址空間時(shí)啟動(dòng)一個(gè)線程。這是發(fā)現(xiàn) Java 進(jìn)程已耗盡內(nèi)存的一種常用方式,因?yàn)樵S多應(yīng)用程序都會在其整個(gè)生存期啟動(dòng)線程。
當(dāng)在 IBM Java 運(yùn)行時(shí)上運(yùn)行時(shí),StartingAThreadUnderNativeStarvation
演示的輸出如下:
Allocated 1019394912 bytes of native memory before running outJVMDUMP006I 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.trcJVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080323.182114.5172.phd'JVMDUMP010I Heap Dump written to C:\heapdump.20080323.182114.5172.phdJVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080323.182114.5172.txt'JVMDUMP010I Java Dump written to C:\javacore.20080323.182114.5172.txtJVMDUMP013I 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()
來嘗試為一個(gè)新的操作系統(tǒng)線程分配內(nèi)存。此嘗試會失敗并拋出 OutOfMemoryError
。JVMDUMP
行通知用戶 Java 運(yùn)行時(shí)已經(jīng)生成了標(biāo)準(zhǔn)的 OutOfMemoryError
調(diào)試數(shù)據(jù)。
嘗試處理第一個(gè) OutOfMemoryError
會導(dǎo)致第二個(gè)錯(cuò)誤 —— :OutOfMemoryError, ENOMEM error in ZipFile.open
。當(dāng)本機(jī)進(jìn)程內(nèi)存耗盡時(shí)通常會拋出多個(gè) OutOfMemoryError
。Failed to fork OS thread
可能是在耗盡本機(jī)內(nèi)存時(shí)最常見的消息。
本文提供的示例會觸發(fā)一個(gè) OutOfMemoryError
集群,這比您在自己的應(yīng)用程序中看到的情況要嚴(yán)重得多。這一定程度上是因?yàn)閹缀跛斜緳C(jī)內(nèi)存都已被使用,與實(shí)際的應(yīng)用程序不同,使用的內(nèi)存不會在以后被釋放。在實(shí)際應(yīng)用程序中,當(dāng)拋出 OutOfMemoryError
時(shí),線程會關(guān)閉,并且可能會釋放一部分本機(jī)內(nèi)存,以讓運(yùn)行時(shí)處理錯(cuò)誤。測試案例的這個(gè)細(xì)微特性還意味著,類庫的許多部分(比如安全系統(tǒng))未被初始化,而且它們的初始化受嘗試處理內(nèi)存耗盡情形的運(yùn)行時(shí)驅(qū)動(dòng)。在實(shí)際應(yīng)用程序中,您可能會看到顯示了很多錯(cuò)誤,但您不太可能在一個(gè)位置看到所有這些錯(cuò)誤。
在 Sun Java 運(yùn)行時(shí)上執(zhí)行相同的測試案例時(shí),會生成以下控制臺輸出:
Allocated 1953546736 bytes of native memory before running outException 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)
盡管堆棧軌跡和錯(cuò)誤消息稍有不同,但其行為在本質(zhì)上是一樣的:本機(jī)分配失敗并拋出 java.lang.OutOfMemoryError
。此場景中拋出的 OutOfMemoryError
與由于 Java 堆耗盡而拋出的錯(cuò)誤的惟一區(qū)別在于消息。
ByteBuffer
com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation
類嘗試在地址空間耗盡時(shí)分配一個(gè)直接(也就是受本機(jī)支持的)java.nio.ByteBuffer
對象。當(dāng)在 IBM Java 運(yùn)行時(shí)上運(yùn)行時(shí),它生成以下輸出:
Allocated 1019481472 bytes of native memory before running outJVMDUMP006I 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.trcJVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080324.100721.4232.phd'JVMDUMP010I Heap Dump written to C:\heapdump.20080324.100721.4232.phdJVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080324.100721.4232.txt'JVMDUMP010I Java Dump written to C:\javacore.20080324.100721.4232.txtJVMDUMP013I 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ā)默認(rèn)的錯(cuò)誤文檔。OutOfMemoryError
到達(dá)主線程堆棧的頂部,并在 stderr
上輸出。
當(dāng)在 Sun Java 運(yùn)行時(shí)上運(yùn)行時(shí),此測試案例生成以下控制臺輸出:
Allocated 1953546760 bytes of native memory before running outException 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)試原則,可用于理解本機(jī)內(nèi)存耗盡場景。您的運(yùn)行時(shí)供應(yīng)商可能提供了自己的調(diào)試說明,供應(yīng)商期望您按照這些說明與其支持團(tuán)隊(duì)聯(lián)系。如果您要與運(yùn)行時(shí)供應(yīng)商(包括 IBM)合作解決問題,請始終檢查其調(diào)試和診斷文檔,查看在提交問題報(bào)告時(shí)應(yīng)該執(zhí)行哪些步驟。
當(dāng)出現(xiàn) java.lang.OutOfMemoryError
或看到有關(guān)內(nèi)存不足的錯(cuò)誤消息時(shí),要做的第一件事是確定哪種類型的內(nèi)存被耗盡。最簡單的方式是首先檢查 Java 堆是否被填滿。如果 Java 堆未導(dǎo)致 OutOfMemory
條件,那么您應(yīng)該分析本機(jī)堆使用情況。
檢查堆使用情況的方法因 Java 實(shí)現(xiàn)不同而異。在 Java 5 和 6 的 IBM 實(shí)現(xiàn)上,當(dāng)拋出 OutOfMemoryError
時(shí)會生成一個(gè) javacore 文件來告訴您。javacore 文件通常在 Java 進(jìn)程的工作目錄中生成,以 javacore.日期.時(shí)間.pid.txt 的形式命名。如果您在文本編輯器中打開該文件,可以看到以下信息:
0SECTION MEMINFO subcomponent dump routineNULL =================================1STHEAPFREE Bytes of Heap Space Free: 416760 1STHEAPALLOC Bytes of Heap Space Allocated: 1344800
這部分信息顯示在生成 javacore 時(shí)有多少空閑的 Java 堆。注意,顯示的值為十六進(jìn)制格式。如果因?yàn)榉峙錀l件不滿足而拋出了 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
。
也可能由于垃圾收集器運(yùn)行太頻繁(意味著堆被填滿了并且 Java 應(yīng)用程序的運(yùn)行速度將很慢或停止運(yùn)行)而拋出 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
當(dāng) Sun 實(shí)現(xiàn)耗盡 Java 堆內(nèi)存時(shí),它使用異常消息來顯示它耗盡的是 Java 堆:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
IBM 和 Sun 實(shí)現(xiàn)都擁有一個(gè)詳細(xì)的 GC 選項(xiàng),用于在每個(gè) GC 周期生成顯示堆填充情況的跟蹤數(shù)據(jù)。此信息可使用工具(比如 IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer (GCMV))來分析,以顯示 Java 堆是否在增長(參見 參考資料)。
如果您確定內(nèi)存耗盡情況不是由 Java 堆耗盡引起的,那么下一步就是分析您的本機(jī)內(nèi)存使用情況。
Windows 提供的 PerfMon 工具可用于監(jiān)控和記錄許多操作系統(tǒng)和進(jìn)程指標(biāo),包括本機(jī)內(nèi)存使用(參見 參考資料)。它允許實(shí)時(shí)跟蹤計(jì)數(shù)器,或?qū)⑵浯鎯υ谌罩疚募幸怨╇x線查看。使用 Private Bytes 計(jì)數(shù)器顯示總體地址空間使用情況。如果顯示值接近于用戶空間的限制(前面已經(jīng)討論過,介于 2 到 3GB 之間),您應(yīng)該會看到本機(jī)內(nèi)存耗盡情況。
Linux 沒有類似于 PerfMon 的工具,但是它提供了幾個(gè)替代工具。命令行工具(比如 ps
、top
和 pmap
)能夠顯示應(yīng)用程序的本機(jī)內(nèi)存占用情況。盡管獲取進(jìn)程內(nèi)存使用情況的實(shí)時(shí)快照非常有用,但通過記錄內(nèi)存隨時(shí)間的使用情況,您能夠更好地理解本機(jī)內(nèi)存是如何被使用的。為此,能夠采取的一種方式是使用 GCMV。
GCMV 最初編寫用于分析冗長的 GC 日志,允許用戶在調(diào)優(yōu)垃圾收集器時(shí)查看 Java 堆使用情況和 GC 性能的變化。GCMV 后來進(jìn)行了擴(kuò)展,支持分析其他數(shù)據(jù)源,包括 Linux 和 AIX 本機(jī)內(nèi)存數(shù)據(jù)。GCMV 是作為 IBM Support Assistant (ISA) 的插件發(fā)布的。
要使用 GCMV 分析 Linux 本機(jī)內(nèi)存配置文件,您首先必須使用腳本收集本機(jī)內(nèi)存數(shù)據(jù)。GCMV 的 Linux 本機(jī)內(nèi)存分析器通過根據(jù)時(shí)間戳隔行掃描的方式,讀取 Linux ps
命令的輸出。GCMV 提供了一個(gè)腳本來幫助以正確形式記錄收集數(shù)據(jù)。要找到該腳本:
圖 5 顯示了該腳本在 ISA 幫助文件中的位置。如果您的幫助文件中沒有 GCMV Tool 條目,很可能是因?yàn)槟鷽]有安裝 GCMV 插件。
GCMV 幫助文件中提供的腳本使用的 ps
命令僅適用于最新的 ps
版本。在一些舊的 Linux 分發(fā)版中,幫助文件中的命令將會生成錯(cuò)誤信息。要查看您的 Linux 分發(fā)版上的行為,可以嘗試運(yùn)行 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
如果您在一個(gè)較老的 ps
版本上運(yùn)行,可以修改本機(jī)內(nèi)存腳本,將
ps -p $PID -o pid,vsz=VSZ,rss=RSS
行替換為
ps -p $PID -o pid,vsz,rss
將幫助面板中的腳本復(fù)制到一個(gè)文件中(在本例中名為 memscript.sh),找到您想要監(jiān)控的 Java 進(jìn)程的進(jìn)程 ID (PID)(本例中為 1234)并運(yùn)行:
./memscript.sh 1234 > ps.out
這會把本機(jī)內(nèi)存日志寫入到 ps.out 中。要分析內(nèi)存使用情況:
一旦您擁有了本機(jī)內(nèi)存隨時(shí)間的使用情況的配置文件,您需要確定是存在本機(jī)內(nèi)存泄漏,還是在嘗試在可用空間中做太多事情。即使對于運(yùn)行良好的 Java 應(yīng)用程序,其本機(jī)內(nèi)存占用也不是從啟動(dòng)開始就一成不變的。一些 Java 運(yùn)行時(shí)系統(tǒng)(尤其是 JIT 編譯器和類加載器)會不斷初始化,這會消耗本機(jī)內(nèi)存。初始化增加的內(nèi)存將高居不下,但是如果初始本機(jī)內(nèi)存占用接近于地址空間的限制,那么僅這個(gè)前期階段就足以導(dǎo)致本機(jī)內(nèi)存耗盡。圖 6 給出了一個(gè) Java 壓力測試示例中的 GCMV 本機(jī)內(nèi)存使用情況,其中突出顯示了前期階段。
本機(jī)內(nèi)存占用也可能應(yīng)工作負(fù)載不同而異。如果您的應(yīng)用程序創(chuàng)建了較多進(jìn)程來處理傳入的工作負(fù)載,或者根據(jù)應(yīng)用于系統(tǒng)的負(fù)載量按比例分配本機(jī)存儲(比如直接 ByteBuffer
),則可能由于負(fù)載過高而耗盡本機(jī)內(nèi)存。
由于 JVM 前期階段的本機(jī)內(nèi)存增長而耗盡本機(jī)內(nèi)存,以及內(nèi)存使用隨負(fù)載增加而增加,這些都是嘗試在可用空間中做太多事情的例子。在這些場景中,您的選擇是:
/3GB
開關(guān)增加用戶空間,或者在 Linux 上使用龐大的內(nèi)核空間),更換平臺(Linux 通常擁有比 Windows 更多的用戶空間),或者 轉(zhuǎn)移到 64 位操作系統(tǒng)。一種實(shí)際的本機(jī)內(nèi)存泄漏表現(xiàn)為本機(jī)堆的持續(xù)增長,這些內(nèi)存不會在移除負(fù)載或運(yùn)行垃圾收集器時(shí)減少。內(nèi)存泄漏程度因負(fù)載不同而不同,但泄漏的總內(nèi)存不會下降。泄漏的內(nèi)存不可能被引用,因此它可以被交換出去,并保持被交換出去的狀態(tài)。
當(dāng)遇到內(nèi)存泄漏時(shí),您的選擇很有限。您可以增加用戶空間(這樣就會有更多的空間供泄漏),但這僅能延緩最終的內(nèi)存耗盡。如果您擁有足夠的物理內(nèi)存和地址空間,并且會在進(jìn)程地址空間耗盡之前重啟應(yīng)用程序,那么可以允許地址空間繼續(xù)泄漏。
一旦確定本機(jī)內(nèi)存被耗盡,下一個(gè)邏輯問題是:是什么在使用這些內(nèi)存?這個(gè)問題很難回答,因?yàn)樵谀J(rèn)情況下,Windows 和 Linux 不會存儲關(guān)于分配給特定內(nèi)存塊的代碼路徑的信息。
當(dāng)嘗試?yán)斫獗緳C(jī)內(nèi)存都到哪里去了時(shí),您的第一步是粗略估算一下,根據(jù)您的 Java 設(shè)置,將會使用多少本機(jī)內(nèi)存。如果沒有對 JVM 工作機(jī)制的深入知識,很難得出精確的值,但您可以根據(jù)以下指南粗略估算一下:
-Xmx
值。ByteBuffer
至少會占用提供給 allocate()
例程的內(nèi)存值。如果總數(shù)比您的最大用戶空間少得多,那么您很可能不安全。Java 運(yùn)行時(shí)中的許多其他組件可能會分配大量內(nèi)存,進(jìn)而引起問題。但是,如果您的初步估算值與最大用戶空間很接近,則可能存在本機(jī)內(nèi)存問題。如果您懷疑存在本機(jī)內(nèi)存泄漏,或者想要準(zhǔn)確了解內(nèi)存都到哪里去了,使用一些工具將有所幫助。
Microsoft 提供了 UMDH(用戶模式轉(zhuǎn)儲堆)和 LeakDiag 工具來在 Windows 上調(diào)試本機(jī)內(nèi)存增長(參見 參考資料)。這兩個(gè)工具的機(jī)制相同:記錄特定內(nèi)存區(qū)域被分配給了哪個(gè)代碼路徑,并提供一種方式來定位所分配的內(nèi)存不會在以后被釋放的代碼部分。我建議您查閱文章 “Umdhtools.exe:如何使用 Umdh.exe 發(fā)現(xiàn) Windows 上的內(nèi)存泄漏”,獲取 UMDH 的使用說明(參見 參考資料)。在本文中,我將主要討論 UMDH 在分析存在泄漏的 JNI 應(yīng)用程序時(shí)的輸出。
本文的 示例包 包含一個(gè)名為 LeakyJNIApp
的 Java 應(yīng)用程序,它循環(huán)調(diào)用一個(gè) JNI 方法來泄漏本機(jī)內(nèi)存。UMDH 命令獲取當(dāng)前的本機(jī)堆的快照,以及分配每個(gè)內(nèi)存區(qū)域的代碼路徑的本機(jī)堆棧軌跡快照。通過獲取兩個(gè)快照,并使用 UMDH 來分析差異,您會得到兩個(gè)快照之間的堆增長報(bào)告。
對于 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 BackTrace00468Total increase == 412192
重要的一行是 + 412192 ( 1031943 - 619751) 963 allocs BackTrace00468
。它顯示一個(gè) backtrace 進(jìn)行了 963 次分配,而且分配的內(nèi)存都沒有釋放 — 總共使用了 412192 字節(jié)內(nèi)存。通過查看一個(gè)快照文件,您可以將 BackTrace00468
與有意義的代碼路徑關(guān)聯(lián)起來。在第一個(gè)快照文件中搜索 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 模塊。
在編寫本文時(shí),Linux 沒有類似于 UMDH 或 LeakDiag 的工具。但在 Linux 上仍然可以采用許多方式來調(diào)試本機(jī)內(nèi)存泄漏。Linux 上提供的許多內(nèi)存調(diào)試器可分為以下類別:
LD_PRELOAD
環(huán)境變量預(yù)先加載一個(gè)庫,這個(gè)庫將標(biāo)準(zhǔn)內(nèi)存例程替換為指定的版本。這些工具不需要重新編譯或重新鏈接源代碼,但其中許多工具與 Java 運(yùn)行時(shí)不太兼容。Java 運(yùn)行時(shí)是一個(gè)復(fù)雜的系統(tǒng),可以以非常規(guī)的方式使用內(nèi)存和線程,這通常會干擾或破壞這類工具。您可以試驗(yàn)一下,看看是否有一些工具適用于您的場景。NJAMD 是這類工具的一個(gè)例子(參見 參考資料)。memcheck
工具是這類內(nèi)存調(diào)試器的惟一例子(參見 參考資料)。它模擬底層處理器,與 Java 運(yùn)行時(shí)模擬 JVM 的方式類似??梢栽?Valgrind 下運(yùn)行 Java 應(yīng)用程序,但是會有嚴(yán)重的性能影響(速度會減慢 10 到 30 倍),這意味著難以通過這種方式運(yùn)行大型、復(fù)雜的 Java 應(yīng)用程序。Valgrind 目前可在 Linux x86、AMD64、PPC 32 和 PPC 64 上使用。如果您使用 Valgrind,請?jiān)谑褂盟皣L試使用最小的測試案例來將減輕性能問題(如果可能,最好移除整個(gè) Java 運(yùn)行時(shí))。對于能夠容忍這種性能開銷的簡單場景,Valgrind memcheck
是最簡單且用戶友好的免費(fèi)工具。它能夠?yàn)樾孤﹥?nèi)存的代碼路徑提供完整的堆棧軌跡,提供方式與 Windows 上的 UMDH 相同。
LeakyJNIApp
非常簡單,能夠在 Valgrind 下運(yùn)行。當(dāng)模擬的程序結(jié)束時(shí),Valgrind memcheck
工具能夠輸出泄漏的內(nèi)存的匯總信息。默認(rèn)情況下,LeakyJNIApp
程序會一直運(yùn)行,要使其在固定時(shí)期之后關(guān)閉,可以將運(yùn)行時(shí)間(以秒為單位)作為惟一的命令行參數(shù)進(jìn)行傳遞。
一些 Java 運(yùn)行時(shí)以非常規(guī)的方式使用線程堆棧和處理器寄存器,這可能使一些調(diào)試工具產(chǎn)生混淆,這些工具要求本機(jī)程序遵從寄存器使用和堆棧結(jié)構(gòu)的標(biāo)準(zhǔn)約定。當(dāng)使用 Valgrind 調(diào)試存在內(nèi)存泄漏的 JNI 應(yīng)用程序時(shí),您可以發(fā)現(xiàn)許多與內(nèi)存使用相關(guān)的警告,并且一些線程堆??雌饋砗芷婀?,這是由 Java 運(yùn)行時(shí)在內(nèi)部構(gòu)造其數(shù)據(jù)的方式所導(dǎo)致的,不用擔(dān)心。
要使用 Valgrind memcheck
工具跟蹤 LeakyJNIApp
,(在一行上)使用以下命令:
valgrind --trace-children=yes --leak-check=full java -Djava.library.path=. com.ibm.jtc.demos.LeakyJNIApp 10
--trace-children=yes
選項(xiàng)使 Valgrind 跟蹤由 Java 啟動(dòng)器啟動(dòng)的任何進(jìn)程。一些 Java 啟動(dòng)器版本會重新執(zhí)行其本身(它們從頭重新啟動(dòng)其本身,再次設(shè)置環(huán)境變量來改變行為)。如果您未指定 --trace-children
,您將不能跟蹤實(shí)際的 Java 運(yùn)行時(shí)。
--leak-check=full
選項(xiàng)請求在代碼運(yùn)行結(jié)束時(shí)輸出對泄漏的代碼區(qū)域的完整堆棧軌跡,而不只是匯總內(nèi)存的狀態(tài)。
當(dāng)該命令運(yùn)行時(shí),Valgrind 輸出許多警告和錯(cuò)誤(在此環(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)試應(yīng)用程序來調(diào)試本機(jī)內(nèi)存泄漏。隨著時(shí)間的推移,會有更多工具(包括開源和專用的)被開發(fā)出來,這對于研究當(dāng)前技術(shù)的發(fā)展現(xiàn)狀很有幫助。
就目前而言,使用免費(fèi)工具調(diào)試 Linux 上的本機(jī)內(nèi)存泄漏比在 Windows 上完成相同的事情更具挑戰(zhàn)性。UMDH 支持就地 調(diào)試 Windows 上本機(jī)內(nèi)存泄漏,在 Linux 上,您可能需要進(jìn)行一些傳統(tǒng)的調(diào)試,而不是依賴工具來解決問題。下面是一些建議的調(diào)試步驟:
ByteBuffer
)。通過縮小 Java 堆大?。◤亩仁估占l繁地發(fā)生),或者在一個(gè)對象緩存中管理對象(而不是依賴于垃圾收集器來清理對象),您可以減少本機(jī)支持對象持有的內(nèi)存量。如果您確定內(nèi)存泄漏或增長來自于 Java 運(yùn)行時(shí)本身,您可能需要聯(lián)系運(yùn)行時(shí)供應(yīng)商來進(jìn)一步調(diào)試。
使用 32 位 Java 運(yùn)行時(shí)很容易遇到本機(jī)內(nèi)存耗盡的情況,因?yàn)榈刂房臻g相對較小。32 位操作系統(tǒng)提供的 2 到 4GB 用戶空間通常小于系統(tǒng)附帶的物理內(nèi)存量,而且現(xiàn)代的數(shù)據(jù)密集型應(yīng)用程序很容易耗盡可用空間。
如果 32 位地址空間不夠您的應(yīng)用程序使用,您可以通過移動(dòng)到 64 位 Java 運(yùn)行時(shí)來獲得更多用戶空間。如果您運(yùn)行的是 64 位操作系統(tǒng),那么 64 位 Java 運(yùn)行時(shí)將能夠滿足海量 Java 堆的需求,還會減少與地址空間相關(guān)的問題。表 2 列出了 64 位操作系統(tǒng)上目前可用的用戶空間。
操作系統(tǒng) | 默認(rèn)用戶空間大小 |
---|---|
Windows x86-64 | 8192GB |
Windows Itanium | 7152GB |
Linux x86-64 | 500GB |
Linux PPC64 | 1648GB |
Linux 390 64 | 4EB |
然而,移動(dòng)到 64 位并不是所有本機(jī)內(nèi)存問題的通用解決方案,您仍然需要足夠的物理內(nèi)存來持有所有數(shù)據(jù)。如果物理內(nèi)存不夠 Java 運(yùn)行時(shí)使用,運(yùn)行時(shí)性能將變得非常糟,因?yàn)椴僮飨到y(tǒng)不得不在內(nèi)存與交換空間之間來回復(fù)制 Java 運(yùn)行時(shí)數(shù)據(jù)。出于相同原因,移動(dòng)到 64 位也不是內(nèi)存泄漏永恒的解決方案,您只是提供了更多空間來供泄漏,這只會延緩您不得不重啟應(yīng)用程序的時(shí)間。
無法在 64 位運(yùn)行時(shí)中使用 32 位本機(jī)代碼。任何本機(jī)代碼(JNI 庫、JVM Tool Interface [JVMTI]、JVM Profiling Interface [JVMPI] 以及 JVM Debug Interface [JVMDI] 代理)都必須編譯為 64 位。64 位運(yùn)行時(shí)的性能也可能比相同硬件上對應(yīng)的 32 位運(yùn)行時(shí)更慢。64 位運(yùn)行時(shí)使用 64 位指針(本機(jī)地址引用),因此,64 位運(yùn)行時(shí)上的 Java 對象會占用比 32 位運(yùn)行時(shí)上包含相同數(shù)據(jù)的對象更多的空間。更大的對象意味著要使用更大的堆來持有相同的數(shù)據(jù)量,同時(shí)保持類似的 GC 性能,這使操作系統(tǒng)和硬件緩存效率更低。令人驚訝的是,更大的 Java 堆并不一定意味著更長的 GC 暫停時(shí)間,因?yàn)槎焉系幕顒?dòng)數(shù)據(jù)量可能不會增加,并且一些 GC 算法在使用更大的堆時(shí)效率更高。
一些現(xiàn)代 Java 運(yùn)行時(shí)包含減輕 64 位 “對象膨脹” 和改善性能的技術(shù)。這些功能在 64 位運(yùn)行時(shí)上使用更短的引用。這在 IBM 實(shí)現(xiàn)中稱為壓縮引用,而在 Sun 實(shí)現(xiàn)中稱為壓縮 oop。
對 Java 運(yùn)行時(shí)性能的比較研究不屬于本文討論范圍,但是如果您正在考慮移動(dòng)到 64 位,盡早測試應(yīng)用程序以理解其執(zhí)行原理會很有幫助。由于更改地址大小會影響到 Java 堆,所以您將需要在新架構(gòu)上重新調(diào)優(yōu)您的 GC 設(shè)置,而不是僅僅移植現(xiàn)有設(shè)置。
在設(shè)計(jì)和運(yùn)行大型 Java 應(yīng)用程序時(shí),理解本機(jī)內(nèi)存至關(guān)重要,但是這一點(diǎn)通常被忽略,因?yàn)樗c復(fù)雜的硬件和操作系統(tǒng)細(xì)節(jié)密切相關(guān),Java 運(yùn)行時(shí)的目的正是幫助我們規(guī)避這些細(xì)節(jié)。JRE 是一個(gè)本機(jī)進(jìn)程,它必須在由這些紛繁復(fù)雜的細(xì)節(jié)定義的環(huán)境中工作。要從 Java 應(yīng)用程序中獲得最佳的性能,您必須理解應(yīng)用程序如何影響 Java 運(yùn)行時(shí)的本機(jī)內(nèi)存使用。
耗盡本機(jī)內(nèi)存與耗盡 Java 堆很相似,但它需要不同的工具集來調(diào)試和解決。修復(fù)本機(jī)內(nèi)存問題的關(guān)鍵在于理解運(yùn)行您的 Java 應(yīng)用程序的硬件和操作系統(tǒng)施加的限制,并將其與操作系統(tǒng)工具知識結(jié)合起來,監(jiān)控本機(jī)內(nèi)存使用。通過這種方法,您將能夠解決 Java 應(yīng)用程序產(chǎn)生的一些非常棘手的問題。
描述 | 名字 | 大小 |
---|---|---|
本機(jī)內(nèi)存示例代碼 | j-nativememory-linux.zip | 115KB |
/3GB
開關(guān)調(diào)整 Windows 內(nèi)核空間的一些爭議。/3GB
開關(guān)相關(guān)的文章和博客鏈接。聯(lián)系客服