天天看點

Java記憶體大家都知道,但你知道要怎麼管理Java記憶體嗎?前言堆棧堆引用類型如何引用字元串垃圾回收程序垃圾回收器類型提示和技巧總結最後

前言

深入研究Java記憶體管理,将增強你對堆如何工作、引用類型和垃圾回收的認識。

你可能會思考,如果你使用Java程式設計,關于記憶體如何工作你需要了解哪些哪些資訊?Java可以進行自動記憶體管理,而且有一個很好的、安靜的垃圾回收器,它在背景工作,清理那些未使用的對象并釋放一些記憶體。

Java記憶體大家都知道,但你知道要怎麼管理Java記憶體嗎?前言堆棧堆引用類型如何引用字元串垃圾回收程式垃圾回收器類型提示和技巧總結最後

是以,作為一名Java程式員,你不需要再為銷毀無用對象這樣的問題而煩惱了。但是,雖然這個過程在Java中是自動的,它也不能保證任何事情。由于不知道垃圾回收器和Java記憶體是如何設計的,有些對象即使你不再使用了,卻也不符合垃圾回收的條件。

是以,了解Java中記憶體實際是如何工作的非常重要,因為它為你編寫高性能和優化的應用程式提供了幫助,這些應用程式永遠不會因記憶體不足而崩潰。另一方面,當你發現自己處于糟糕的境地時,你将能夠很快發現記憶體的漏洞。

首先,讓我們看看記憶體在Java中通常是如何組織的:

Java記憶體大家都知道,但你知道要怎麼管理Java記憶體嗎?前言堆棧堆引用類型如何引用字元串垃圾回收程式垃圾回收器類型提示和技巧總結最後

通常,記憶體分為兩大部分:堆棧和堆。請記住,記憶體類型在上圖中的大小與實際記憶體大小不成比例。與堆棧相比,堆是一個巨大數量的記憶體。

堆棧

堆棧記憶體負責儲存對堆對象的引用和存儲值類型(在Java中也稱為基元類型),值類型儲存值本身而不儲存對堆中對象的引用。

此外,堆棧上的變量具有一定的可見性,也稱為作用域。隻有活躍作用域内的對象才能被使用。例如,假設我們沒有任何全局作用域變量(字段),隻有局部變量,如果編譯器執行方法的主體,它隻能通路方法主體内堆棧中的對象。它不能通路其它局部變量,因為這些變量超出了作用域。一旦方法完成并傳回,堆棧頂部就會溢出,活躍作用域也會發生變化。

或許你注意到了在上圖中顯示的多個堆棧記憶體,這是因為Java中的堆棧記憶體是按線程配置設定的。是以,每次一個線程被建立和啟動時,它都有自己的堆棧記憶體,并且不能通路另一個線程的堆棧記憶體。

堆記憶體将實際對象存儲在記憶體中。這些對象被堆棧中的變量引用。例如,讓我們分析下面一行代碼發生了什麼:

StringBuilder builder = new StringBuilder();
           

“new”關鍵字負責確定堆上有足夠的可用空間,在記憶體中建立一個StringBuilder類型的對象,并通過堆棧中的“builder”引用它。

每個正在運作的JVM程序隻有一個堆記憶體。是以,無論運作多少線程,這都是記憶體中的一個共享部分。實際上,堆結構與上圖中顯示的略有不同。堆本身被分成幾個部分,這有助于垃圾回收程序。

最大堆棧和堆大小都沒有預定義 - 這取決于正在運作的計算機。 然而,在後文中,我們将研究一些JVM配置,這些配置允許我們為正在運作的應用程式明确設定它們的大小

​​

Java記憶體大家都知道,但你知道要怎麼管理Java記憶體嗎?前言堆棧堆引用類型如何引用字元串垃圾回收程式垃圾回收器類型提示和技巧總結最後

引用類型

如果仔細觀察記憶體結構圖檔,你或許會注意到,代表對堆中對象引用的箭頭的樣式實際是不同的。這是因為,在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
           

進行上述更改後輸出如下:

垃圾回收程序

正如前面所讨論的,根據堆棧中的變量對堆中對象的引用類型,在某個确定的時間點,該對象符合垃圾回收器的條件。

Java記憶體大家都知道,但你知道要怎麼管理Java記憶體嗎?前言堆棧堆引用類型如何引用字元串垃圾回收程式垃圾回收器類型提示和技巧總結最後

比方說,所有紅色的對象都符合被垃圾回收器的條件。 你可能會注意到堆上有一個對象,它對同一堆上的其它對象進行了強引用(例如,可能是引用了自己項的清單,或者是具有兩個引用類型字段的對象)。但是,由于堆棧中的引用丢失,這個對象就無法再被通路,是以它也成了垃圾。

為了更深入地了解細節,我們先提出以下幾點:

1.這個過程是由Java自動觸發的,何時啟動以及是否啟動此過程取決于Java。

2.實際上這個程序是昂貴的。當垃圾回收器運作時,應用程式中的所有線程都會暫停(取決于GC類型,稍後将對此進行讨論)。

3.這實際上是一個比垃圾回收和釋放記憶體更複雜的程序。

盡管由Java決定何時運作垃圾回收器,你也可以直接調用System.gc( )并期望垃圾回收器在執行這行代碼時運作,對吧?

這是一個錯誤的假設。

你隻需要讓Java運作垃圾回收器,但是是否運作垃圾回收器仍然取決于Java。無論如何,不建議直接調用System.gc( )。

由于這是一個非常複雜的過程,并且它可能會影響你程式的表現,它需要以一個智能的方式實作。 一個被稱作“标記和掃描”的程序來完成此任務。Java分析堆棧中的變量并“标記”所有保持活躍的對象,然後清除所有不會使用的對象。

實際上,Java并沒有回收任何垃圾。事實上,垃圾越多,标記為活躍的對象就越少,程序也就越快。為了使這個程序更加優化,堆記憶體實際由多個部分組成。我們可以通過JVisualVM(Java JDK附帶的工具)可視化記憶體使用情況和其它一些有用的東西。您唯一需要做的就是安裝一個名為Visual GC的插件,它允許您檢視記憶體的實際結構。讓我們放大一點,分解大局:

Java記憶體大家都知道,但你知道要怎麼管理Java記憶體嗎?前言堆棧堆引用類型如何引用字元串垃圾回收程式垃圾回收器類型提示和技巧總結最後

當一個對象被建立時,它被配置設定到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應用程式中有太多字元串,則它可能會崩潰。歡迎大家關注我的公種浩【程式員追風】,文章都會在裡面更新,整理的資料也會放在裡面。

Java記憶體大家都知道,但你知道要怎麼管理Java記憶體嗎?前言堆棧堆引用類型如何引用字元串垃圾回收程式垃圾回收器類型提示和技巧總結最後

垃圾回收器類型

實際上,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。如果使用正确的工具,發現和修複記憶體漏洞隻是一件容易的事情。

最後

歡迎大家一起交流,喜歡文章記得點個贊喲,感謝支援!