天天看點

JVM記憶體管理、直接記憶體和垃圾回收

無論對于Java程式員還是大資料研發人員,JVM是必須掌握的技能之一。既是面試中經常問的問題,也是在實際業務中對程式進行調優、排查類似于記憶體溢出、棧溢出、記憶體洩漏等問題的關鍵。筆者将按下圖分多篇文章詳細闡述JVM:

JVM記憶體管理、直接記憶體和垃圾回收

本篇文章主要叙述JVM記憶體管理、直接記憶體、垃圾回收和常見的垃圾回收算法:

運作時資料區域

JVM在執行一些基于JVM運作的程式,典型的如Java程式、Scala程式時,會把它所管理的記憶體劃分為多個不同的資料區域。這些區域有各個的作用、建立和銷毀時間,有的區域生命周期依賴于使用者線程的啟動和結束,有些區域則随着虛拟機的啟動而存在,下圖展示了JVM在運作時的資料區域劃分:

JVM記憶體管理、直接記憶體和垃圾回收

1. 方法區

方法區是各個線程共享的記憶體區域,主要用于存放一些"自始至終都不會變化"的東西,比如final定義的常量、類的資訊(class執行個體)、靜态變量等、方法資訊。因為這些東西一旦被加載,是幾乎不會被GC的,是以方法區又被稱為永久代(注意一點,二者本質并不等價)。

方法區有一部分叫常量池,用于存儲編譯期生成的一些字面變量、符号引用以及一些運作時産生的常量(如String常量池)。方法區中的靜态區用于存放類變量、靜态塊等。

方法區又稱非堆,是有大小限制的,如果方法區使用記憶體超過了配置設定的大小,就會報類似OutOfMemory: PermGen Space的錯誤。

2. Java虛拟機棧

Java 虛拟機棧是線程私有的,它的生命周期與線程相同,為虛拟機執行Java方法即位元組碼服務,是描述Java方法執行時的記憶體模型。

每個方法執行時都會建立一個棧幀用于存儲局部變量表(比如編譯期可知的基本資料類型、對象引用等)、操作棧、動态連結、方法出口等資訊。每一個方法被調用至執行完成的過程,對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。

如果線程請求的棧深度大于虛拟機所允許的深度,将會報StackOverFlowError;如果虛拟機棧無法申請到足夠的記憶體時會報OutOfMemoryError。

調整虛拟機棧大小的方式:-Xss。

3. 本地方法棧

本地方法棧為使用的到Native方法服務,本地方法接口都會使用某種本地方法棧。

當線程調用Java方法時,虛拟機會建立一個新的棧幀并壓入Java棧。然而,當它調用的是本地方法時,虛拟機會保持Java棧不變,不會線上程的Java棧中壓入新的棧幀,而是動态連接配接并直接調用指定的本地方法。

4. 堆

堆是JVM管理記憶體中最大的一塊區域,由Java線程共享,主要用來存儲new出來的對象和數組,并且這塊區域随着虛拟機的啟動而建立。堆可以處于邏輯上連續但實體上不連續的記憶體空間中。

堆是垃圾回收器管理的主要區域,可以細分為新生代和老年代,新生代又劃分為eden區,from survivor區、to survivor區。

對象在被建立時,首先在新生代進行配置設定,eden區存放新生成的對象,兩個survivor區用來存放新生代中每次垃圾回收後依然存活下來的對象。但是當建立新建立的對象非常大,該對象會直接進入老年代。

JVM記憶體管理、直接記憶體和垃圾回收

5. 程式計數器

程式計數器是線程私有的即每個線程都會有自己的程式計數器,用來記錄線程執行的位元組碼位置,是一個沒有OOM的區域。

直接記憶體

直接記憶體(direct memory)不屬于JVM運作時資料區的一部分,屬于堆外記憶體,會被頻繁使用,是以在設定各個記憶體範圍時要留出一部分實體記憶體,否則也容易抛出OutOfMemoryError。

垃圾收集

垃圾收集即GC,是JVM進行記憶體回收的處理過程。

開發人員更多的是關注業務需求的實作,而記憶體管理是交由JVM完成的,如果不進行或者錯誤的進行垃圾回收會導緻程式不穩定甚至崩潰。Java提供的GC功能可以自動監測對象是否超過作用域等進而達到自動回收記憶體的目的,可以有效防止記憶體洩露,有效的使用可用記憶體。

GC主要分為3種:minor GC、major GC和full GC。

minor GC是發生在新生代的,minor GC是發生在老年代的。對于full GC出發的原因則比較多,比如老年代空間不足,它會出發stop world,處理不好往往會影響整個程式的穩定性嚴重會導緻系統不可用,需要特别注意。

常見的垃圾回收算法

1. 标記清除算法

首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。

存在如下兩個缺點:

  1. 效率低

    需要先對要回收的對象進行标記,然後再統一清除,然而标記和清除兩個過程效率都很低下。

  2. 記憶體碎片問題

    标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作,影響性能。

2. 複制算法

先将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當使用的這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

優點:這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。

缺點:不适合對象存活率較高的場景,因為這種場景要進行較多的複制操作影響效率;實際可用記憶體變為配置設定記憶體的一半,因為每次隻使用其中的一半記憶體。

3. 标記整理算法

先标記(标記過程與标記清除算法一樣),讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。這樣可以解決記憶體碎片問題。

4. 分代收集算法

就是針對Java堆記憶體中新生代、老年代等采用不同的垃圾回收算法。如在新生代中,往往隻有少量對象存活(最後會進入老年代),則适合用複制算法。而老年代中對象存活率較高,沒有額外的空間對它進行配置設定擔保,就使用标記清除算法。

當然實際應用中,使用什麼算法,要看使用的垃圾回收器。

推薦文章:

Java并發隊列與容器

必須掌握的HDFS知識點

Spark叢集和任務執行