JVM相關知識整理

JVM內存模型

在這裏插入圖片描述
由上圖可以清楚的看到JVM的內存空間分爲3大部分:

  • 堆內存
  • 方法區
  • 棧內存

其中棧內存可以再細分爲java虛擬機棧和本地方法棧,堆內存可以劃分爲新生代和老年代,新生代中還可以再次劃分爲Eden區、From Survivor區和To Survivor區。

其中一部分是線程共享的,包括 Java 堆和方法區;另一部分是線程私有的,包括虛擬機棧和本地方法棧,以及程序計數器這一小部分內存。

堆內存(Heap)

java 堆(Java Heap)是Java 虛擬機所管理的內存中最大的一塊。堆是被所有線程共享的區域,實在虛擬機啓動時創建的。堆裏面存放的都是對象的實例(new 出來的對象都存在堆中)。

此內存區域的唯一目的就是存放對象實例(new的對象),幾乎所有的對象實例都在這裏分配內存。

堆內存分爲兩個部分:年輕代和老年代。我們平常所說的垃圾回收,主要回收的就是堆區。更細一點劃分新生代又可劃分爲Eden區和2個Survivor區(From Survivor和To Survivor)。

下圖中的Perm代表的是永久代,但是注意永久代並不屬於堆內存中的一部分,同時jdk1.8之後永久代已經被移除。
在這裏插入圖片描述
新生代 ( Young ) 與老年代 ( Old ) 的比例的值爲 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 )

默認的,Eden : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

方法區(Method Area)

方法區也稱”永久代“,它用於存儲虛擬機加載的類信息、常量、靜態變量、是各個線程共享的內存區域

在JDK8之前的HotSpot JVM,存放這些”永久的”的區域叫做“永久代(permanent generation)”。永久代是一片連續的堆空間,在JVM啓動之前通過在命令行設置參數-XX:MaxPermSize來設定永久代最大可分配的內存空間,默認大小是64M(64位JVM默認是85M)。

隨着JDK8的到來JVM不再有 永久代(PermGen)。但類的元數據信息(metadata)還在,只不過不再是存儲在連續的堆空間上,而是移動到叫做“Metaspace”的本地內存(Native memory。

方法區或永生代相關設置

  • -XX:PermSize=64MB 最小尺寸,初始分配
  • -XX:MaxPermSize=256MB 最大允許分配尺寸,按需分配
  • XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled
  • 設置垃圾不回收
  • 默認大小
  • -server選項下默認MaxPermSize爲64m
  • -client選項下默認MaxPermSize爲32m

虛擬機棧(JVM Stack)

java虛擬機棧是線程私有,生命週期與線程相同。創建線程的時候就會創建一個java虛擬機棧。

虛擬機執行java程序的時候,每個方法都會創建一個棧幀,棧幀存放在java虛擬機棧中,通過壓棧出棧的方式進行方法調用。

棧幀又分爲一下幾個區域:局部變量表、操作數棧、動態連接、方法出口等。
平時我們所說的變量存在棧中,這句話說的不太嚴謹,應該說局部變量存放在java虛擬機棧的局部變量表中。
java的8中基本類型的局部變量的值存放在虛擬機棧的局部變量表中,如果是引用型的變量,則只存儲對象的引用地址。

本地方法棧(Native Stack)

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務

程序計數器(PC Register)

程序計數器就是記錄當前線程執行程序的位置,改變計數器的值來確定執行的下一條指令,比如循環、分支、方法跳轉、異常處理,線程恢復都是依賴程序計數器來完成。
Java虛擬機多線程是通過線程輪流切換並分配處理器執行時間的方式實現的。爲了線程切換能恢復到正確的位置,每條線程都需要一個獨立的程序計數器,所以它是線程私有的。

直接內存

直接內存並不是虛擬機內存的一部分,也不是Java虛擬機規範中定義的內存區域。jdk1.4中新加入的NIO,引入了通道與緩衝區的IO方式,它可以調用Native方法直接分配堆外內存,這個堆外內存就是本機內存,不會影響到堆內存的大小。

JVM內存參數設置

在這裏插入圖片描述

  • -Xms設置堆的最小空間大小。
  • -Xmx設置堆的最大空間大小。
  • -Xmn:設置年輕代大小。
  • -XX:NewSize設置新生代最小空間大小。
  • -XX:MaxNewSize設置新生代最大空間大小。
  • -XX:PermSize設置永久代最小空間大小。
  • -XX:MaxPermSize設置永久代最大空間大小。
  • -Xss設置每個線程的堆棧大小。
  • -XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用串行收集。
  • -XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。

典型JVM參數配置參考

  • java-Xmx3550m-Xms3550m-Xmn2g-Xss128k
  • -XX:ParallelGCThreads=20
  • -XX:+UseConcMarkSweepGC-XX:+UseParNewGC

-Xmx3550m:設置JVM最大可用內存爲3550M。

-Xms3550m:設置JVM促使內存爲3550m。此值可以設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存。

-Xmn2g:設置年輕代大小爲2G。整個堆大小=年輕代大小+年老代大小+持久代大小。持久代一般固定大小爲64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,官方推薦配置爲整個堆的3/8。

-Xss128k:設置每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小爲1M,以前每個線程堆棧大小爲256K。更具應用的線程所需內存大
小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000
左右。

垃圾回收機制

年輕代分爲Eden區和survivor區(兩塊兒:from和to),且Eden:from:to==8:1:1。
在這裏插入圖片描述
jvm內存結構

  1. 新產生的對象優先分配在Eden區(除非配置了-XX:PretenureSizeThreshold,大於該值的對象會直接進入年老代);

  2. 當Eden區滿了或放不下了,這時候其中存活的對象會複製到from區。

這裏,需要注意的是,如果存活下來的對象from區都放不下,則這些存活下來的對象全部進入年老代。之後Eden區的內存全部回收掉。

  1. 之後產生的對象繼續分配在Eden區,當Eden區又滿了或放不下了,這時候將會把Eden區和from區存活下來的對象複製到to區(同理,如果存活下來的對象to區都放不下,則這些存活下來的對象全部進入年老代),之後回收掉Eden區和from區的所有內存。

  2. 如上這樣,會有很多對象會被複制很多次(每複製一次,對象的年齡就+1),默認情況下,當對象被複制了15次(這個次數可以通過:-XX:MaxTenuringThreshold來配置),就會進入年老代了。

  3. 當年老代滿了或者存放不下將要進入年老代的存活對象的時候,就會發生一次Full GC(這個是我們最需要減少的,因爲耗時很嚴重)。

垃圾回收有兩種類型:Minor GC 和 Full GC。

1.Minor GC
對新生代進行回收,不會影響到年老代。因爲新生代的 Java 對象大多死亡頻繁,所以 Minor GC 非常頻繁,一般在這裏使用速度快、效率高的算法,使垃圾回收能儘快完成。

2.Full GC
也叫
Major GC,對整個堆進行回收,包括新生代和老年代。由於Full GC需要對整個堆進行回收,所以比Minor
GC要慢,因此應該儘可能減少Full GC的次數,導致Full
GC的原因包括:老年代被寫滿、永久代(Perm)被寫滿和System.gc()被顯式調用等。

垃圾回收算法

1.標記清除

標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段

在標記階段首先通過根節點(GC Roots),標記所有從根節點開始的對象,未被標記的對象就是未被引用的垃圾對象。然後,在清除階段,清除所有未被標記的對象。
在這裏插入圖片描述
適用場合:

  • 存活對象較多的情況下比較高效
  • 適用於年老代(即舊生代)

缺點:

  • 容易產生內存碎片,再來一個比較大的對象時(典型情況:該對象的大小大於空閒表中的每一塊兒大小但是小於其中兩塊兒的和),會提前觸發垃圾回收
  • 掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)

2.複製算法

從根集合節點進行掃描,標記出所有的存活對象,並將這些存活的對象複製到一塊兒新的內存(圖中下邊的那一塊兒內存)上去,之後將原來的那一塊兒內存(圖中上邊的那一塊兒內存)全部回收掉
在這裏插入圖片描述
現在的商業虛擬機都採用這種收集算法來回收新生代。

適用場合:

  • 存活對象較少的情況下比較高效
  • 掃描了整個空間一次(標記存活對象並複製移動)
  • 適用於年輕代(即新生代):基本上98%的對象是”朝生夕死”的,存活下來的會很少

缺點:

  • 需要一塊兒空的內存空間
  • 需要複製移動對象

3.標記整理

複製算法的高效性是建立在存活對象少、垃圾對象多的前提下的。

這種情況在新生代經常發生,但是在老年代更常見的情況是大部分對象都是存活對象。如果依然使用複製算法,由於存活的對象較多,複製的成本也將很高。
在這裏插入圖片描述
標記-壓縮算法是一種老年代的回收算法,它在標記-清除算法的基礎上做了一些優化。

首先也需要從根節點開始對所有可達對象做一次標記,但之後,它並不簡單地清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之後,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比比較高。

4.分代收集算法

分代收集算法就是目前虛擬機使用的回收算法,它解決了標記整理不適用於老年代的問題,將內存分爲各個年代。一般情況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。

在不同年代使用不同的算法,從而使用最合適的算法,新生代存活率低,可以使用複製算法。而老年代對象存活率搞,沒有額外空間對它進行分配擔保,所以只能使用標記清除或者標記整理算法。
在這裏插入圖片描述

垃圾回收算法總結

1.年輕代:複製算法

  1. 所有新生成的對象首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的對象。

  2. 新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個
    Survivor區(一般而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區爲空,
    如此往復。

  3. 當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC(Major GC),也就是新生代、老年代都進行回收。

  4. 新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)。

2.年老代:標記-清除或標記-整理

  1. 在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。

  2. 內存比新生代也大很多(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。

以上這種年輕代與年老代分別採用不同回收算法的方式稱爲”分代收集算法”,這也是當下企業使用的一種方式

每一種算法都會有很多不同的垃圾回收器去實現,在實際使用中,根據自己的業務特點做出選擇就好。

JVM內存調優

對JVM內存的系統級的調優主要的目的是減少GC的頻率和Full GC的次數。

1.Full GC

會對整個堆進行整理,包括Young、Tenured和Perm。Full GC因爲需要對整個堆進行回收,所以比較慢,因此應該儘可能減少Full GC的次數。

2.導致Full GC的原因

1)年老代(Tenured)被寫滿

調優時儘量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象 。

2)持久代Pemanet Generation空間不足

增大Perm Gen空間,避免太多靜態對象 , 控制好新生代和舊生代的比例

3)System.gc()被顯示調用

垃圾回收不要手動觸發,儘量依靠JVM自身的機制

在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節,下面詳細介紹對應JVM調優的方法和步驟。

JVM性能調優方法和步驟

  1. 監控GC的狀態
    使用各種JVM工具,查看當前日誌,分析當前JVM參數設置,並且分析當前堆內存快照和gc日誌,根據實際的各區域內存劃分和GC執行時間,覺得是否進行優化。

舉一個例子: 系統崩潰前的一些現象:

  • 每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延長到4、5s
  • FullGC的次數越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
  • 年老代的內存越來越大並且每次FullGC後年老代沒有內存被釋放
    之後系統會無法響應新的請求,逐漸到達OutOfMemoryError的臨界值,這個時候就需要分析JVM內存快照dump。
  1. 生成堆的dump文件
    通過JMX的MBean生成當前的Heap信息,大小爲一個3G(整個堆的大小)的hprof文件,如果沒有啓動JMX可以通過Java的jmap命令來生成該文件。

  2. 分析dump文件
    打開這個3G的堆信息文件,顯然一般的Window系統沒有這麼大的內存,必須藉助高配置的Linux,幾種工具打開該文件:

  • Visual VM
  • IBM HeapAnalyzer
  • JDK 自帶的Hprof工具
  • Mat(Eclipse專門的靜態內存分析工具)推薦使用

備註:文件太大,建議使用Eclipse專門的靜態內存分析工具Mat打開分析。

  1. 分析結果,判斷是否需要優化
    如果各項參數設置合理,系統沒有超時日誌出現,GC頻率不高,GC耗時不高,那麼沒有必要進行GC優化,如果GC時間超過1-3秒,或者頻繁GC,則必須優化。
    注:如果滿足下面的指標,則一般不需要進行GC:
  • Minor GC執行時間不到50ms;
  • Minor GC執行不頻繁,約10秒一次;
  • Full GC執行時間不到1s;
  • Full GC執行頻率不算頻繁,不低於10分鐘1次;
  1. 調整GC類型和內存分配
    如果內存分配過大或過小,或者採用的GC收集器比較慢,則應該優先調整這些參數,並且先找1臺或幾臺機器進行beta,然後比較優化過的機器和沒有優化的機器的性能對比,並有針對性的做出最後選擇。

  2. 不斷的分析和調整
    通過不斷的試驗和試錯,分析並找到最合適的參數,如果找到了最合適的參數,則將這些參數應用到所有服務器。
    在這裏插入圖片描述
    cms參數優化步流程

下面我再繼續介紹下JVM的關鍵參數配置(僅用於參考)。

JVM調優參數參考

  1. 針對JVM堆的設置,一般可以通過-Xms -Xmx限定其最小、最大值,爲了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,通常把最大、最小設置爲相同的值;

  2. 年輕代和年老代將根據默認的比例(1:2)分配堆內存, 可以通過調整二者之間的比率NewRadio來調整二者之間的大小,也可以針對回收代。

  • 比如年輕代,通過 -XX:newSize -XX:MaxNewSize來設置其絕對大小。同樣,爲了防止年輕代的堆收縮,我們通常會把-XX:newSize -XX:MaxNewSize設置爲同樣大小。
  1. 年輕代和年老代設置多大才算合理
  • 更大的年輕代必然導致更小的年老代,大的年輕代會延長普通GC的週期,但會增加每次GC的時間;小的年老代會導致更頻繁的Full GC
  • 更小的年輕代必然導致更大年老代,小的年輕代會導致普通GC很頻繁,但每次的GC時間會更短;大的年老代會減少Full GC的頻率

如何選擇應該依賴應用程序對象生命週期的分佈情況: 如果應用存在大量的臨時對象,應該選擇更大的年輕代;如果存在相對較多的持久對象,年老代應該適當增大。但很多應用都沒有這樣明顯的特性。

在抉擇時應該根 據以下兩點:

(1)本着Full GC儘量少的原則,讓年老代儘量緩存常用對象,JVM的默認比例1:2也是這個道理 。

(2)通過觀察應用一段時間,看其他在峯值時年老代會佔多少內存,在不影響Full GC的前提下,根據實際情況加大年輕代,比如可以把比例控制在1:1。但應該給年老代至少預留1/3的增長空間。

  1. 在配置較好的機器上(比如多核、大內存),可以爲年老代選擇並行收集算法: -XX:+UseParallelOldGC

  2. 線程堆棧的設置:每個線程默認會開啓1M的堆棧,用於存放棧幀、調用參數、局部變量等,對大多數應用而言這個默認值太了,一般256K就足用。

理論上,在內存不變的情況下,減少每個線程的堆棧,可以產生更多的線程,但這實際上還受限於操作系統。

7種JVM垃圾收集器特點,優劣勢、及使用場景

常見的垃圾收集器有3類:

  1. 新生代的收集器包括:
    Serial
    PraNew
    Parallel Scavenge
  2. 老年代的收集器包括:
    Serial Old
    Parallel Old
    CMS
  3. 回收整個Java堆(新生代和老年代)
    G1收集器

今天我們詳細談談以上7種垃圾收集器的優劣勢和使用場景。

新生代垃圾收集器

1.Serial串行收集器-複製算法
Serial收集器是新生代單線程收集器,優點是簡單高效,算是最基本、發展歷史最悠久的收集器。它在進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集完成。
在這裏插入圖片描述
Serial收集器依然是虛擬機運行在Client模式下默認新生代收集器,對於運行在Client模式下的虛擬機來說是一個很好的選擇。

2.ParNew收集器-複製算法

ParNew收集器是新生代並行收集器,其實就是Serial收集器的多線程版本。
在這裏插入圖片描述
除了使用多線程進行垃圾收集之外,其餘行爲包括Serial收集器可用的所有控制參數、收集算法、Stop The Worl、對象分配規則、回收策略等都與Serial 收集器完全一樣。

3.Parallel Scavenge(並行回收)收集器-複製算法

Parallel Scavenge收集器是新生代並行收集器,追求高吞吐量,高效利用 CPU。

該收集器的目標是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即 吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可用高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。

老年代垃圾收集器

1.Serial Old 收集器-標記整理算法

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程(串行)收集器,使用標記整理算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用

如果在Server模式下,主要兩大用途:

(1)在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用

(2)作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用

2.Parallel Old 收集器-標記整理算法

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器在1.6中才開始提供。

3.CMS收集器-標記整理算法

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。

目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務器的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

CMS收集器是基於“標記-清除”算法實現的,它的運作過程相對前面幾種收集器來說更復雜一些,整個過程分爲4個步驟:

(1)初始標記

(2)併發標記

(3)重新標記

(4)併發清除

其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”
在這裏插入圖片描述
CMS收集器主要優點:

  • 併發收集
  • 低停頓
    CMS三個明顯的缺點:
  1. CMS收集器對CPU資源非常敏感。CPU個數少於4個時,CMS對於用戶程序的影響就可能變得很大,爲了應付這種情況,虛擬機提供了一種稱爲“增量式併發收集器”的CMS收集器變種。
  2. CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。在JDK1.5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活。
  3. CMS是基於“標記-清除”算法實現的收集器,手機結束時會有大量空間碎片產生。空間碎片過多,可能會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前出發FullGC。

新生代和老年代垃圾收集器

1.G1收集器-標記整理算法

JDK1.7後全新的回收器, 用於取代CMS收集器。

G1收集器的優勢:

  • 獨特的分代垃圾回收器,分代GC: 分代收集器, 同時兼顧年輕代和老年代
  • 使用分區算法, 不要求eden, 年輕代或老年代的空間都連續
  • 並行性: 回收期間, 可由多個線程同時工作, 有效利用多核cpu資源
  • 空間整理: 回收過程中, 會進行適當對象移動, 減少空間碎片
  • 可預見性: G1可選取部分區域進行回收, 可以縮小回收範圍, 減少全局停頓
    G1收集器的運作大致可劃分爲一下步驟:
    在這裏插入圖片描述
    G1收集器的階段分以下幾個步驟:

1、初始標記(它標記了從GC Root開始直接可達的對象)

2、併發標記(從GC Roots開始對堆中對象進行可達性分析,找出存活對象)

3、最終標記(標記那些在併發標記階段發生變化的對象,將被回收)

4、篩選回收(首先對各個Regin的回收價值和成本進行排序,根據用戶所期待的GC停頓時間指定回收計劃,回收一部分Region)

JVM垃圾收集器總結

本文主要介紹了JVM中的垃圾回收器,主要包括串行回收器、並行回收器以及CMS回收器、G1回收器。他們各自都有優缺點,通常來說你需要根據你的業務,進行基於垃圾回收器的性能測試,然後再做選擇。下面給出配置回收器時,經常使用的參數:

-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:+UseParNewGC:在新生代使用並行收集器
-XX:+UseParallelGC :新生代使用並行回收收集器,更加關注吞吐量
-XX:+UseParallelOldGC:老年代使用並行回收收集器
-XX:ParallelGCThreads:設置用於垃圾回收的線程數
-XX:+UseConcMarkSweepGC:新生代使用並行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:設定CMS的線程數量
-XX:+UseG1GC:啓用G1垃圾回收器

JVM面試題目和答案

首先我們來了解一下JVM的內存模型的怎麼樣的:
在這裏插入圖片描述
1.堆:存放對象實例,幾乎所有的對象實例都在這裏分配內存

  • 堆得內存由-Xms指定,默認是物理內存的1/64;最大的內存由-Xmx指定,默認是物理內存的1/4。
  • 默認空餘的堆內存小於40%時,就會增大,直到-Xmx設置的內存。具體的比例可以由-XX:MinHeapFreeRatio指定
  • 空餘的內存大於70%時,就會減少內存,直到-Xms設置的大小。具體由-XX:MaxHeapFreeRatio指定。

2.虛擬機棧

虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息本地方法棧:本地方法棧則是爲虛擬機使用到的Native方法服務。

3.方法區:存儲已被虛擬機加載的類元數據信息(元空間)

1)有時候也成爲永久代,在該區內很少發生垃圾回收,但是並不代表不發生GC,在這裏進行的GC主要是對方法區裏的常量池和對類型的卸載
2)方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯後的代碼等數據。
該區域是被線程共享的。
3)方法區裏有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池具有動態性,也就是說常量並不一定是編譯時確定,運行時生成的常量也會存在這個常量池中。

4.程序計數器:當前線程所執行的字節碼的行號指示器

總結:
在這裏插入圖片描述

JVM垃圾回收算法

1.標記-清除: 這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的對象,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以後程序在分配較大的對象時,由於沒有充足的連續內存而提前觸發一次GC動作。

2.複製算法: 爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,然後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,然後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一般的內存。 於是將該算法進行了改進,內存區域不再是按照1:1去劃分,而是將內存劃分爲8:1:1三部分,較大那份內存交Eden區,其餘是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象複製到第二塊內存區上,然後清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制複製到老年代中。(java堆又分爲新生代和老年代)

3. 標記-整理 該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生內存碎片了。

4.分代收集 現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。

JVM垃圾收集器有哪些?以及優劣勢比較?

1.串行收集器

串行收集器是最簡單的,它設計爲在單核的環境下工作(32位或者windows),你幾乎不會使用到它。它在工作的時候會暫停整個應用的運行,因此在所有服務器環境下都不可能被使用。

使用方法:-XX:+UseSerialGC

2.並行收集器

這是JVM默認的收集器,跟它名字顯示的一樣,它最大的優點是使用多個線程來掃描和壓縮堆。缺點是在minor和full GC的時候都會暫停應用的運行。並行收集器最適合用在可以容忍程序停滯的環境使用,它佔用較低的CPU因而能提高應用的吞吐(throughput)。

使用方法:-XX:+UseParallelGC

3.CMS收集器

CMS是Concurrent-Mark-Sweep的縮寫,併發的標記與清除。

這個算法使用多個線程併發地(concurrent)掃描堆,標記不使用的對象,然後清除它們回收內存。在兩種情況下會使應用暫停(Stop the World, STW):

1)當初次開始標記根對象時initial mark。

2)當在並行收集時應用又改變了堆的狀態時,需要它從頭再確認一次標記了正確的對象final remark。

這個收集器最大的問題是在年輕代與老年代收集時會出現的一種競爭情況(race condition),稱爲提升失敗promotion failure。對象從年輕代複製到老年代稱爲提升promotion,但有時侯老年代需要清理出足夠空間來放這些對象,這需要一定的時間,它收集的速度可能趕不上不斷產生的要提升的年輕代對象的速度,這時就需要做STW的收集。STW正是CMS想避免的問題。爲了避免這個問題,需要增加老年代的空間大小或者增加更多的線程來做老年代的收集以趕上從年輕代複製對象的速度。

除了上文所說的內容之外,CMS最大的問題就是內存空間碎片化的問題。CMS只有在觸發FullGC的情況下才會對堆空間進行compact。如果線上應用長時間運行,碎片化會非常嚴重,會很容易造成promotion failed。爲了解決這個問題線上很多應用通過定期重啓或者手工觸發FullGC來觸發碎片整理。

對比並行收集器它的一個壞處是需要佔用比較多的CPU。對於大多數長期運行的服務器應用來說,這通常是值得的,因爲它不會導致應用長時間的停滯。但是它不是JVM的默認的收集器。

4.G1收集器

如果你的堆內存大於4G的話,那麼G1會是要考慮使用的收集器。它是爲了更好支持大於4G堆內存在JDK 7 u4引入的。G1收集器把堆分成多個區域,大小從1MB到32MB,並使用多個後臺線程來掃描這些區域,優先會掃描最多垃圾的區域,這就是它名稱的由來,垃圾優先Garbage First。

如果在後臺線程完成掃描之前堆空間耗光的話,纔會進行STW收集。它另外一個優點是它在處理的同時會整理壓縮堆空間,相比CMS只會在完全STW收集的時候纔會這麼做。

使用過大的堆內存在過去幾年是存在爭議的,很多開發者從單個JVM分解成使用多個JVM的微服務(micro-service)和基於組件的架構。其他一些因素像分離程序組件、簡化部署和避免重新加載類到內存的考慮也促進了這樣的分離。

除了這些因素,最大的因素當然是避免在STW收集時JVM用戶線程停滯時間過長,如果你使用了很大的堆內存的話就可能出現這種情況。另外,像Docker那樣的容器技術讓你可以在一臺物理機器上輕鬆部署多個應用也加速了這種趨勢。

使用方法:-XX:+UseG1GC

轉載原文章:
http://youzhixueyuan.com/jvm-classic-interview-questions-and-answers.html
http://youzhixueyuan.com/jvm-garbage-collector.html
http://youzhixueyuan.com/jvm-performance-optimization.html
http://youzhixueyuan.com/jvm-memory-model-and-parameter-configuration.html
http://youzhixueyuan.com/jvm-garbage-collection-algorithm.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章