深入研究Java記憶體管理
您可能會認為,如果您使用Java程式設計,那麼您需要了解記憶體的工作原理?Java具有自動記憶體管理功能,一個漂亮而安靜的垃圾收集器,可在後臺執行以清理未使用的物件並釋放一些記憶體。
因此,作為Java程式設計師,您不需要為破壞物件等問題而煩惱,因為它們不再被使用。但是,即使這個過程在Java中是自動的,它也不能保證任何東西。通過不知道垃圾收集器和Java記憶體是如何設計的,即使您不再使用它們,也可能有不符合垃圾收集條件的物件。
因此,瞭解記憶體在Java中的實際執行方式非常重要,因為它為您提供了編寫高效能和優化應用程式的優勢,這些應用程式永遠不會崩潰OutOfMemoryError 。另一方面,當您發現自己陷入困境時,您將能夠快速找到記憶體洩漏。
首先,讓我們看看記憶體在Java中的組織方式:
記憶結構
通常,記憶體分為兩大部分:堆疊 和堆。 請記住,此圖片中的記憶體型別大小與實際記憶體大小不成比例。與堆疊相比,堆是一個巨大的記憶體。
堆疊
堆疊記憶體負責儲存對堆物件的引用和儲存值型別(在Java中也稱為基本型別),它們儲存值本身而不是對堆中物件的引用。
此外,堆疊上的變數具有一定的可見性,也稱為範圍 。僅使用活動範圍中的物件。例如,假設我們沒有任何全域性範圍變數(欄位),並且只有區域性變數,如果編譯器執行方法的主體,它只能訪問方法體內的堆疊中的物件。它不能訪問其他區域性變數,因為它們超出了範圍。方法完成並返回後,堆疊頂部彈出,活動範圍發生變化。
也許您注意到在上圖中,顯示了多個堆疊記憶體。這是因為Java中的堆疊記憶體是按Thread分配的。因此,每次建立和啟動Thread時,它都有自己的堆疊記憶體 - 並且無法訪問另一個執行緒的堆疊記憶體。
堆
這部分記憶體將實際物件儲存在記憶體中。這些是由堆疊中的變數引用的。例如,讓我們分析以下程式碼行中發生的情況:
StringBuilder builder = new StringBuilder();
該new關鍵字是負責確保有上堆足夠的自由空間,在儲存器中建立StringBuilder的型別的物件,並通過“助洗劑”的參考,其進入堆疊上提到它。
每個正在執行的JVM程序只存在一個堆記憶體。因此,無論正在執行多少執行緒,這都是記憶體的共享部分。實際上,堆結構與上圖中顯示的有點不同。堆本身分為幾個部分,便於垃圾收集過程。
最大堆疊和堆大小未預定義 - 這取決於正在執行的計算機。但是,在本文後面,我們將研究一些JVM配置,這些配置允許我們為正在執行的應用程式明確指定它們的大小。
參考型別
如果仔細檢視記憶體結構 圖,您可能會注意到表示堆中物件引用的箭頭實際上是不同型別的。這是因為,在Java程式語言中,我們有不同型別的引用:強 引用,弱 引用,軟 引用和幻像 引用。引用型別之間的區別在於它們引用的堆上的物件符合不同條件下的垃圾收集條件。讓我們仔細看看它們中的每一個。
1.強烈的參考
這些是我們都習慣的最流行的參考型別。在上面的StringBuilder示例中,我們實際上對堆中的物件進行了強引用。堆上的物件不是垃圾收集,而是有強引用指向它,或者它是通過強引用鏈強烈可達的。
2.弱參考
簡單來說,在下一個垃圾收集過程之後,對堆中物件的弱引用很可能無法生存。建立弱引用如下:
WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());
WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());
弱引用的一個很好的用例是快取場景。想象一下,您檢索了一些資料,並希望它也儲存在記憶體中 - 可以再次請求相同的資料。另一方面,您不確定何時或是否會再次請求此資料。所以你可以保留一個弱引用,如果垃圾收集器執行,它可能會破壞堆上的物件。因此,過了一段時間,如果要檢索所引用的物件,可能會突然找回一個null值。快取場景的一個很好的實現是集合WeakHashMap <K,V> 。如果我們WeakHashMap在Java API中開啟該類,我們會看到它的條目實際上擴充套件了WeakReference類並使用其ref 欄位作為對映的鍵:
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
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()此類引用的方法將始終返回null。 這些型別的引用被認為優於終結器。
如何 引用字串
Java中的型別有點不同。字串是不可變的,這意味著每次使用字串執行某些操作時,實際上會在堆上建立另一個物件。對於字串,Java管理記憶體中的字串池。這意味著Java會盡可能地儲存和重用字串。這對於字串文字大多是正確的。例如: String
String localPrefix = "297"; //1
String prefix = "297"; //2
if (prefix == localPrefix)
{
System.out.println("Strings are equal" );
}
else
{
System.out.println("Strings are different");
}
執行時,會打印出以下內容:
字串是平等的
因此,事實證明,在比較String型別的兩個引用之後,它們實際上指向堆上的相同物件。但是,這對於計算的字串無效。假設我們在上面程式碼的第// 1行中進行了以下更改
String localPrefix = new Integer(297).toString(); //1
輸出:
字串是不同的
在這種情況下,我們實際上看到堆上有兩個不同的物件。如果我們考慮經常使用計算的String,我們可以通過新增來強制JVM將它新增到字串池中.intern()計算字串末尾的方法:
String localPrefix = new Integer(297).toString().intern(); //1
新增上述更改會建立以下輸出:
字串是平等的
垃圾收集過程
如前所述,根據堆疊中的變數從堆中儲存到物件的引用型別,在某個特定時間點,該物件符合垃圾收集器的條件。
符合垃圾資格的物件
例如,所有紅色的物件都有資格被垃圾收集器收集。您可能會注意到堆上有一個物件,它對堆上的其他物件也有強引用(例如,可能是包含對其項的引用的列表,或者是具有兩個引用型別欄位的物件)。但是,由於堆疊中的引用丟失,因此無法再訪問它,因此它也是垃圾。
為了更深入地瞭解細節,我們首先提一下:
這個過程是由Java自動觸發的,它是由Java決定何時以及是否啟動此過程。
這實際上是一個昂貴的過程。當垃圾收集器執行時,應用程式中的所有執行緒都會暫停(取決於GC型別,稍後將對此進行討論)。
這實際上是一個比垃圾收集和釋放記憶體更復雜的過程。
即使Java決定何時執行垃圾收集器,您也可以顯式呼叫System.gc()並期望垃圾收集器在執行這行程式碼時執行,對吧?
這是一個錯誤的假設。
你只是要求Java執行垃圾收集器,但它是否再次取決於它是否這樣做。無論如何,System.gc()不建議明確打電話 。
由於這是一個非常複雜的過程,並且它可能會影響您的效能,因此它以智慧方式實現。為此使用所謂的“標記和掃描”過程。Java分析堆疊中的變數並“標記”所有需要保持活動的物件。然後,清除所有未使用的物件。
實際上,Java並沒有收集任何垃圾。事實上,垃圾越多,活動標記的物件越少,過程就越快。為了使這更加優化,堆記憶體實際上由多個部分組成。我們可以使用JVisualVM (Java JDK附帶的工具)視覺化記憶體使用情況和其他有用的東西。您唯一需要做的就是安裝一個名為Visual GC 的外掛,它允許您檢視記憶體的實際結構。讓我們放大一點,分解大局:
堆記憶體世代
建立物件時,它將在Eden(1) 空間中分配。因為伊甸園的空間並不那麼大,所以它的速度非常快。垃圾收集器在Eden空間上執行,並將物件標記為活動。
一旦物件在垃圾收集過程中存活,它就會被移動到所謂的倖存者空間S0(2)中 。第二次垃圾收集器在Eden空間上執行時,它會將所有幸存的物件移動到S1(3) 空間中。此外,當前在S0(2) 上的所有內容都將 移動到S1(3) 空間中。
如果一個物件在X輪垃圾收集中存活(X取決於JVM實現,在我的情況下它是8),它很可能永遠存在,並且它被移動到Old(4) 空間。
到目前為止所說的一切,如果你看一下垃圾收集器圖(6) ,每次執行時,你都可以看到物件切換到倖存者空間並且Eden空間獲得了空間。等等等等。老一代也可以被垃圾收集,但由於它與Eden空間相比是記憶體的更大部分,因此通常不會發生這種情況。在元空間(5) 用於元資料儲存有關的JVM您載入的類。
提供的圖片實際上是Java 8應用程式。在Java 8之前,記憶體的結構有點不同。元空間實際上稱為PermGen。空間。例如,在Java 6中,此空間還儲存了字串池的記憶體。因此,如果Java 6應用程式中有太多字串,則可能會崩潰。
垃圾收集器型別
實際上,JVM有三種類型的垃圾收集器,程式設計師可以選擇應該使用哪種垃圾收集器。預設情況下,Java根據底層硬體選擇要使用的垃圾收集器型別。
1.序列GC - 單執行緒收集器。主要適用於資料使用量較小的小型應用程式。可以通過指定命令列選項啟用:-XX:+UseSerialGC
2.並行GC - 即使從命名,序列和並行之間的區別在於並行GC使用多個執行緒來執行垃圾收集過程。此GC型別也稱為吞吐量收集器。可以通過顯式指定選項來啟用它:-XX:+UseParallelGC
3.大多數併發GC - 如果你還記得,在本文前面提到垃圾收集過程實際上相當昂貴,並且當它執行時,所有執行緒都被暫停。但是,我們有這種大多數併發GC型別,它宣告它與應用程式併發工作。但是,它有“大多數”併發的原因。它不能100%同時應用於應用程式。執行緒暫停一段時間。儘管如此,暫停時間儘可能短,以實現最佳的GC效能。實際上,有兩種型別的大多數併發GC:
3.1垃圾優先 - 具有合理應用暫停時間的高吞吐量。啟用選項:-XX:+UseG1GC
3.2併發標記掃描 - 應用程式暫停時間保持最短。可以通過指定選項來使用它:-XX:+UseConcMarkSweepGC。從JDK 9開始,不推薦使用此GC型別。
技巧和竅門
要最小化記憶體佔用,請儘可能限制變數的範圍。請記住,每次彈出堆疊中的頂級作用域時,該作用域的引用都將丟失,這可能使物件有資格進行垃圾回收。
明確引用null過時的引用。這將使那些參考的物件有資格進行垃圾收集。
避免終結者。他們放慢了流程,他們不保證任何事情。更喜歡幻像引用以進行清理工作。
在弱引用或軟引用適用的情況下,請勿使用強引用。最常見的記憶體缺陷是快取方案,即使可能不需要將資料儲存在記憶體中也是如此。
JVisualVM還具有在某一點進行堆轉儲的功能,因此您可以按類分析它佔用的記憶體量。
根據您的應用程式要求配置JVM。執行應用程式時,明確指定JVM的堆大小。記憶體分配過程也很昂貴,因此為堆分配合理的初始和最大記憶體量。如果您知道從一開始就使用較小的初始堆大小是沒有意義的,JVM將擴充套件此記憶體空間。使用以下選項指定記憶體選項:
初始堆大小-Xms512m- 將初始堆大小設定為512 MB。
最大堆大小-Xmx1024m- 將最大堆大小設定為1024兆位元組。
執行緒堆疊大小-Xss128m- 將執行緒堆疊大小設定為128兆位元組。
年輕一代的大小-Xmn256m- 將年輕代的大小設定為256兆位元組。
如果Java應用程式崩潰OutOfMemoryError並且您需要一些額外的資訊來檢測洩漏,請執行該過程–XX:HeapDumpOnOutOfMemory引數,下次發生此錯誤時將建立堆轉儲檔案。
使用該-verbose:gc選項獲取垃圾收集輸出。每次進行垃圾收集時,都會生成一個輸出。
結論
瞭解記憶體的組織方式可以為您提供在記憶體資源方面編寫優質程式碼的優勢。有利的是,您可以通過提供最適合您正在執行的應用程式的不同配置來調整正在執行的JVM。如果使用正確的工具,查詢和修復記憶體洩漏只是一件容易的事。
另外本人從事線上教育多年,將自己的資料整合建了一個公眾號,對於有興趣一起交流學習java的微信搜尋:“程式設計師文明”,裡面有大神會給予解答,也會有許多的資源可以供大家學習分享,歡迎大家前來一起學習進步!