您好,登錄后才能下訂單哦!
前言
深入研究Java內(nèi)存管理,將增強(qiáng)你對(duì)堆如何工作、引用類(lèi)型和垃圾回收的認(rèn)識(shí)。
你可能會(huì)思考,如果你使用Java編程,關(guān)于內(nèi)存如何工作你需要了解哪些哪些信息?Java可以進(jìn)行自動(dòng)內(nèi)存管理,而且有一個(gè)很好的、安靜的垃圾回收器,它在后臺(tái)工作,清理那些未使用的對(duì)象并釋放一些內(nèi)存。
因此,作為一名Java程序員,你不需要再為銷(xiāo)毀無(wú)用對(duì)象這樣的問(wèn)題而煩惱了。但是,雖然這個(gè)過(guò)程在Java中是自動(dòng)的,它也不能保證任何事情。由于不知道垃圾回收器和Java內(nèi)存是如何設(shè)計(jì)的,有些對(duì)象即使你不再使用了,卻也不符合垃圾回收的條件。
因此,了解Java中內(nèi)存實(shí)際是如何工作的非常重要,因?yàn)樗鼮槟憔帉?xiě)高性能和優(yōu)化的應(yīng)用程序提供了幫助,這些應(yīng)用程序永遠(yuǎn)不會(huì)因內(nèi)存不足而崩潰。另一方面,當(dāng)你發(fā)現(xiàn)自己處于糟糕的境地時(shí),你將能夠很快發(fā)現(xiàn)內(nèi)存的漏洞。
首先,讓我們看看內(nèi)存在Java中通常是如何組織的:
通常,內(nèi)存分為兩大部分:堆棧和堆。請(qǐng)記住,內(nèi)存類(lèi)型在上圖中的大小與實(shí)際內(nèi)存大小不成比例。與堆棧相比,堆是一個(gè)巨大數(shù)量的內(nèi)存。
堆棧
堆棧內(nèi)存負(fù)責(zé)保存對(duì)堆對(duì)象的引用和存儲(chǔ)值類(lèi)型(在Java中也稱(chēng)為基元類(lèi)型),值類(lèi)型保存值本身而不保存對(duì)堆中對(duì)象的引用。
此外,堆棧上的變量具有一定的可見(jiàn)性,也稱(chēng)為作用域。只有活躍作用域內(nèi)的對(duì)象才能被使用。例如,假設(shè)我們沒(méi)有任何全局作用域變量(字段),只有局部變量,如果編譯器執(zhí)行方法的主體,它只能訪問(wèn)方法主體內(nèi)堆棧中的對(duì)象。它不能訪問(wèn)其它局部變量,因?yàn)檫@些變量超出了作用域。一旦方法完成并返回,堆棧頂部就會(huì)溢出,活躍作用域也會(huì)發(fā)生變化。
或許你注意到了在上圖中顯示的多個(gè)堆棧內(nèi)存,這是因?yàn)镴ava中的堆棧內(nèi)存是按線程分配的。因此,每次一個(gè)線程被創(chuàng)建和啟動(dòng)時(shí),它都有自己的堆棧內(nèi)存,并且不能訪問(wèn)另一個(gè)線程的堆棧內(nèi)存。
堆
堆內(nèi)存將實(shí)際對(duì)象存儲(chǔ)在內(nèi)存中。這些對(duì)象被堆棧中的變量引用。例如,讓我們分析下面一行代碼發(fā)生了什么:
StringBuilder?builder?=?new?StringBuilder();
“new”關(guān)鍵字負(fù)責(zé)確保堆上有足夠的可用空間,在內(nèi)存中創(chuàng)建一個(gè)StringBuilder類(lèi)型的對(duì)象,并通過(guò)堆棧中的“builder”引用它。
每個(gè)正在運(yùn)行的JVM進(jìn)程只有一個(gè)堆內(nèi)存。因此,無(wú)論運(yùn)行多少線程,這都是內(nèi)存中的一個(gè)共享部分。實(shí)際上,堆結(jié)構(gòu)與上圖中顯示的略有不同。堆本身被分成幾個(gè)部分,這有助于垃圾回收進(jìn)程。
最大堆棧和堆大小都沒(méi)有預(yù)定義 - 這取決于正在運(yùn)行的計(jì)算機(jī)。 然而,在后文中,我們將研究一些JVM配置,這些配置允許我們?yōu)檎谶\(yùn)行的應(yīng)用程序明確設(shè)定它們的大小
引用類(lèi)型
如果仔細(xì)觀察內(nèi)存結(jié)構(gòu)圖片,你或許會(huì)注意到,代表對(duì)堆中對(duì)象引用的箭頭的樣式實(shí)際是不同的。這是因?yàn)?,在Java編程語(yǔ)言中,我們有不同類(lèi)型的引用:強(qiáng)引用、弱引用、軟引用和虛引用。引用類(lèi)型之間的區(qū)別在于它們所引用堆上的對(duì)象在不同的條件下可以被作為垃圾回收。讓我們來(lái)仔細(xì)認(rèn)識(shí)一下每一種引用類(lèi)型。
1. 強(qiáng)引用>>>
這種引用類(lèi)型是我們都習(xí)慣并且最受歡迎的引用類(lèi)型。在上面的StringBuilder示例中,我們實(shí)際上使用了對(duì)堆中對(duì)象的強(qiáng)引用。當(dāng)有一個(gè)強(qiáng)引用指向堆上的對(duì)象時(shí),或者通過(guò)一系列強(qiáng)引用可以強(qiáng)訪問(wèn)該對(duì)象,則該對(duì)象不會(huì)被作為垃圾回收。
2. 弱引用>>
簡(jiǎn)單來(lái)說(shuō),在下一個(gè)垃圾回收進(jìn)程之后,對(duì)堆中對(duì)象的弱引用很可能不會(huì)繼續(xù)存在了。弱引用的創(chuàng)建示例如下:
WeakReference<StringBuilder>?reference?=?new?WeakReference<>(new?StringBuilder());
弱引用的一個(gè)很好的用例是緩存方案。假設(shè)你檢索了一些數(shù)據(jù),并且還希望將其存儲(chǔ)在內(nèi)存中—這樣同樣的數(shù)據(jù)可以被再次請(qǐng)求。另一方面,你不確定何時(shí)或者是否會(huì)再次請(qǐng)求這些數(shù)據(jù)。因此,你可以保留對(duì)它的弱引用,萬(wàn)一垃圾回收器運(yùn)行,它可能會(huì)破壞堆中的對(duì)象。因此,過(guò)了一會(huì)兒,如果你想要檢索你引用的對(duì)象,你可能會(huì)突然得到一個(gè)空的返回值。緩存方案的一個(gè)很好的使用是回收WeakHashMap。如果我們?cè)贘ava API中打開(kāi)WeakHashMap類(lèi),我們會(huì)看到它的條目實(shí)際上擴(kuò)展了WeakReference類(lèi),并使用它的引用字段作為映射的關(guān)鍵字:
private?static?class?Entry<K,V>?extends?WeakReference<Object>?implements?Map.Entry<K,V>? {? V?value; ...... }
一旦WeakHashMap中的一個(gè)關(guān)鍵字被進(jìn)行了垃圾回收,整個(gè)條目就會(huì)從映射中移除。
3. 軟引用>>>
這種引用類(lèi)型用于對(duì)內(nèi)存更敏感的方案,因?yàn)橹挥挟?dāng)應(yīng)用程序內(nèi)存不足時(shí),所引用的對(duì)象才會(huì)被作為垃圾回收。因此,只要沒(méi)有迫切需要釋放出一些內(nèi)存空間,垃圾回收器就不會(huì)去回收軟引用的對(duì)象。Java保證在拋出OutOfMemoryError之前清除所有軟引用的對(duì)象。Javadocs表明:“在虛擬機(jī)拋出OutOfMemoryError之前,所有對(duì)可軟訪問(wèn)對(duì)象的軟引用都會(huì)確保被清除?!?與弱引用類(lèi)似,軟引用的創(chuàng)建示例如下:
SoftReference<StringBuilder>?reference?=?new?SoftReference<>(new?StringBuilder()); ...... }
4. 虛引用>>>
用于算法檢查后的清理操作,因?yàn)槲覀冎烙行?duì)象不需要再存在。僅與引用隊(duì)列一起使用,因?yàn)榇祟?lèi)引用的.get()方法將始終返回空值。這些引用類(lèi)型被認(rèn)為是優(yōu)于終結(jié)器的。
如何引用字符串
Java中對(duì)字符串類(lèi)型的處理略有不同。字符串是不可變的,這意味著每次使用字符串執(zhí)行操作時(shí),實(shí)際上都會(huì)在堆上創(chuàng)建另一個(gè)對(duì)象。對(duì)于字符串,Java在內(nèi)存中進(jìn)行字符串池管理。這意味著Java會(huì)盡可能地存儲(chǔ)和重用字符串。對(duì)于字符串文字,更是這樣。例如:
String?localPrefix?=?"297";?//1 String?prefix?=?"297";??????//2 if?(prefix?==?localPrefix) { ????System.out.println("Strings?are?equal"?); } else { ????System.out.println("Strings?are?different"); }
運(yùn)行時(shí),將輸出以下內(nèi)容:
Strings are equal
因此,可以看出在比較了字符串類(lèi)型的兩個(gè)引用之后,它們實(shí)際上指向了堆中的相同對(duì)象。但是,這對(duì)于被計(jì)算的字符串無(wú)效。假設(shè)我們對(duì)上述代碼的//1行進(jìn)行以下更改
String?localPrefix?=?new?Integer(297).toString();?//1
輸出:
Strings are different
在這種情況下,我們實(shí)際上看到堆上有兩個(gè)不同的對(duì)象。如果我們考慮到計(jì)算出的字符串會(huì)被經(jīng)常使用,我們可以強(qiáng)制JVM通過(guò)在計(jì)算的字符串末尾添加.intern()方法將計(jì)算的字符串添加到字符串池當(dāng)中:
String?localPrefix?=?new?Integer(297).toString().intern();?//1
進(jìn)行上述更改后輸出如下:
Strings are equal
垃圾回收進(jìn)程
正如前面所討論的,根據(jù)堆棧中的變量對(duì)堆中對(duì)象的引用類(lèi)型,在某個(gè)確定的時(shí)間點(diǎn),該對(duì)象符合垃圾回收器的條件。
比方說(shuō),所有紅色的對(duì)象都符合被垃圾回收器的條件。 你可能會(huì)注意到堆上有一個(gè)對(duì)象,它對(duì)同一堆上的其它對(duì)象進(jìn)行了強(qiáng)引用(例如,可能是引用了自己項(xiàng)的列表,或者是具有兩個(gè)引用類(lèi)型字段的對(duì)象)。但是,由于堆棧中的引用丟失,這個(gè)對(duì)象就無(wú)法再被訪問(wèn),因此它也成了垃圾。
為了更深入地了解細(xì)節(jié),我們先提出以下幾點(diǎn):
1.這個(gè)過(guò)程是由Java自動(dòng)觸發(fā)的,何時(shí)啟動(dòng)以及是否啟動(dòng)此過(guò)程取決于Java。
2.實(shí)際上這個(gè)進(jìn)程是昂貴的。當(dāng)垃圾回收器運(yùn)行時(shí),應(yīng)用程序中的所有線程都會(huì)暫停(取決于GC類(lèi)型,稍后將對(duì)此進(jìn)行討論)。
3.這實(shí)際上是一個(gè)比垃圾回收和釋放內(nèi)存更復(fù)雜的進(jìn)程。
盡管由Java決定何時(shí)運(yùn)行垃圾回收器,你也可以直接調(diào)用System.gc( )并期望垃圾回收器在執(zhí)行這行代碼時(shí)運(yùn)行,對(duì)吧?
這是一個(gè)錯(cuò)誤的假設(shè)。
你只需要讓Java運(yùn)行垃圾回收器,但是是否運(yùn)行垃圾回收器仍然取決于Java。無(wú)論如何,不建議直接調(diào)用System.gc( )。
由于這是一個(gè)非常復(fù)雜的過(guò)程,并且它可能會(huì)影響你程序的表現(xiàn),它需要以一個(gè)智能的方式實(shí)現(xiàn)。 一個(gè)被稱(chēng)作“標(biāo)記和掃描”的進(jìn)程來(lái)完成此任務(wù)。Java分析堆棧中的變量并“標(biāo)記”所有保持活躍的對(duì)象,然后清除所有不會(huì)使用的對(duì)象。
實(shí)際上,Java并沒(méi)有回收任何垃圾。事實(shí)上,垃圾越多,標(biāo)記為活躍的對(duì)象就越少,進(jìn)程也就越快。為了使這個(gè)進(jìn)程更加優(yōu)化,堆內(nèi)存實(shí)際由多個(gè)部分組成。我們可以通過(guò)JVisualVM(Java JDK附帶的工具)可視化內(nèi)存使用情況和其它一些有用的東西。您唯一需要做的就是安裝一個(gè)名為Visual GC的插件,它允許您查看內(nèi)存的實(shí)際結(jié)構(gòu)。讓我們放大一點(diǎn),分解大局:
當(dāng)一個(gè)對(duì)象被創(chuàng)建時(shí),它被分配到Eden(1)區(qū)。因?yàn)镋den區(qū)的空間沒(méi)有那么大,它很快就滿(mǎn)了。垃圾回收器在Eden區(qū)運(yùn)行,并標(biāo)記出活躍的對(duì)象。
一旦一個(gè)對(duì)象在一次垃圾回收進(jìn)程中存活,它就會(huì)被移動(dòng)到所謂的幸存者區(qū)S0(2)中。 垃圾器第回收二次在Eden區(qū)上運(yùn)行時(shí),它會(huì)將所有幸存的對(duì)象移動(dòng)到S1(3)區(qū)中。此外,當(dāng)前在S0(2)區(qū)上的所有內(nèi)容都將被移動(dòng)到S1(3)區(qū)中。
如果一個(gè)對(duì)象在X輪垃圾回收中存活了下來(lái)(取決于JVM的實(shí)現(xiàn),在我的例子中是8輪),那么它很可能會(huì)永遠(yuǎn)存活下來(lái),并被移入到Old(4)區(qū)。
結(jié)合目前為止所說(shuō)的一切,如果你看一下圖中標(biāo)號(hào)(6)的垃圾回收器,它每次運(yùn)行時(shí),你都可以看到對(duì)象切換到幸存者空間,并且Eden區(qū)的空間增大了。如此反復(fù)。老一代也可以被作為垃圾回收,但由于它在內(nèi)存中空間是比Eden區(qū)更大的部分,因此這種情況不會(huì)經(jīng)常發(fā)生。Metaspace(5)用于在JVM中存儲(chǔ)已加載類(lèi)的元數(shù)據(jù)。
所呈現(xiàn)的圖片實(shí)際上是一個(gè)Java 8的應(yīng)用程序。在Java 8之前的版本,內(nèi)存的結(jié)構(gòu)有點(diǎn)不同。元空間實(shí)際上稱(chēng)為PermGen. 區(qū)。例如,在Java 6中,此空間還為字符串池存儲(chǔ)了內(nèi)存。因此,如果Java 6應(yīng)用程序中有太多字符串,則它可能會(huì)崩潰。歡迎大家關(guān)注我的公種浩【程序員追風(fēng)】,文章都會(huì)在里面更新,整理的資料也會(huì)放在里面。
垃圾回收器類(lèi)型
實(shí)際上,JVM有三種類(lèi)型的垃圾回收器,程序員可以選擇應(yīng)該使用哪種垃圾回收器。默認(rèn)情況下,Java根據(jù)底層硬件選擇要使用的垃圾回收器類(lèi)型。
1.串行垃圾回收器 - 一個(gè)單線程回收器。 主要適用于數(shù)據(jù)使用量較小的小型應(yīng)用程序。 可以通過(guò)指定命令行選項(xiàng)來(lái)啟用:-XX:+ UseSerialGC
2.并行垃圾回收器 - 從命名可以看出,串行垃圾回收器和并行垃圾回收器之間的區(qū)別在于并行垃圾回收器使用多個(gè)線程來(lái)執(zhí)行垃圾回收進(jìn)行。并行垃圾回收器也被稱(chēng)作吞吐量回收器??梢酝ㄟ^(guò)直接指定選項(xiàng)來(lái)啟用它:-XX:+ UseParallelGC
3.主要并發(fā)標(biāo)記垃圾回收器 - 如果你還記得,在本文前面提到垃圾回收過(guò)程實(shí)際上相當(dāng)昂貴,并且當(dāng)它運(yùn)行時(shí),所有線程都被暫停。但是,我們有這種大多數(shù)并發(fā)GC類(lèi)型,它聲明它與應(yīng)用程序并發(fā)工作。但是,它有“大多數(shù)”并發(fā)的原因。它不能100%同時(shí)應(yīng)用于應(yīng)用程序。線程暫停一段時(shí)間。盡管如此,暫停時(shí)間盡可能短,以實(shí)現(xiàn)最佳的GC性能。實(shí)際上,有兩種類(lèi)型的大多數(shù)并發(fā)GC:
3.1垃圾優(yōu)先 - 應(yīng)用程序合理暫停時(shí)間內(nèi)的高吞吐量。 通過(guò)以下選項(xiàng)啟用:-XX:+ UseG1GC
3.2并發(fā)標(biāo)記掃描 - 應(yīng)用程序暫停時(shí)間保持最短??梢酝ㄟ^(guò)指定選項(xiàng)來(lái)啟用:-XX:+ UseConcMarkSweepGC。從JDK 9開(kāi)始,這個(gè)垃圾回收器類(lèi)型不推薦使用。。
提示和技巧
1.為了最小化內(nèi)存的占用,請(qǐng)盡可能限制變量的作用域。請(qǐng)記住,每次堆棧中的頂級(jí)作用域溢出時(shí),來(lái)自該作用域的引用都會(huì)丟失,這可能會(huì)導(dǎo)致相應(yīng)的對(duì)象被作為垃圾回收。
2.直接對(duì)空的、廢棄對(duì)象的引用,這會(huì)導(dǎo)致被引用的對(duì)象被作為垃圾回收。
3.避免成為終結(jié)者。 它們放慢了進(jìn)程,不保證任何事情, 更喜歡進(jìn)行對(duì)虛引用的清理工作。
4.當(dāng)弱引用或軟引用適用時(shí),請(qǐng)不要使用強(qiáng)引用。最常見(jiàn)的內(nèi)存缺陷是緩存方案,即使數(shù)據(jù)可能不需要,也會(huì)被保存在內(nèi)存中。
5.JVisualVM還具有在某一點(diǎn)時(shí)間點(diǎn)進(jìn)行堆轉(zhuǎn)儲(chǔ)的功能,因此你可以分析每一類(lèi)所占用的內(nèi)存量。
6.根據(jù)你的應(yīng)用程序需求來(lái)配置JVM。運(yùn)行應(yīng)用程序時(shí),明確指定JVM的堆大小。內(nèi)存分配進(jìn)程是寶貴的,因此要為堆分配一個(gè)合理的初始最大內(nèi)存空間。如果你知道一開(kāi)始使用較小的初始堆空間是沒(méi)有意義的,JVM將擴(kuò)展這個(gè)內(nèi)存空間。 根據(jù)以下命令來(lái)明確內(nèi)存空間:
(1)初始堆大小 -Xms512m 將初始堆大小設(shè)置為512 mb。
(2)最大堆大小 -Xmx1024m 將最大堆大小設(shè)置為1024 mb。
(3)線程堆棧大小 -Xss128m 將線程堆棧大小設(shè)置為128mb。
(4)新生代堆大小 -Xmn256m 將新生代堆大小設(shè)置為256mb。
7.如果Java應(yīng)用程序崩潰并出現(xiàn)OutOfMemoryError,你需要一些額外的信息來(lái)檢測(cè)漏洞,運(yùn)行以下進(jìn)程:-XX:HeapDumpOnOutOfMemory,它將在下次發(fā)生此錯(cuò)誤時(shí)創(chuàng)建堆轉(zhuǎn)儲(chǔ)文件。
8.使用-verbose:gc選項(xiàng)獲取垃圾回收輸出。 每次進(jìn)行垃圾回收時(shí),都會(huì)生成一個(gè)輸出
總結(jié)
從內(nèi)存資源的角度看,了解內(nèi)存是如何組織的,會(huì)為你編寫(xiě)良好、優(yōu)化的代碼提供優(yōu)勢(shì)。這樣做的好處是,你可以通過(guò)提供最適合你所運(yùn)行應(yīng)用程序的不同配置,來(lái)優(yōu)化你正在運(yùn)行的JVM。如果使用正確的工具,發(fā)現(xiàn)和修復(fù)內(nèi)存漏洞只是一件容易的事情。
最后
歡迎大家一起交流,喜歡文章記得點(diǎn)個(gè)贊喲,感謝支持!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。