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