阿里終面:每天100w次登陸請求, 8G 記憶體該如何設定JVM引數?

碼猿技術專欄發表於2023-03-03

大家好,我是不才陳某~

上週知識星球的同學在阿里雲技術面終面的時候被問到這麼一個問題:假設一個每天100w次登陸請求的平臺,一個服務節點 8G 記憶體,該如何設定JVM引數? 覺得回答的不太理想,過來找我覆盤。

下面以面試題的形式給大家梳理出來,做到一箭雙鵰:

  • 既供大家實操參考
  • 又供大家面試參考

大家要學習的,除了 JVM 配置方案 之外,是其 分析問題的思路、思考問題的視角。 這些思路和視角,能幫助大家走更遠、更遠。

接下來,進入正題。

關注公眾號:碼猿技術專欄,回覆關鍵詞:1111 獲取阿里內部Java效能調優手冊!

每天100w次登陸請求, 8G 記憶體該如何設定JVM引數?

每天100w次登陸請求, 8G 記憶體該如何設定JVM引數,大概可以分為以下8個步驟

Step1:新系統上線如何規劃容量?

1.套路總結

任何新的業務系統在上線以前都需要去估算伺服器配置和JVM的記憶體引數,這個容量與資源規劃並不僅僅是系統架構師的隨意估算的,需要根據系統所在業務場景去估算,推斷出來一個系統執行模型,評估JVM效能和GC頻率等等指標。以下是我結合大牛經驗以及自身實踐來總結出來的一個建模步驟:

  • 計算業務系統每秒鐘建立的物件會佔用多大的記憶體空間,然後計算叢集下的每個系統每秒的記憶體佔用空間(物件建立速度)
  • 設定一個機器配置,估算新生代的空間,比較不同新生代大小之下,多久觸發一次MinorGC。
  • 為了避免頻繁GC,就可以重新估算需要多少機器配置,部署多少臺機器,給JVM多大記憶體空間,新生代多大空間。
  • 根據這套配置,基本可以推算出整個系統的執行模型,每秒建立多少物件,1s以後成為垃圾,系統執行多久新生代會觸發一次GC,頻率多高。

2.套路實戰——以登入系統為例

有些同學看到這些步驟還是發憷,說的好像是那麼回事,一到實際專案中到底怎麼做我還是不知道!

光說不練假把式,以登入系統為例模擬一下推演過程:

  • 假設每天100w次登陸請求,登陸峰值在早上,預估峰值時期每秒100次登陸請求。
  • 假設部署3臺伺服器,每臺機器每秒處理30次登陸請求,假設一個登陸請求需要處理1秒鐘,JVM新生代裡每秒就要生成30個登陸物件,1s之後請求完畢這些物件成為了垃圾。
  • 一個登陸請求物件假設20個欄位,一個物件估算500位元組,30個登陸佔用大約15kb,考慮到RPC和DB操作,網路通訊、寫庫、寫快取一頓操作下來,可以擴大到20-50倍,大約1s產生幾百k-1M資料。
  • 假設2C4G機器部署,分配2G堆記憶體,新生代則只有幾百M,按照1s1M的垃圾產生速度,幾百秒就會觸發一次MinorGC了。
  • 假設4C8G機器部署,分配4G堆記憶體,新生代分配2G,如此需要幾個小時才會觸發一次MinorGC。

所以,可以粗略的推斷出來一個每天100w次請求的登入系統,按照4C8G的3例項叢集配置,分配4G堆記憶體、2G新生代的JVM,可以保障系統的一個正常負載。

基本上把一個新系統的資源評估了出來,所以搭建新系統要每個例項需要多少容量多少配置,叢集配置多少個例項等等這些,並不是拍拍腦袋和胸脯就可以決定的下來的。

Step2:該如何進行垃圾回收器的選擇?

吞吐量還是響應時間

首先引入兩個概念:吞吐量和低延遲

吞吐量 = CPU在使用者應用程式執行的時間 / (CPU在使用者應用程式執行的時間 + CPU垃圾回收的時間)

響應時間 = 平均每次的GC的耗時

通常,吞吐優先還是響應優先這個在JVM中是一個兩難之選。

堆記憶體增大,gc一次能處理的數量變大,吞吐量大;但是gc一次的時間會變長,導致後面排隊的執行緒等待時間變長;相反,如果堆記憶體小,gc一次時間短,排隊等待的執行緒等待時間變短,延遲減少,但一次請求的數量變小(並不絕對符合)。

無法同時兼顧,是吞吐優先還是響應優先,這是一個需要權衡的問題。

垃圾回收器設計上的考量

  • JVM在GC時不允許一邊垃圾回收,一邊還建立新物件(就像不能一邊打掃衛生,還在一邊扔垃圾)。
  • JVM需要一段Stop the world的暫停時間,而STW會造成系統短暫停頓不能處理任何請求;
  • 新生代收集頻率高,效能優先,常用複製演算法;老年代頻次低,空間敏感,避免複製方式。
  • 所有垃圾回收器的涉及目標都是要讓GC頻率更少,時間更短,減少GC對系統影響!

CMS和G1

目前主流的垃圾回收器配置是新生代採用ParNew,老年代採用CMS組合的方式,或者是完全採用G1回收器,

從未來的趨勢來看,G1是官方維護和更為推崇的垃圾回收器。

業務系統:

  • 延遲敏感的推薦CMS;
  • 大記憶體服務,要求高吞吐的,採用G1回收器!

CMS垃圾回收器的工作機制

CMS主要是針對老年代的回收器,老年代是標記-清除,預設會在一次FullGC演算法後做整理演算法,清理記憶體碎片。

CMS GC描述Stop the world速度
1.開始標記初始標記僅標記GCRoots能直接關聯到的物件,速度很快Yes很快
2.併發標記併發標記階段就是進行GCRoots Tracing的過程No
3.重新標記重新標記階段則是為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄。Yes很快
4.垃圾回收併發清理垃圾物件(標記清除演算法)No
  • 優點:併發收集、主打“低延時” 。在最耗時的兩個階段都沒有發生STW,而需要STW的階段都以很快速度完成。
  • 缺點:1、消耗CPU;2、浮動垃圾;3、記憶體碎片
  • 適用場景:重視伺服器響應速度,要求系統停頓時間最短。

總之:

業務系統,延遲敏感的推薦CMS;

大記憶體服務,要求高吞吐的,採用G1回收器!

Step3:如何對各個分割槽的比例、大小進行規劃

一般的思路為:

首先,JVM最重要最核心的引數是去評估記憶體和分配,第一步需要指定堆記憶體的大小,這個是系統上線必須要做的,-Xms 初始堆大小,-Xmx 最大堆大小,後臺Java服務中一般都指定為系統記憶體的一半,過大會佔用伺服器的系統資源,過小則無法發揮JVM的最佳效能。

其次,需要指定-Xmn新生代的大小,這個引數非常關鍵,靈活度很大,雖然sun官方推薦為3/8大小,但是要根據業務場景來定,針對於無狀態或者輕狀態服務(現在最常見的業務系統如Web應用)來說,一般新生代甚至可以給到堆記憶體的3/4大小;而對於有狀態服務(常見如IM服務、閘道器接入層等系統)新生代可以按照預設比例1/3來設定。服務有狀態,則意味著會有更多的本地快取和會話狀態資訊常駐記憶體,應為要給老年代設定更大的空間來存放這些物件。

最後,是設定-Xss棧記憶體大小,設定單個執行緒棧大小,預設值和JDK版本、系統有關,一般預設512~1024kb。一個後臺服務如果常駐執行緒有幾百個,那麼棧記憶體這邊也會佔用了幾百M的大小。

JVM引數描述預設推薦
-XmsJava堆記憶體的大小OS記憶體64/1OS記憶體一半
-XmxJava堆記憶體的最大大小OS記憶體4/1OS記憶體一半
-XmnJava堆記憶體中的新生代大小,扣除新生代剩下的就是老年代的記憶體大小了跌認堆的1/3sun推薦3/8
-Xss每個執行緒的棧記憶體大小和idk有關sun

對於8G記憶體,一般分配一半的最大記憶體就可以了,因為機器本上還要佔用一定記憶體,一般是分配4G記憶體給JVM,

引入效能壓測環節,測試同學對登入介面壓至1s內60M的物件生成速度,採用ParNew+CMS的組合回收器,

正常的JVM引數配置如下:

-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 

這樣設定可能會由於動態物件年齡判斷原則導致頻繁full gc。為啥呢?

壓測過程中,短時間(比如20S後)Eden區就滿了,此時再執行的時候物件已經無法分配,會觸發MinorGC,

假設在這次GC後S1裝入100M,馬上過20S又會觸發一次MinorGC,多出來的100M存活物件+S1區的100M已經無法順利放入到S2區,此時就會觸發JVM的動態年齡機制,將一批100M左右的物件推到老年代儲存,持續執行一段時間,系統可能一個小時候內就會觸發一次FullGC。

按照預設8:1:1的比例來分配時, survivor區只有 1G的 10%左右,也就是幾十到100M,

如果 每次minor GC垃圾回收過後進入survivor物件很多,並且survivor物件大小很快超過 Survivor 的 50% , 那麼會觸發動態年齡判定規則,讓部分物件進入老年代.

而一個GC過程中,可能部分WEB請求未處理完畢, 幾十兆物件,進入survivor的機率,是非常大的,甚至是一定會發生的.

如何解決這個問題呢? 為了讓物件儘可能的在新生代的eden區和survivor區, 儘可能的讓survivor區記憶體多一點,達到200兆左右,

於是我們可以更新下JVM引數設定:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8  

說明:
‐Xmn2048M ‐XX:SurvivorRatio=8 
年輕代大小2g,eden與survivor的比例為8:1:1,也就是1.6g:0.2g:0.2g

survivor達到200m,如果幾十兆物件到底survivor, survivor 也不一定超過 50%

這樣可以防止每次垃圾回收過後,survivor物件太早超過 50% ,

這樣就降低了因為物件動態年齡判斷原則導致的物件頻繁進入老年代的問題,

什麼是JVM動態年齡判斷規則呢?

物件進入老年代的動態年齡判斷規則(動態晉升年齡計算閾值):Minor GC 時,Survivor 中年齡 1 到 N 的物件大小超過 Survivor 的 50% 時,則將大於等於年齡 N 的物件放入老年代。

核心的最佳化策略是:是讓短期存活的物件儘量都留在survivor裡,不要進入老年代,這樣在minor gc的時候這些物件都會被回收,不會進到老年代從而導致full gc

應該如何去評估新生代記憶體和分配合適?

這裡特別說一下,JVM最重要最核心的引數是去評估記憶體和分配,

第一步需要指定堆記憶體的大小,這個是系統上線必須要做的,-Xms 初始堆大小,-Xmx 最大堆大小,

後臺Java服務中一般都指定為系統記憶體的一半,過大會佔用伺服器的系統資源,過小則無法發揮JVM的最佳效能。

其次需要指定-Xmn新生代的大小,這個引數非常關鍵,靈活度很大,雖然sun官方推薦為3/8大小,但是要根據業務場景來定:

  • 針對於無狀態或者輕狀態服務(現在最常見的業務系統如Web應用)來說,一般新生代甚至可以給到堆記憶體的3/4大小;
  • 而對於有狀態服務(常見如IM服務、閘道器接入層等系統)新生代可以按照預設比例1/3來設定。

服務有狀態,則意味著會有更多的本地快取和會話狀態資訊常駐記憶體,應為要給老年代設定更大的空間來存放這些物件。

step4:棧記憶體大小多少比較合適?

-Xss棧記憶體大小,設定單個執行緒棧大小,預設值和JDK版本、系統有關,一般預設512~1024kb。一個後臺服務如果常駐執行緒有幾百個,那麼棧記憶體這邊也會佔用了幾百M的大小。

step5:物件年齡應該為多少才移動到老年代比較合適?

假設一次minor gc要間隔二三十秒,並且,大多數物件一般在幾秒內就會變為垃圾,

如果物件這麼長時間都沒被回收,比如2分鐘沒有回收,可以認為這些物件是會存活的比較長的物件,從而移動到老年代,而不是繼續一直佔用survivor區空間。

所以,可以將預設的15歲改小一點,比如改為5,

那麼意味著物件要經過5次minor gc才會進入老年代,整個時間也有一兩分鐘了(5*30s= 150s),和幾秒的時間相比,物件已經存活了足夠長時間了。

所以:可以適當調整JVM引數如下:

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 

step6:多大的物件,可以直接到老年代比較合適?

對於多大的物件直接進入老年代(引數-XX:PretenureSizeThreshold),一般可以結合自己系統看下有沒有什麼大物件 生成,預估下大物件的大小,一般來說設定為1M就差不多了,很少有超過1M的大物件,

所以:可以適當調整JVM引數如下:

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M

step7:垃圾回收器CMS老年代的引數最佳化

JDK8預設的垃圾回收器是-XX:+UseParallelGC(年輕代)和-XX:+UseParallelOldGC(老年代),

如果記憶體較大(超過4個G,只是經驗 值),還是建議使用G1.

這裡是4G以內,又是主打“低延時” 的業務系統,可以使用下面的組合:

ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)

新生代的採用ParNew回收器,工作流程就是經典複製演算法,在三塊區中進行流轉回收,只不過採用多執行緒並行的方式加快了MinorGC速度。

老生代的採用CMS。再去最佳化老年代引數:比如老年代預設在標記清除以後會做整理,還可以在CMS的增加GC頻次還是增加GC時長上做些取捨,

如下是響應優先的引數調優:

XX:CMSInitiatingOccupancyFraction=70

設定CMS在對記憶體佔用率達到70%的時候開始GC(因為CMS會有浮動垃圾,所以一般都較早啟動GC)

XX:+UseCMSInitiatinpOccupancyOnly

和上面搭配使用,否則只生效一次

-XX:+AlwaysPreTouch

強制作業系統把記憶體真正分配給IVM,而不是用時才分配。

綜上,只要年輕代引數設定合理,老年代CMS的引數設定基本都可以用預設值,如下所示:

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=70 ‐XX:+UseCMSInitiatingOccupancyOnly ‐XX:+AlwaysPreTouch

引數解釋

1.‐Xms3072M ‐Xmx3072M 最小最大堆設定為3g,最大最小設定為一致防止記憶體抖動

2.‐Xss1M 執行緒棧1m

3.‐Xmn2048M ‐XX:SurvivorRatio=8 年輕代大小2g,eden與survivor的比例為8:1:1,也就是1.6g:0.2g:0.2g

4.-XX:MaxTenuringThreshold=5 年齡為5進入老年代 5.‐XX:PretenureSizeThreshold=1M 大於1m的大物件直接在老年代生成

6.‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC 使用ParNew+cms垃圾回收器組合

7.‐XX:CMSInitiatingOccupancyFraction=70 老年代中物件達到這個比例後觸發fullgc

8.‐XX:+UseCMSInitiatinpOccupancyOnly 老年代中物件達到這個比例後觸發fullgc,每次

9.‐XX:+AlwaysPreTouch 強制作業系統把記憶體真正分配給IVM,而不是用時才分配。

step8:配置OOM時候的記憶體dump檔案和GC日誌

額外增加了GC日誌列印、OOM自動dump等配置內容,幫助進行問題排查

-XX:+HeapDumpOnOutOfMemoryError

在Out Of Memory,JVM快死掉的時候,輸出Heap Dump到指定檔案。

不然開發很多時候還真不知道怎麼重現錯誤。

路徑只指向目錄,JVM會保持檔名的唯一性,叫java_pid${pid}.hprof。

-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=${LOGDIR}/

因為如果指向特定的檔案,而檔案已存在,反而不能寫入。

輸出4G的HeapDump,會導致IO效能問題,在普通硬碟上,會造成20秒以上的硬碟IO跑滿,

需要注意一下,但在容器環境下,這個也會影響同一宿主機上的其他容器。

GC的日誌的輸出也很重要:

-Xloggc:/dev/xxx/gc.log 
-XX:+PrintGCDateStamps 
-XX:+PrintGCDetails

GC的日誌實際上對系統效能影響不大,打日誌對排查GC問題很重要。

一份通用的JVM引數模板

一般來說,大企業或者架構師團隊,都會為專案的業務系統定製一份較為通用的JVM引數模板,但是許多小企業和團隊可能就疏於這一塊的設計,如果老闆某一天突然讓你負責定製一個新系統的JVM引數,你上網去搜大量的JVM調優文章或部落格,結果發現都是零零散散的、不成體系的JVM引數講解,根本下不了手,這個時候你就需要一份較為通用的JVM引數模板了,不能保證效能最佳,但是至少能讓JVM這一層是穩定可控的,

在這裡給大家總結了一份模板:

基於4C8G系統的ParNew+CMS回收器模板(響應優先),新生代大小根據業務靈活調整!

-Xms4g
-Xmx4g
-Xmn2g
-Xss1m
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=10
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+AlwaysPreTouch
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:gc.log

如果是GC的吞吐優先,推薦使用G1,基於8C16G系統的G1回收器模板:

G1收集器自身已經有一套預測和調整機制了,因此我們首先的選擇是相信它,

即調整-XX:MaxGCPauseMillis=N引數,這也符合G1的目的——讓GC調優儘量簡單!

同時也不要自己顯式設定新生代的大小(用-Xmn或-XX:NewRatio引數),

如果人為干預新生代的大小,會導致目標時間這個引數失效。

-Xms8g
-Xmx8g
-Xss1m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=150
-XX:InitiatingHeapOccupancyPercent=40
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:gc.log
G1引數描述預設值
XX:MaxGCPauseMillis=N最大GC停頓時間。柔性目標,JVM滿足90%,不保證100%。200
-XX:nitiatingHeapOccupancyPercent=n當整個堆的空間使用百分比超過這個值時,就會融發MixGC45

針對-XX:MaxGCPauseMillis來說,引數的設定帶有明顯的傾向性:調低↓:延遲更低,但MinorGC頻繁,MixGC回收老年代區減少,增大Full GC的風險。調高↑:單次回收更多的物件,但系統整體響應時間也會被拉長。

針對InitiatingHeapOccupancyPercent來說,調參大小的效果也不一樣:調低↓:更早觸發MixGC,浪費cpu。調高↑:堆積過多代回收region,增大FullGC的風險。

調優總結

系統在上線前的綜合調優思路:

1、業務預估:根據預期的併發量、平均每個任務的記憶體需求大小,然後評估需要幾臺機器來承載,每臺機器需要什麼樣的配置。

2、容量預估:根據系統的任務處理速度,然後合理分配Eden、Surivior區大小,老年代的記憶體大小。

3、回收器選型:響應優先的系統,建議採用ParNew+CMS回收器;吞吐優先、多核大記憶體(heap size≥8G)服務,建議採用G1回收器。

4、最佳化思路:讓短命物件在MinorGC階段就被回收(同時回收後的存活物件<Survivor區域50%,可控制保留在新生代),長命物件儘早進入老年代,不要在新生代來回複製;儘量減少Full GC的頻率,避免FGC系統的影響。

5、到目前為止,總結到的調優的過程主要基於上線前的測試驗證階段,所以我們儘量在上線之前,就將機器的JVM引數設定到最優!

JVM調優只是一個手段,但並不一定所有問題都可以透過JVM進行調優解決,大多數的Java應用不需要進行JVM最佳化,我們可以遵循以下的一些原則:

  • 上線之前,應先考慮將機器的JVM引數設定到最優;
  • 減少建立物件的數量(程式碼層面);
  • 減少使用全域性變數和大物件(程式碼層面);
  • 優先架構調優和程式碼調優,JVM最佳化是不得已的手段(程式碼、架構層面);
  • 分析GC情況最佳化程式碼比最佳化JVM引數更好(程式碼層面);

透過以上原則,我們發現,其實最有效的最佳化手段是架構和程式碼層面的最佳化,而JVM最佳化則是最後不得已的手段,也可以說是對伺服器配置的最後一次“壓榨”。

什麼是ZGC?

ZGC (Z Garbage Collector)是一款由Oracle公司研發的,以低延遲為首要目標的一款垃圾收集器。

它是基於動態Region記憶體佈局,(暫時)不設年齡分代,使用了讀屏障、染色指標和記憶體多重對映等技術來實現可併發的標記-整理演算法的收集器。

在 JDK 11 新加入,還在實驗階段,

主要特點是:回收TB級記憶體(最大4T),停頓時間不超過10ms。

優點:低停頓,高吞吐量, ZGC 收集過程中額外耗費的記憶體小

缺點:浮動垃圾

目前使用的非常少,真正普及還是需要寫時間的。

如何選擇垃圾收集器?

在真實場景中應該如何去選擇呢,下面給出幾種建議,希望對你有幫助:

1、如果你的堆大小不是很大(比如 100MB ),選擇序列收集器一般是效率最高的。引數:-XX:+UseSerialGC

2、如果你的應用執行在單核的機器上,或者你的虛擬機器核數只有 單核,選擇序列收集器依然是合適的,這時候啟用一些並行收集器沒有任何收益。引數:-XX:+UseSerialGC

3、如果你的應用是“吞吐量”優先的,並且對較長時間的停頓沒有什麼特別的要求。選擇並行收集器是比較好的。引數:-XX:+UseParallelGC

4、如果你的應用對響應時間要求較高,想要較少的停頓。甚至 1 秒的停頓都會引起大量的請求失敗,那麼選擇 G1 、 ZGC 、 CMS 都是合理的。雖然這些收集器的 GC 停頓通常都比較短,但它需要一些額外的資源去處理這些工作,通常吞吐量會低一些。引數:-XX:+UseConcMarkSweepGC-XX:+UseG1GC-XX:+UseZGC 等。從上面這些出發點來看,我們平常的 Web 伺服器,都是對響應性要求非常高的。

選擇性其實就集中在 CMS、G1、ZGC 上。而對於某些定時任務,使用並行收集器,是一個比較好的選擇。

Hotspot為什麼使用元空間替換了永久代?

什麼是元空間?什麼是永久代?為什麼用元空間代替永久代?

我們先回顧一下方法區吧,看看虛擬機器執行時資料記憶體圖,如下:

方法區和堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯後的程式碼等資料。

什麼是永久代?它和方法區有什麼關係呢?

如果在HotSpot虛擬機器上開發、部署,很多程式設計師都把方法區稱作永久代。

可以說方法區是規範,永久代是Hotspot針對該規範進行的實現。

在Java7及以前的版本,方法區都是永久代實現的。

什麼是元空間?它和方法區有什麼關係呢?

對於Java8,HotSpots取消了永久代,取而代之的是元空間(Metaspace)。

換句話說,就是方法區還是在的,只是實現變了,從永久代變為元空間了。

為什麼使用元空間替換了永久代?

永久代的方法區,和堆使用的實體記憶體是連續的。

永久代是透過以下這兩個引數配置大小的~

  • -XX:PremSize:設定永久代的初始大小
  • -XX:MaxPermSize: 設定永久代的最大值,預設是64M

對於永久代,如果動態生成很多class的話,就很可能出現java.lang.OutOfMemoryError:PermGen space錯誤,因為永久代空間配置有限嘛。最典型的場景是,在web開發比較多jsp頁面的時候。

JDK8之後,方法區存在於元空間(Metaspace)。

實體記憶體不再與堆連續,而是直接存在於本地記憶體中,理論上機器記憶體有多大,元空間就有多大

可以透過以下的引數來設定元空間的大小:

  • -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行型別解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
  • -XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。
  • -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集

所以,為什麼使用元空間替換永久代?

表面上看是為了避免OOM異常。

因為通常使用PermSize和MaxPermSize設定永久代的大小就決定了永久代的上限,但是不是總能知道應該設定為多大合適, 如果使用預設值很容易遇到OOM錯誤。

當使用元空間時,可以載入多少類的後設資料就不再由MaxPermSize控制, 而由系統的實際可用空間來控制啦。

什麼是Stop The World ? 什麼是OopMap?什麼是安全點?

進行垃圾回收的過程中,會涉及物件的移動。

為了保證物件引用更新的正確性,必須暫停所有的使用者執行緒,像這樣的停頓,虛擬機器設計者形象描述為Stop The World。也簡稱為STW。

在HotSpot中,有個資料結構(對映表)稱為OopMap

一旦類載入動作完成的時候,HotSpot就會把物件內什麼偏移量上是什麼型別的資料計算出來,記錄到OopMap。

在即時編譯過程中,也會在特定的位置生成 OopMap,記錄下棧上和暫存器裡哪些位置是引用。

這些特定的位置主要在:1.迴圈的末尾(非 counted 迴圈)

2.方法臨返回前 / 呼叫方法的call指令後

3.可能拋異常的位置

這些位置就叫作安全點(safepoint)。

使用者程式執行時並非在程式碼指令流的任意位置都能夠在停頓下來開始垃圾收集,而是必須是執行到安全點才能夠暫停。

最後說一句(別白嫖,求關注)

陳某每一篇文章都是精心輸出,如果這篇文章對你有所幫助,或者有所啟發的話,幫忙點贊在看轉發收藏,你的支援就是我堅持下去的最大動力!

關注公眾號:【碼猿技術專欄】,公眾號內有超讚的粉絲福利,回覆:加群,可以加入技術討論群,和大家一起討論技術,吹牛逼!

相關文章