1. 程式人生 > >JVM和垃圾回收面試入門,這一篇就夠了

JVM和垃圾回收面試入門,這一篇就夠了

前些天在 google 上搜索了一些JVM的參考資料,偶然發現了一篇文章,如獲至寶,簡單易懂而且相對全面的JVM和垃圾回收介紹寫得非常的棒,因此一直儲存著,今天有時間特意翻譯了一下。本人水平有限,但是遇到好文章希望可以與更多的人分享,特此釋出此文。 英文OK的話,可以檢視 原文

前言

JVM是大多數開發人員的致命弱點,甚至可能導致老練的開發者一敗塗地。事實上,除非出現問題,否則我們正常情況下很少會關心它。也許在應用開始執行的時候我們對JVM進行些許的調整,但那之後就不會再去碰它,除非再有什麼問題出現。這讓它成為一個在面試中很難跨越的障礙。更糟糕的是,面試者非常喜歡問這方面的問題。每個開發者必須具有JVM的基本知識以完成他們的工作,但通用情況下,招聘方希望找尋那些懂得怎樣修復類似記憶體洩露這樣的問題的人。 在這篇文章裡,我們將從零開始學習JVM和垃圾回收的,讓你有信心繼續深入學習。

JVM

面試題:JVM是什麼?它有什麼好處?什麼是“一次編寫,到處執行”?它們有什麼缺點?

答:JVM是Java虛擬機器( Java Virtual Machine)的縮寫。Java原始碼被編譯成一種中間的語言,即位元組碼。然後虛擬機器處理執行這些位元組碼。而其他語言,如C++,會直接將原始碼編譯成指定平臺上可執行的原生代碼(native code)。 這就是為什麼Java擁有了“一次編寫,到處執行”的能力的原因。對那些直接為指定平臺編譯的語言,你必須分別為你要直接的不同的平臺編譯和測試你的應用程式。這很可能造成一些問題,例如你的應用程式依賴某些庫,你必須保證這些庫在不同的平臺上都是可用的。每新增加一個平臺,就意味著新的編譯和新的測試。非常消耗時間,也非常昂貴。 而Java程式可以在任何安裝了Java虛擬機器的系統上執行。JVM扮演的是一箇中間層的角色,它負責處理特定系統的細節,這意味著作為開發人員,我們不必關心這些細節。事實上,在不同的系統之間仍然存在一些不同,但是這些相對較小。這個機制讓開發變得快速且容易,而且意味著開發者可以在 Windows 機上為其他平臺編寫軟體。我的軟體只需要編寫一次就可以將其執行在從 Android 到 Solaris 之類的各種各樣的平臺上。 理論上,這會造成一些速度上的損耗。額外的JVM層意味著它將比直接編譯到特定平臺的語言更慢,比如C語言。然而 Java 在最近這些年已經有了長足的進步,而且提供而其他更多的好處,比如易用性,它已經被越來越多地應用於各種低延遲的應用程式中。 另一個JVM帶來的好處是,任何可以被編譯成位元組碼的語言都可以在它上面執行。不僅僅是Java,像 Groovy,Scala和Clojure都是基於JVM的語言。這也意味著這些語言能夠輕鬆地應用其他語言編寫的庫。作為一名 Scala 開發者,我可以在我的應用程式中使用 Java 庫,因為它們都執行在同一個平臺上。 與硬體的分離也意味著程式碼是沙箱式的,從而限制了對主機的損害程度。安全性是 JVM 帶來的一大好處。 還有一個有趣的方面值得考慮:並不是所有的 JVM 都一樣。除了Oracle 標準的 JVM 實現,還有有很多不同的其他實現。JRockit 是一款由於速度非常快而出名的 JVM。OpenJDK 是一款開源的與標準 JVM 等價的實現。還有非常多的其他各種實現可供使用。這些選擇最終是一個好事,各種不同的 JVM 的表現會有細微的差別。Java 規範在它們的實現方面故意留有很多含糊不清的地方,以讓不同的虛擬機器可以做不同的事情。這可能導致一個bug,這個bug只在特定虛擬機器和特定平臺上才會出現。這些可能是最難修復的 bug 了。 從開發者的角度上,JVM 圍繞著記憶體管理和效能優化提供了一系列的好處。

Java面試題:JIT是什麼?

JIT就是即時編譯(Just in Time)的意思。前面討論過,JVM 執行位元組碼。然而,如果它發現某段程式碼被頻繁地執行,那麼它將選擇性地把這段程式碼編譯為原生代碼以提高執行速度。可以被即時編譯的最小單位是方法。預設情況下,一段程式碼需要被執行 1500 次才會被即時編譯。不過這個值可以配置。因此才有了給 JVM “熱身”的概念。因為有這些優化發生,程式的執行時間越長,效能越好。然而 JIT 也有壞處,因為它不是免費的,當它發生時,會帶來時間和資源上的開銷。

Java 垃圾收集(GC)

Java面試題:我們說用 Java 管理記憶體,是什麼意思?垃圾收集器(Gabage Collector)是什麼?

在一些語言如C語言中,開發人員擁有對記憶體的直接訪問權。程式碼在字面上直接線上服務內容空間的地址。這是非常困難而且危險的,很容易導致記憶體洩漏。而在 Java 中,所有的記憶體都是被自動管理的。作為程式設計師我們只處理物件和原始型別,對記憶體和指標下面發生的事情沒有概念。最重要的是,Java 有垃圾收集器的概念。當物件不再被需要的時候 JVM 會為我們自動識別並清理記憶體空間。

Java面試題:垃圾回收器的優缺點是什麼?

優點:

  • 開發人員無須過多地關心記憶體管理,而是關注解決具體的業務。雖然記憶體洩漏在技術上仍然是可能出現的,但不常見。
  • GC 在管理記憶體上有很多智慧的演算法,它們自動在後臺執行。與流行的想法相反,這些通常比手動回收更能確定什麼時候是執行垃圾回收的最好時機。

缺點:

  • 當垃圾回收發生時將影響程式的效能,顯著地降低執行速度甚至將程式停止。所謂 “Stop the world” 就是當垃圾回收發生的時候應用程式的其他任務都將被凍結。對於應用程式的要求來說,這將是不可接收的,雖然 GC 調優可以最小化甚至消除這個影響。
  • 雖然GC有很多方面可以調優,但你不能指定應用程式在何時怎樣執行GC。

Java面試題:“Stop the World”是什麼?

當GC發生的時候,有必要完全地暫停應用程式的執行緒。這就是眾所周知的 Stop The World。對於大部分應用程式來說,長時間的暫停是不可接受的。所以調節垃圾收集器,最小化垃圾收集造成的影響到一個可以接受的範圍內至關重要。

Java面試題:代際GC(Generational Garbage Collection)是如何工作的?為什麼我們要使用代際GC?Java堆是如何構造的?

理解 Java 堆的執行機制對回答關於GC的問題非常重要。所有的物件都被儲存在堆中(與之相對應的是棧,變數和方法以及堆中物件的引用都儲存在那裡)。垃圾收集是一個從堆中移除不再被需要的物件並釋放記憶體空間的過程。幾乎所有的垃圾收集器都是“代際”的,它們把堆分為不同的部分,或不同的代。這已經被證明是明顯更優的,所以幾乎所有的收集器都使用這種模式。

新生代 (New Generation)

大多數應用程式都有大量的生命週期短暫的物件。在一次GC中分析程式的所有物件將非常慢而且耗時,因此將“短命”的物件分開處理以快速收集它們就非常有必要了。所以所有的新物件都將放在新生代中。新生代又進一步切分為:

  • 伊甸區(Eden Space):所有新的物件將放在這裡。當這個空間滿了之後,次要GC(minor GC)將發生。所有仍然被引用的物件將被提升到倖存區。
  • 倖存區(Survivor Space):倖存區的實現根據不同的 JVM 有所不同但是前提是相同的。每次新生代的 GC 都會增加倖存區中物件的年齡。當一個物件從Minor GC中倖存的次數足夠多時(預設值有所有同,一般是15次),它會被提升到老年代。一些實現使用兩個倖存區,一個 From 區和一個 To 區。每次回收過程中,這兩個區會相互交換角色,將所有被提升的 Eden 中的物件和倖存區的物件移到到 To 區,清空 From 區。

在新生代的GC叫做 次要GC(Minor GC)。 使用新生代的一個好處是減少碎片的影響。當一個物件被垃圾收集時,它會在它所在的位置留下一個記憶體中的間隔。我們可以整理其餘的物件(會發生 stop-the-world 現象)或者我們可以把間隔留著,下次有新物件時將新物件放在這個位置上。利用代際GC,我們限制了間隔的數量,這發生在老年代中,因為它通常更加穩定,這有利於減少 stop the world,以改善延遲。然而,如果我們不整理物件,我們會發現,新的物件也許因為大小的原因,不能放入到間隔當中。如果是這樣的話,你將看到物件無法從新生代中被提升。

老年代(Old Generation)

任何從新生代的倖存區中倖存下來的物件都會被提升到老年代。老年代通常比新生代要大得多。在老年代中發生的GC叫做 Full GC. Full GC 也會 stop-the-world 而且需要更長的時間,這也正是為什麼大多數的 JVM 調優要調整這裡的原因。在垃圾收集中有多種不同的演算法可以使用,而且可以對新生代和老年代使用不同的演算法。

序列GC(Serial GC)

為單核計算機設計的,當GC發生的時候將會停止整個程式。它使用標記-清除-整理(mark-sweep-compact)演算法。這意味著它將掃描所有物件,從而標記出所有可以被收集的物件,然後清理它們,最後把所有的物件都拷貝到一個連續的空間中(沒有碎片)。

並行GC(Parallel GC)

與序列GC類似,不同的是它使用了多執行緒執行GC,所以會比較快一些。

併發標記清除(Concurrent Mark Sweep, CMS)

CMS GC 通過與應用程式程序併發執行GC相關的大部分工作使暫停最小化。這把程式不得不完全暫停的時間最小化了,因此更適合對時間比較敏感的應用程式。CMS是一個不整理(non compacting)的演算法,這將導致碎片問題。CMS 收集器實際上為新生代使用並行GC(Parallel GC)。

G1GC(Gabage 1st Gabage Collector)

一個併發並行的收集器,它被視為未來CMS的替代者,它不會像CMS那樣被碎片問題所困擾。

永久代(PermGen)

永久代是 JVM 儲存類的元資料的地方。它在 Java8中已經被元資料區(metaspace)所代替。儘管當類沒有被正確地解除安裝的情況下可能出現洩漏,但是通常情況下永久代不需要任何調優來確保它有足夠的空間。

Java面試題:哪個(GC)更好?序列、並行還是CMS?

這要看整個應用程式的情況。每個收集器都是為特定的應用程式量身訂做的。如果你使用的是單個CPU,或者單臺機器上執行的虛擬機器超過CPU的個數的情況下,序列GC(Serial GC)會更好。如果你有大量的工作要做而且可以接收暫停現象,並行GC(Parallel GC)是個好選擇。如果你需要穩定的響應和最小的暫停,CMS是這三者中最好的選擇。

Java面試題:你能讓系統執行垃圾回收嗎?

這是一個有趣的問題。答案即是肯定的又是否定的。我們可以執行 System.gc() 建議 JVM 執行垃圾回收。然而,沒有保證這個建議會執行任何效果。作為Java開發者,我們不能確定地知道我們的程式碼將執行在哪個JVM上。JVM不保證這個方法執行後會發生什麼。甚至有一個啟動引數 -XX:+DisableExplicitGC 來禁止手動執行GC。 使用System.gc()是一個糟糕的實踐。

Java面試題:finallize() 方法會做什麼?

finallize() 是一個 java.lang.Object 裡的方法,所以所有的物件都有該方法。預設的實現沒有做任何事情。這個方法將在這個物件已經沒有被引用,垃圾收集決定回收它的時候呼叫。因此這裡的程式碼沒有保證會被執行,所以這個方法不能被用於執行實際的功能。相反,它被用來清理資源,例如檔案的引用。一個物件中,這個方法只會被JVM呼叫一次。

Java面試題:我可以使用哪些引數來對JVM和GC進行調優?

為GC進行JVM進行調優有非常多的書可以看。但是為面試準備瞭解幾個是很好的。

-XX:-UseConcMarkSweepGC: 對老年代使用CMS收集器 -XX:-UseParallelGC: 對新生代使用 Parallel GC -XX:-UseParallelOldGC: 在老年代和新生代都使用 Parallel GC -XX:-HeapDumpOnOutOfMemeryError: 當應用發生OOM時建立一個執行緒轉儲(dump)。對診斷非常有用。 -XX:-PrintGCDetails: 列印GC的詳細日誌 -Xms512m: 設定初始堆大小為 512m -Xmx1024m: 設定最大堆大小為 1024m -XX:NewSize 和 -XX:MaxNewSize: 指定新生代的預設和最大空間。 -XX:NewRatio=3: 設定新生代和老年代大小的比例為 1:3 -XX:SurvivorRatio=10: 設定 Eden space 和 Survivor space 的比例