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

成都創(chuàng)新互聯(lián)主營大峪網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,重慶APP開發(fā)公司,大峪h5微信平臺小程序開發(fā)搭建,大峪網(wǎng)站營銷推廣歡迎大峪等地區(qū)企業(yè)咨詢
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)存必須預(yù)先保留。
保留本機內(nèi)存與分配本機內(nèi)存不同。當(dāng)本機內(nèi)存被保留時,無法使用物理內(nèi)存或其他存儲器作為備用內(nèi)存。盡管保留地址空間塊不會耗盡物理資源,但會阻止內(nèi)存被用于其他用途。由保留從未使用的內(nèi)存導(dǎo)致的泄漏與泄漏分配的內(nèi)存一樣嚴(yán)重。
當(dāng)使用的堆區(qū)域縮小時,一些垃圾收集器會回收堆的一部分(釋放堆的后備存儲空間),從而減少使用的物理內(nèi)存。
對于維護 Java 堆的內(nèi)存管理系統(tǒng),需要更多本機內(nèi)存來維護它的狀態(tài)。當(dāng)進行垃圾收集時,必須分配數(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 應(yīng)用程序以與本機代碼相當(dāng)?shù)乃俣冗\行。
字節(jié)碼編譯使用本機內(nèi)存(使用方式與 gcc 等靜態(tài)編譯器使用內(nèi)存來運行一樣),但 JIT 編譯器的輸入(字節(jié)碼)和輸出(可執(zhí)行代碼)必須也存儲在本機內(nèi)存中。包含多個經(jīng)過 JIT 編譯的方法的 Java 應(yīng)用程序會使用比小型應(yīng)用程序更多的本機內(nèi)存。
類和類加載器
Java 應(yīng)用程序由一些類組成,這些類定義對象結(jié)構(gòu)和方法邏輯。Java 應(yīng)用程序也使用 Java 運行時類庫(比如 java.lang.String)中的類,也可以使用第三方庫。這些類需要存儲在內(nèi)存中以備使用。
存儲類的方式取決于具體實現(xiàn)。Sun JDK 使用***生成(permanent generation,PermGen)堆區(qū)域。Java 5 的 IBM 實現(xiàn)會為每個類加載器分配本機內(nèi)存塊,并將類數(shù)據(jù)存儲在其中?,F(xiàn)代 Java 運行時擁有類共享等技術(shù),這些技術(shù)可能需要將共享內(nèi)存區(qū)域映射到地址空間。要理解這些分配機制如何影響您 Java 運行時的本機內(nèi)存占用,您需要查閱該實現(xiàn)的技術(shù)文檔。然而,一些普遍的事實會影響所有實現(xiàn)。
從最基本的層面來看,使用更多的類將需要使用更多內(nèi)存。(這可能意味著您的本機內(nèi)存使用量會增加,或者您必須明確地重新設(shè)置 PermGen 或共享類緩存等區(qū)域的大小,以裝入所有類)。記住,不僅您的應(yīng)用程序需要加載到內(nèi)存中,框架、應(yīng)用服務(wù)器、第三方庫以及包含類的 Java 運行時也會按需加載并占用空間。
Java 運行時可以卸載類來回收空間,但是只有在非常嚴(yán)酷的條件下才會這樣做。不能卸載單個類,而是卸載類加載器,隨其加載的所有類都會被卸載。只有在以下情況下才能卸載類加載器:
- Java 堆不包含對表示該類加載器的
java.lang.ClassLoader對象的引用。 - Java 堆不包含對表示類加載器加載的類的任何
java.lang.Class對象的引用。 - 在 Java 堆上,該類加載器加載的任何類的所有對象都不再存活(被引用)。
需要注意的是,Java 運行時為所有 Java 應(yīng)用程序創(chuàng)建的 3 個默認(rèn)類加載器( bootstrap、extension 和 application )都不可能滿足這些條件,因此,任何系統(tǒng)類(比如 java.lang.String)或通過應(yīng)用程序類加載器加載的任何應(yīng)用程序類都不能在運行時釋放。
即使類加載器適合進行收集,運行時也只會將收集類加載器作為 GC 周期的一部分。一些實現(xiàn)只會在某些 GC 周期中卸載類加載器。
也可能在運行時生成類,而不用釋放它。許多 JEE 應(yīng)用程序使用 JavaServer Pages (JSP) 技術(shù)來生成 Web 頁面。使用 JSP 會為執(zhí)行的每個 .jsp 頁面生成一個類,并且這些類會在加載它們的類加載器的整個生存期中一直存在 —— 這個生存期通常是 Web 應(yīng)用程序的生存期。
另一種生成類的常見方法是使用 Java 反射。反射的工作方式因 Java 實現(xiàn)的不同而不同,但 Sun 和 IBM 實現(xiàn)都使用了這種方法,我馬上就會講到。
當(dāng)使用 java.lang.reflect API 時,Java 運行時必須將一個反射對象(比如 java.lang.reflect.Field)的方法連接到被反射到的對象或類。這可以通過使用 Java 本機接口(Java Native Interface,JNI)訪問器來完成,這種方法需要的設(shè)置很少,但是速度緩慢。也可以在運行時為您想要反射到的每種對象類型動態(tài)構(gòu)建一個類。后一種方法在設(shè)置上更慢,但運行速度更快,非常適合于經(jīng)常反射到一個特定類的應(yīng)用程序。
Java 運行時在最初幾次反射到一個類時使用 JNI 方法,但當(dāng)使用了若干次 JNI 方法之后,訪問器會膨脹為字節(jié)碼訪問器,這涉及到構(gòu)建類并通過新的類加載器進行加載。執(zhí)行多次反射可能導(dǎo)致創(chuàng)建了許多訪問器類和類加載器。保持對反射對象的引用會導(dǎo)致這些類一直存活,并繼續(xù)占用空間。因為創(chuàng)建字節(jié)碼訪問器非常緩慢,所以 Java 運行時可以緩存這些訪問器以備以后使用。一些應(yīng)用程序和框架還會緩存反射對象,這進一步增加了它們的本機內(nèi)存占用。
JNI
JNI 支持本機代碼(使用 C 和 C++ 等本機編譯語言編寫的應(yīng)用程序)調(diào)用 Java 方法,反之亦然。Java 運行時本身極大地依賴于 JNI 代碼來實現(xiàn)類庫功能,比如文件和網(wǎng)絡(luò) I/O。JNI 應(yīng)用程序可能通過 3 種方式增加 Java 運行時的本機內(nèi)存占用:
- JNI 應(yīng)用程序的本機代碼被編譯到共享庫中,或編譯為加載到進程地址空間中的可執(zhí)行文件。大型本機應(yīng)用程序可能僅僅加載就會占用大量進程地址空間。
- 本機代碼必須與 Java 運行時共享地址空間。任何本機代碼分配或本機代碼執(zhí)行的內(nèi)存映射都會耗用 Java 運行時的內(nèi)存。
- 某些 JNI 函數(shù)可能在它們的常規(guī)操作中使用本機內(nèi)存。
GetTypeArrayElements和GetTypeArrayRegion函數(shù)可以將 Java 堆數(shù)據(jù)復(fù)制到本機內(nèi)存緩沖區(qū)中,以供本機代碼使用。是否復(fù)制數(shù)據(jù)依賴于運行時實現(xiàn)。(IBM Developer Kit for Java 5.0 和更高版本會進行本機復(fù)制)。通過這種方式訪問大量 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 堆與本機堆之間復(fù)制數(shù)據(jù)。
對于在何處存儲直接 ByteBuffer 數(shù)據(jù),很容易產(chǎn)生混淆。應(yīng)用程序仍然在 Java 堆上使用一個對象來編排 I/O 操作,但持有該數(shù)據(jù)的緩沖區(qū)將保存在本機內(nèi)存中,Java 堆對象僅包含對本機堆緩沖區(qū)的引用。非直接 ByteBuffer 將其數(shù)據(jù)保存在 Java 堆上的 byte[] 數(shù)組中。下圖展示了直接與非直接 ByteBuffer 對象之間的區(qū)別:
直接與非直接 java.nio.ByteBuffer 的內(nèi)存拓?fù)浣Y(jié)構(gòu)
直接 ByteBuffer 對象會自動清理本機緩沖區(qū),但這個過程只能作為 Java 堆 GC 的一部分來執(zhí)行,因此它們不會自動響應(yīng)施加在本機堆上的壓力。GC 僅在 Java 堆被填滿,以至于無法為堆分配請求提供服務(wù)時發(fā)生,或者在 Java 應(yīng)用程序中顯式請求它發(fā)生(不建議采用這種方式,因為這可能導(dǎo)致性能問題)。
發(fā)生垃圾收集的情形可能是,本機堆被填滿,并且一個或多個直接 ByteBuffers 適合于垃圾收集(并且可以被釋放來騰出本機堆的空間),但 Java 堆幾乎總是空的,所以不會發(fā)生垃圾收集。
線程
應(yīng)用程序中的每個線程都需要內(nèi)存來存儲器堆棧(用于在調(diào)用函數(shù)時持有局部變量并維護狀態(tài)的內(nèi)存區(qū)域)。每個 Java 線程都需要堆??臻g來運行。根據(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ù)百個線程的應(yīng)用程序來說,線程堆棧的總內(nèi)存使用量可能非常大。如果運行的應(yīng)用程序的線程數(shù)量比可用于處理它們的處理器數(shù)量多,效率通常很低,并且可能導(dǎo)致糟糕的性能和更高的內(nèi)存占用。
【編輯推薦】
- 本機內(nèi)存簡介:操作系統(tǒng),硬件限制及虛擬內(nèi)存
- Java 理論與實踐: 用弱引用堵住內(nèi)存泄漏
- Java內(nèi)存泄漏的檢測和處理
- 幾種典型的Java內(nèi)存泄漏
- 詳細(xì)介紹Java的內(nèi)存管理與內(nèi)存泄露
網(wǎng)頁名稱:Java運行時如何使用本機內(nèi)存
瀏覽地址:http://www.dlmjj.cn/article/dhdojge.html


咨詢
建站咨詢
