日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java中的垃圾回收

發布時間:2024/2/28 java 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java中的垃圾回收 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

轉載自http://www.wolfbe.com/detail/201609/365.html

GC算法基礎

摘要:研究人員發現應用中絕大多數的內存分配會分為兩大類:絕大部分的對象很快會變為不可用狀態;還有一些,它們的存活時間通常也不會很長。 《GC算法基礎》中對標記刪除算法的介紹更多還是偏理論性質的。實踐中,為了更好地滿足現實的場景及需求,還需要對算法進行大量的調整。舉個簡單的例子,我們來看下JVM需要記錄哪些信息才能讓我們得以安全地分配對象空間。

碎片及整理(Fragmenting and Compacting)

JVM在清除不可達對象之后,還得確保它們所在的空間是可以進行復用的。對象刪除會導致碎片的出現,這有點類似于磁盤碎片,這會帶來兩個問題:

  • 寫操作會變得更加費時,因為查找下一個可用空閑塊已不再是一個簡單操作。
  • JVM在創建新對象的,會在連續的區塊中分配內存。因此如果碎片已經嚴重到沒有一個空閑塊能足夠容納新創建的對象時,內存分配便會報錯。

為了避免此類情形,JVM需要確保碎片化在可控范圍內。因此,在垃圾回收的過程中,除了進行標記和刪除外,還有一個“內存去碎片化”的過程。在這個過程當中,會給可達對象重新分配空間,讓它們互相緊挨著對方,這樣便可以去除碎片。下圖展示的便是這一過程:

分代假設

如前所述,垃圾回收需要完全中止應用運行。顯然,對象越多,回收的時間也越長。那么我們能不能在更小的內存區域上進行回收呢?通過可行性調查,一組研究人員發現應用中絕大多數的內存分配會分為兩大類:

  • 絕大部分的對象很快會變為不可用狀態。
  • 還有一些,它們的存活時間通常也不會很長。

這些結論最終構成了弱分代假設(Weak Generational Hypothesis)。基于這一假設,虛擬機內的內存被分為兩類,新生代(Young Generation)及老生代(Old Generation)。后者又被稱為年老代(Tenured Generation)。

有了各自獨立的可清除區域后,這才出現了眾多不同的回收算法,正是它們一直以來在持續提升著GC的性能。

這并不說明這樣的方式是沒有問題的。比如說,不同分代中的對象可能彼此間有引用,在進行分代回收時,它們便為視為是“事實上”的GC根對象(GC roots)。

而更為重要的是,分代假設對于某些應用來說并不成立。由于GC算法主要是為那些“快速消失”或者“永久存活”的對象而進行的優化,因此對于那些生命周期“適中的對象,JVM就顯得無能為力了。

內存池

在堆里面進行內存池的劃分對大家來說應該是非常熟悉的了。不過大家可能不太清楚的是在不同的內存池中,垃圾回收是如何履行它的職責的。值得注意的是,雖然不同的GC算法細節實現上有所不同,但是本章中所提到的概念卻是大同小異的。


伊甸區(Eden)

新對象被創建時,通常便會被分配到伊甸區。由于通常都會有多個線程在同時分配大量的對象,因為伊甸區又被進一步劃分成一個或多個線程本地分配緩沖(Thread Local Allocation Buffer,簡稱TLAB)。有了這些緩沖區使得JVM中大多數對象的分配都可以在各個線程自己對應的TLAB中完成,從而避免了線程間昂貴的同步開銷。

如果在TLAB中無法完成分配(通常是由于沒有足夠的空間),便會到伊甸區的共享空間中進行分配。如果這里還是沒有足夠的空間,則會觸發一次新生代垃圾回收的過程來釋放空間。如果垃圾回收后伊甸區還是沒有足夠的空間,那么這個對象便會到老生代中去分配。

當進行伊甸區的回收時,垃圾回收器會從根對象開始遍歷所有的可達對象,并將它們標記為存活狀態。

前面我們已經提到,對象間可能會存在跨代引用,因此最直觀的做法便是掃描其它分區到伊甸區的所有引用。但不幸的是這么做會做成分代的做法變得毫無意義。JVM對此有它自己的妙招:卡片式標記(card-marking)。基本的做法是,JVM將伊甸區中可能存在老生代引用的對象標記為”臟”對象。關于這點Nitsan的博客這里有更進一步的介紹。

標記完成后,所有存活對象會被復制到其中的一個存活區。于是整個伊甸區便可認為是清空了,又可以重新用來分配對象了。這一過程便被稱為”標記復制“:存活對象先被標記,隨后被復制到存活區中。


存活區(Survivor)

緊挨著伊甸區的是兩個存活區,分別是from區和to區。值得一提的是其中的一個存活區始終都是空的。

空的存活區會在下一次新生代GC的時候迎來它的居民。整個新生代中的所有存活對象(包含伊甸區以及那個非空的名為from的存活區)都會被復制到to區中。一旦完成之后,對象便都跑到to區中而from區則被清空了。這時兩者的角色便會發生調轉。

存活對象會不斷地在兩個存活區之間來回地復制,直到其中的一些對象被認為是已經成熟,“足夠老”了。請記住這點,基于分代假設,已經存活了一段時間的對象,在相當長的一段時間內仍可能繼續存活。

這些“年老”的對象會被提升至老年代空間。出現對象提升的時候,這些對象則不會再被復制到另一個存活區,而是直接復制到老年代中,它們會一直待到不再被引用為止。

垃圾回收器會跟蹤每個對象歷經的回收次數,來判斷它們是否已經“足夠年老”,可以傳播至老年代中。在一輪GC完成之后,每個分區中存活下來的對象的計數便會加一。當一個對象的年齡超過了一個特定的年老閾值之后,它便會被提升到老年代中。

JVM會動態地調整實際的年齡閾值,不過通過指定-XX:+MaxTenuringThreshold參數可以給該值設置一個上限。將-XX:+MaxTenuringThreshold設置為0則立即觸發對象提升,而不會復制到存活區中。在現代的JVM中,這個值默認會被設置為15個GC周期。在HotSpot虛擬機中這也是該值的上限。

如果存活區的大小不足以存放所有的新生代存活對象,則會出現過早提升。


老年代

老年代的內存空間的實現則更為復雜。老年代的空間通常都會非常大,里面存放的對象都是不太可能會被回收的。

老年代的GC比新生代的GC發生的頻率要少得多。由于老年代中的多數對象都被認為是存活的,也就不會存在標記-復制操作了。在GC中,這些對象會被挪到一起以減少碎片。老年代的回收算法通常都是根據不同的理論來構建的。不過大體上都會分成如下幾步:

  • 標記可達對象,設置GC根對象可達的所有對象后的標記位
  • 刪除不可達對象
  • 整理老年代空間的對象,將存活對象復制到老年代開始的連續空間內。

從以上描述中可知,為了避免過度碎片化,老年代的GC是明確需要進行整理操作的。


永久代

在Java 8以前還有一個特殊的空間叫做永久代(Permanent Generation)。這是元數據比如類相關數據存放的地方。除此之外,像駐留的字符串(internalized string)也會被存放在持久代中。這的確給Java開發人員帶來了不少麻煩事,因為很難評估這究竟會使用到多少空間。評估不到位偏會拋出java.lang.OutOfMemoryError: Permgen space的異常。只要不是真的因為內存泄漏而引起的OutOfMemoryError異常,可以通過增加永久代空間的大小來解決這一問題,比如下例中的把永久代最大空間設置為256MB:

java -XX:MaxPermSize=256m com.mycompany.MyApplication

元空間

由于元數據空間大小的預測是件繁瑣且低效的工作,于是Java 8中干脆就去掉了持久代,轉而推出了元空間。從此以后,那些個雜七雜八的東西便都存儲到正常的Java堆了。

但是,類定義如今則是存儲到了元空間里。它存儲在本地內存中,不會與堆 內存相混雜。默認情況下,元空間的大小只受限于Java進程的可用本地內存的大小。這大大解放了開發人員,他們不會再因為多增加了一個類而引發java.lang.OutOfMemoryError: Permgen space異常了。值得注意的是,雖然看似元空間大小毫無限制了,但這一些并非是沒有代價的——如果任由元空間無節制地增長,你可能會面臨的是頻繁的內存交換(swapping)或者是本地內存分配失敗。

如果你希望避免此類情況,可以像下例中這樣限制一下元空間的大小,將它設置成比如256MB:

java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication

新生代GC(Minor GC) vs 老年代GC(Major GC) vs Full GC

清除堆內存不同區域的垃圾回收事件又被稱為新生代GC,老生代GC,以及Full GC事件。本章我們將介紹一下不同事件的區別在哪里。不過你會發現其實各自的差別并不是那么重要。

重要的是我們希望知道應用是否到達它的服務能力上限了,而這又只能去監控應用的處理延時或者吞吐量。只有在這個時間GC事件才能派上用場。這些事件的關鍵之處在于它們是否停止了應用的運行,以及停了多久。

不過由于新生代GC,老生代GC,Full GC這幾個術語被廣泛使用卻又沒有一個清晰的定義,我們還是先來詳細地介紹一下它們的區別再說吧。


新生代GC

新生代垃圾的回收被稱作Minor GC。這個定義非常清晰,理解起來也不會有什么歧義。不過當處理新生代GC事件時,還是有一些有意思的東西值得注意的:

  • 只要JVM無法為新創建的對象分配空間,就肯定會觸發新生代GC,比方說Eden區滿了。因此對象創建得越頻繁,新生代GC肯定也更頻繁。
  • 一旦內存池滿了,它的所有內容就會被拷貝走,指針又將重新歸零。因此和經典的標記(Mark),清除(Sweep),整理(Compact)的過程不同的是,Eden區和Survivor區的清理只涉及到標記和拷貝。在它們中是不會出現碎片的。寫指針始終在當前使用區的頂部。
  • 在一次新生代GC事件中,通常不涉及到年老代。年老代到年輕代的引用被認為是GC的根對象。而在標記階段中,從年輕代到年老代的引用則會被忽略掉。
  • 和通常所理解的不一樣的是,所有的新生代GC都會觸發“stop-the-world”暫停,這會中斷應用程序的線程。對絕大多數應用而言,暫停的時間是可以忽略不計的。如果Eden區中的大多數對象都是垃圾對象并且永遠不會被拷貝到Survivor區/年老代中的話,這么做是合理的。如果恰好相反的話,那么絕大多數的新生對象都不應該被回收,新生代GC的暫停時間就會變得相對較長了。
  • 現在來看新生代GC還是很清晰的——每一次新生代GC都會對年輕代進行垃圾清除


    老年代GC與Full GC

    你會發現關于這兩種GC其實并沒有明確的定義。JVM規范或者垃圾回收相關的論文中都沒有提及。不過從直覺來說,根據新生代GC(Minor GC)清理的是新生代空間的認識來看,不難得出以下推論(這里應當從英文出發來理解,Minor, Major與Full GC,翻譯過來的名稱已經帶有明顯的釋義了):

    • Major GC清理的是老年代的空間。
    • Full GC清理的是整個堆——包括新生代與老年代空間

    不幸的是這么理解會有一點復雜與困惑。首先——許多老年代GC其實是由新生代GC觸發的,因此在很多情況下兩者無法孤立來看待。另一方面——許多現代的垃圾回收器會對老年代進行部分清理,因此,使用“清理”這個術語則顯得有點牽強。

    那么問題就來了,先別再糾結某次GC到底是老年代GC還是Full GC了,你應該關注的是這次GC是否中斷了應用線程還是能夠和應用線程并發地執行

    即便是在JVM的官方工具中,也存在著這一困擾。通過一個例子來說明應該更容易理解一些。我們用兩款工具來跟蹤某個運行著CMS回收器的JVM,來比較下它們的輸出有什么不同:

    首先通過jstat的輸出來查看下GC的信息:

    my-precious: me$ jstat -gc -t 4235 1s Time S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 5.7 34048.0 34048.0 0.0 34048.0 272640.0 194699.7 1756416.0 181419.9 18304.0 17865.1 2688.0 2497.6 3 0.275 0 0.000 0.275 6.7 34048.0 34048.0 34048.0 0.0 272640.0 247555.4 1756416.0 263447.9 18816.0 18123.3 2688.0 2523.1 4 0.359 0 0.000 0.359 7.7 34048.0 34048.0 0.0 34048.0 272640.0 257729.3 1756416.0 345109.8 19072.0 18396.6 2688.0 2550.3 5 0.451 0 0.000 0.451 8.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0 444982.5 19456.0 18681.3 2816.0 2575.8 7 0.550 0 0.000 0.550 9.7 34048.0 34048.0 34046.7 0.0 272640.0 16777.0 1756416.0 587906.3 20096.0 19235.1 2944.0 2631.8 8 0.720 0 0.000 0.720 10.7 34048.0 34048.0 0.0 34046.2 272640.0 80171.6 1756416.0 664913.4 20352.0 19495.9 2944.0 2657.4 9 0.810 0 0.000 0.810 11.7 34048.0 34048.0 34048.0 0.0 272640.0 129480.8 1756416.0 745100.2 20608.0 19704.5 2944.0 2678.4 10 0.896 0 0.000 0.896 12.7 34048.0 34048.0 0.0 34046.6 272640.0 164070.7 1756416.0 822073.7 20992.0 19937.1 3072.0 2702.8 11 0.978 0 0.000 0.978 13.7 34048.0 34048.0 34048.0 0.0 272640.0 211949.9 1756416.0 897364.4 21248.0 20179.6 3072.0 2728.1 12 1.087 1 0.004 1.091 14.7 34048.0 34048.0 0.0 34047.1 272640.0 245801.5 1756416.0 597362.6 21504.0 20390.6 3072.0 2750.3 13 1.183 2 0.050 1.233 15.7 34048.0 34048.0 0.0 34048.0 272640.0 21474.1 1756416.0 757347.0 22012.0 20792.0 3200.0 2791.0 15 1.336 2 0.050 1.386 16.7 34048.0 34048.0 34047.0 0.0 272640.0 48378.0 1756416.0 838594.4 22268.0 21003.5 3200.0 2813.2 16 1.433 2 0.050 1.484

    這段輸出是從JVM啟動后第17秒開始截取的。從中可以看出,在經過了12次新生代GC后出現了兩次Full GC,共耗時50ms。通過GUI的工具也可以獲取到同樣的信息,比如說jsonsole或者是jvisualvm。

    在接受這一結論前,我們再來看下同樣是這次JVM啟動后所輸出的GC日志。很明顯-XX:+PrintGCDetails給我們講述的是一段截然不同卻更為詳盡的故事:

    java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer 3.157: [GC (Allocation Failure) 3.157: [ParNew: 272640K->34048K(306688K), 0.0844702 secs] 272640K->69574K(2063104K), 0.0845560 secs] [Times: user=0.23 sys=0.03, real=0.09 secs] 4.092: [GC (Allocation Failure) 4.092: [ParNew: 306688K->34048K(306688K), 0.1013723 secs] 342214K->136584K(2063104K), 0.1014307 secs] [Times: user=0.25 sys=0.05, real=0.10 secs] ... cut for brevity ... 11.292: [GC (Allocation Failure) 11.292: [ParNew: 306686K->34048K(306688K), 0.0857219 secs] 971599K->779148K(2063104K), 0.0857875 secs] [Times: user=0.26 sys=0.04, real=0.09 secs] 12.140: [GC (Allocation Failure) 12.140: [ParNew: 306688K->34046K(306688K), 0.0821774 secs] 1051788K->856120K(2063104K), 0.0822400 secs] [Times: user=0.25 sys=0.03, real=0.08 secs] 12.989: [GC (Allocation Failure) 12.989: [ParNew: 306686K->34048K(306688K), 0.1086667 secs] 1128760K->931412K(2063104K), 0.1087416 secs] [Times: user=0.24 sys=0.04, real=0.11 secs] 13.098: [GC (CMS Initial Mark) [1 CMS-initial-mark: 897364K(1756416K)] 936667K(2063104K), 0.0041705 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 13.102: [CMS-concurrent-mark-start] 13.341: [CMS-concurrent-mark: 0.238/0.238 secs] [Times: user=0.36 sys=0.01, real=0.24 secs] 13.341: [CMS-concurrent-preclean-start] 13.350: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 13.350: [CMS-concurrent-abortable-preclean-start] 13.878: [GC (Allocation Failure) 13.878: [ParNew: 306688K->34047K(306688K), 0.0960456 secs] 1204052K->1010638K(2063104K), 0.0961542 secs] [Times: user=0.29 sys=0.04, real=0.09 secs] 14.366: [CMS-concurrent-abortable-preclean: 0.917/1.016 secs] [Times: user=2.22 sys=0.07, real=1.01 secs] 14.366: [GC (CMS Final Remark) [YG occupancy: 182593 K (306688 K)]14.366: [Rescan (parallel) , 0.0291598 secs]14.395: [weak refs processing, 0.0000232 secs]14.395: [class unloading, 0.0117661 secs]14.407: [scrub symbol table, 0.0015323 secs] 14.409: [scrub string table, 0.0003221 secs][1 CMS-remark: 976591K(1756416K)] 1159184K(2063104K), 0.0462010 secs] [Times: user=0.14 sys=0.00, real=0.05 secs] 14.412: [CMS-concurrent-sweep-start] 14.633: [CMS-concurrent-sweep: 0.221/0.221 secs] [Times: user=0.37 sys=0.00, real=0.22 secs] 14.633: [CMS-concurrent-reset-start] 14.636: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

    從以上能夠看出,在運行了12次新生代GC后的確出現了一些“不太尋常”的事情。但并不是執行了兩次Full GC,這個“不尋常”的事情其實只是在年老代中執行了一次包含了數個階段的GC而已:

    • 初始標記階段,從0.0041705 秒或者說4ms的時候開始。這一階段是一次“stop-the-world”事件,所有的應用線程都被暫停以便進行初始標記。
    • 并發地執行標記和預清理(Preclean)的階段。這是和應用線程一起并發執行的。
    • 最終標記階段,從0.0462010秒或者說46毫秒的時候開始。這一階段也同樣是“stop-the-world”的。
    • 并發地進行清除操作。正如名字所說的,這一階段也無需中斷應用線程,可以并發地執行。

    因此我們從實際的GC日志中所看到的是這樣——其實沒有什么兩次所謂的Full GC,只有一次清理年老代空間的Major GC而已。

    如果你再看下jstat輸出的結果,就不難得出結論了。它確切地指出了兩次stop-the-world事件,總耗時50ms,這段時間內所有活躍線程都會出現延遲響應。不過如果你想據此來優化吞吐量的話,很可能會徒勞無功——jstat只列出了兩次stop-the-world的初始標記及最終標記的部分,而并發執行的那部分工作卻被它給隱藏掉了。


    總結

    以上是生活随笔為你收集整理的Java中的垃圾回收的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。