Java運(yùn)行時(shí)如何使用本機(jī)內(nèi)存
Java 運(yùn)行時(shí)是一個(gè)操作系統(tǒng)進(jìn)程,它會(huì)受到我在上一節(jié)中列出的硬件和操作系統(tǒng)局限性的限制。運(yùn)行時(shí)環(huán)境提供的功能受一些未知的用戶代碼驅(qū)動(dòng),這使得無法預(yù)測(cè)在每種情形中運(yùn)行時(shí)環(huán)境將需要何種資源。Java 應(yīng)用程序在托管 Java 環(huán)境中執(zhí)行的每個(gè)操作都會(huì)潛在地影響提供該環(huán)境的運(yùn)行時(shí)的需求。本節(jié)描述 Java 應(yīng)用程序?yàn)槭裁春腿绾问褂帽緳C(jī)內(nèi)存。
Java 堆和垃圾收集
Java 堆是分配了對(duì)象的內(nèi)存區(qū)域。大多數(shù) Java SE 實(shí)現(xiàn)都擁有一個(gè)邏輯堆,但是一些專家級(jí) 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ù)堆上的對(duì)象數(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)存或其他存儲(chǔ)器作為備用內(nèi)存。盡管保留地址空間塊不會(huì)耗盡物理資源,但會(huì)阻止內(nèi)存被用于其他用途。由保留從未使用的內(nèi)存導(dǎo)致的泄漏與泄漏分配的內(nèi)存一樣嚴(yán)重。
當(dāng)使用的堆區(qū)域縮小時(shí),一些垃圾收集器會(huì)回收堆的一部分(釋放堆的后備存儲(chǔ)空間),從而減少使用的物理內(nèi)存。
對(duì)于維護(hù) Java 堆的內(nèi)存管理系統(tǒng),需要更多本機(jī)內(nèi)存來維護(hù)它的狀態(tài)。當(dāng)進(jìn)行垃圾收集時(shí),必須分配數(shù)據(jù)結(jié)構(gòu)來跟蹤空閑存儲(chǔ)空間和記錄進(jìn)度。這些數(shù)據(jù)結(jié)構(gòu)的確切大小和性質(zhì)因?qū)崿F(xiàn)的不同而不同,但許多數(shù)據(jù)結(jié)構(gòu)都與堆大小成正比。
即時(shí) (JIT) 編譯器
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í)行代碼)必須也存儲(chǔ)在本機(jī)內(nèi)存中。包含多個(gè)經(jīng)過 JIT 編譯的方法的 Java 應(yīng)用程序會(huì)使用比小型應(yīng)用程序更多的本機(jī)內(nèi)存。
類和類加載器
Java 應(yīng)用程序由一些類組成,這些類定義對(duì)象結(jié)構(gòu)和方法邏輯。Java 應(yīng)用程序也使用 Java 運(yùn)行時(shí)類庫(比如 java.lang.String
)中的類,也可以使用第三方庫。這些類需要存儲(chǔ)在內(nèi)存中以備使用。
存儲(chǔ)類的方式取決于具體實(shí)現(xiàn)。Sun JDK 使用***生成(permanent generation,PermGen)堆區(qū)域。Java 5 的 IBM 實(shí)現(xiàn)會(huì)為每個(gè)類加載器分配本機(jī)內(nèi)存塊,并將類數(shù)據(jù)存儲(chǔ)在其中?,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í)會(huì)影響所有實(shí)現(xiàn)。
從最基本的層面來看,使用更多的類將需要使用更多內(nèi)存。(這可能意味著您的本機(jī)內(nèi)存使用量會(huì)增加,或者您必須明確地重新設(shè)置 PermGen 或共享類緩存等區(qū)域的大小,以裝入所有類)。記住,不僅您的應(yīng)用程序需要加載到內(nèi)存中,框架、應(yīng)用服務(wù)器、第三方庫以及包含類的 Java 運(yùn)行時(shí)也會(huì)按需加載并占用空間。
Java 運(yùn)行時(shí)可以卸載類來回收空間,但是只有在非常嚴(yán)酷的條件下才會(huì)這樣做。不能卸載單個(gè)類,而是卸載類加載器,隨其加載的所有類都會(huì)被卸載。只有在以下情況下才能卸載類加載器:
- Java 堆不包含對(duì)表示該類加載器的
java.lang.ClassLoader
對(duì)象的引用。 - Java 堆不包含對(duì)表示類加載器加載的類的任何
java.lang.Class
對(duì)象的引用。 - 在 Java 堆上,該類加載器加載的任何類的所有對(duì)象都不再存活(被引用)。
需要注意的是,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í)也只會(huì)將收集類加載器作為 GC 周期的一部分。一些實(shí)現(xiàn)只會(huì)在某些 GC 周期中卸載類加載器。
也可能在運(yùn)行時(shí)生成類,而不用釋放它。許多 JEE 應(yīng)用程序使用 JavaServer Pages (JSP) 技術(shù)來生成 Web 頁面。使用 JSP 會(huì)為執(zhí)行的每個(gè) .jsp 頁面生成一個(gè)類,并且這些類會(huì)在加載它們的類加載器的整個(gè)生存期中一直存在 —— 這個(gè)生存期通常是 Web 應(yīng)用程序的生存期。
另一種生成類的常見方法是使用 Java 反射。反射的工作方式因 Java 實(shí)現(xiàn)的不同而不同,但 Sun 和 IBM 實(shí)現(xiàn)都使用了這種方法,我馬上就會(huì)講到。
當(dāng)使用 java.lang.reflect
API 時(shí),Java 運(yùn)行時(shí)必須將一個(gè)反射對(duì)象(比如 java.lang.reflect.Field
)的方法連接到被反射到的對(duì)象或類。這可以通過使用 Java 本機(jī)接口(Java Native Interface,JNI)訪問器來完成,這種方法需要的設(shè)置很少,但是速度緩慢。也可以在運(yùn)行時(shí)為您想要反射到的每種對(duì)象類型動(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 方法之后,訪問器會(huì)膨脹為字節(jié)碼訪問器,這涉及到構(gòu)建類并通過新的類加載器進(jìn)行加載。執(zhí)行多次反射可能導(dǎo)致創(chuàng)建了許多訪問器類和類加載器。保持對(duì)反射對(duì)象的引用會(huì)導(dǎo)致這些類一直存活,并繼續(xù)占用空間。因?yàn)閯?chuàng)建字節(jié)碼訪問器非常緩慢,所以 Java 運(yùn)行時(shí)可以緩存這些訪問器以備以后使用。一些應(yīng)用程序和框架還會(huì)緩存反射對(duì)象,這進(jìn)一步增加了它們的本機(jī)內(nèi)存占用。
JNI
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)存占用:
- JNI 應(yīng)用程序的本機(jī)代碼被編譯到共享庫中,或編譯為加載到進(jìn)程地址空間中的可執(zhí)行文件。大型本機(jī)應(yīng)用程序可能僅僅加載就會(huì)占用大量進(jìn)程地址空間。
- 本機(jī)代碼必須與 Java 運(yùn)行時(shí)共享地址空間。任何本機(jī)代碼分配或本機(jī)代碼執(zhí)行的內(nèi)存映射都會(huì)耗用 Java 運(yùn)行時(shí)的內(nèi)存。
- 某些 JNI 函數(shù)可能在它們的常規(guī)操作中使用本機(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 和更高版本會(huì)進(jìn)行本機(jī)復(fù)制)。通過這種方式訪問大量 Java 堆數(shù)據(jù)可能會(huì)使用大量本機(jī)堆。
NIO
Java 1.4 中添加的新 I/O (NIO) 類引入了一種基于通道和緩沖區(qū)來執(zhí)行 I/O 的新方式。就像 Java 堆上的內(nèi)存支持 I/O 緩沖區(qū)一樣,NIO 添加了對(duì)直接 ByteBuffer
的支持(使用 java.nio.ByteBuffer.allocateDirect()
方法進(jìn)行分配), ByteBuffer
受本機(jī)內(nèi)存而不是 Java 堆支持。直接 ByteBuffer
可以直接傳遞到本機(jī)操作系統(tǒng)庫函數(shù),以執(zhí)行 I/O — 這使這些函數(shù)在一些場(chǎng)景中要快得多,因?yàn)樗鼈兛梢员苊庠?Java 堆與本機(jī)堆之間復(fù)制數(shù)據(jù)。
對(duì)于在何處存儲(chǔ)直接 ByteBuffer
數(shù)據(jù),很容易產(chǎn)生混淆。應(yīng)用程序仍然在 Java 堆上使用一個(gè)對(duì)象來編排 I/O 操作,但持有該數(shù)據(jù)的緩沖區(qū)將保存在本機(jī)內(nèi)存中,Java 堆對(duì)象僅包含對(duì)本機(jī)堆緩沖區(qū)的引用。非直接 ByteBuffer
將其數(shù)據(jù)保存在 Java 堆上的 byte[]
數(shù)組中。下圖展示了直接與非直接 ByteBuffer
對(duì)象之間的區(qū)別:
java.nio.ByteBuffer
的內(nèi)存拓?fù)浣Y(jié)構(gòu)
直接 ByteBuffer
對(duì)象會(huì)自動(dòng)清理本機(jī)緩沖區(qū),但這個(gè)過程只能作為 Java 堆 GC 的一部分來執(zhí)行,因此它們不會(huì)自動(dòng)響應(yīng)施加在本機(jī)堆上的壓力。GC 僅在 Java 堆被填滿,以至于無法為堆分配請(qǐng)求提供服務(wù)時(shí)發(fā)生,或者在 Java 應(yīng)用程序中顯式請(qǐng)求它發(fā)生(不建議采用這種方式,因?yàn)檫@可能導(dǎo)致性能問題)。
發(fā)生垃圾收集的情形可能是,本機(jī)堆被填滿,并且一個(gè)或多個(gè)直接 ByteBuffers
適合于垃圾收集(并且可以被釋放來騰出本機(jī)堆的空間),但 Java 堆幾乎總是空的,所以不會(huì)發(fā)生垃圾收集。
線程
應(yīng)用程序中的每個(gè)線程都需要內(nèi)存來存儲(chǔ)器堆棧(用于在調(diào)用函數(shù)時(shí)持有局部變量并維護(hù)狀態(tài)的內(nèi)存區(qū)域)。每個(gè) Java 線程都需要堆棧空間來運(yùn)行。根據(jù)實(shí)現(xiàn)的不同,Java 線程可以分為本機(jī)線程和 Java 堆棧。除了堆??臻g,每個(gè)線程還需要為線程本地存儲(chǔ)(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)存量非常小,但對(duì)于擁有數(shù)百個(gè)線程的應(yīng)用程序來說,線程堆棧的總內(nèi)存使用量可能非常大。如果運(yùn)行的應(yīng)用程序的線程數(shù)量比可用于處理它們的處理器數(shù)量多,效率通常很低,并且可能導(dǎo)致糟糕的性能和更高的內(nèi)存占用。
【編輯推薦】