年後,累計修改履歷100多份,模擬面試200多場。
就在上週,一個同學在阿里雲技術面終面的時候被問到這麼一個問題:假設一個每天100w次登陸請求的平台,一個服務節點8G 內存,該如何設定JVM參數? 覺得回答的不太理想,過來找我複盤。
如果你也需要履歷修改、履歷美化、履歷包裝、模擬面試等,都可以聯絡我。
以下以面試題的形式給大家梳理出來,做到一箭雙雕:
接下來,進入正題。
每天100w次登陸請求, 8G 記憶體該如何設定JVM參數,大概可以分成以下8個步驟。
1.套路總結
任何新的業務系統在上線以前都需要去估算伺服器設定和JVM的記憶體參數,這個容量與資源規劃並非僅是系統架構師的隨意估算的,需要根據系統所在業務場景去估算,推斷出來一個系統運行模型,評估JVM性能和GC頻率等等指標。以下是我結合大牛經驗以及自身實踐來總結出來的一個建模步驟:
2.套路实战——以登录系统为例
有些同学看到这些步骤还是发憷,说的好像是那么回事,一到实际项目中到底怎麽做我还是不知道!
光说不练假把式,以登录系统为例模拟一下推演过程:
所以,可以粗略的推断出来一个每天100w次请求的登录系统,按照4C8G的3实例集群配置,分配4G堆内存、2G新生代的JVM,可以保障系统的一个正常负载。
基本上把一个新系统的资源评估了出来,所以搭建新系统要每个实例需要多少容量多少配置,集群配置多少个实例等等这些,并不是拍拍脑袋和胸脯就可以决定的下来的。
首先引入两个概念:吞吐量和低延迟
吞吐量= CPU在用戶應用程式運行的時間/(CPU在用戶應用程式運行的時間CPU垃圾回收的時間)
回應時間= 平均每次的GC的耗時
通常,吞吐優先還是回應優先這個在JVM中是一個兩難之選。
堆記憶體增大,gc一次能處理的數量變大,吞吐量大;但是gc一次的時間會變長,導致後面排隊的線程等待時間變長;相反,如果堆內存小, gc一次時間短,排隊等待的線程等待時間變短,延遲減少,但一次請求的數量變小(並不絕對符合)。
無法同時兼顧,是吞吐優先還是回應優先,這是一個需要權衡的問題。
目前主流的垃圾回收器配置是新生代採用ParNew,老年代採用CMS組合的方式,或是完全採用G1回收器,
從未來的趨勢來看,G1是官方維護和更為推崇的垃圾回收器。
業務系統:
CMS主要是針對老年代的回收器,老年代是標記-清除,預設會在一次FullGC演算法後做整理演算法,清理記憶體碎片。
CMS GC | 描述 | #Stop the world | |
---|---|---|---|
##1.開始標記 | 初始標記僅標記GCRoots能直接關聯到的對象,速度很快 | Yes | |
2.並發標記######並發標記階段就是進行GCRoots Tracing的過程######No######慢####### #####3.重新標記######重新標記階段則是為了修正並發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄。 | Yes | 很快 | |
4.垃圾回收 | 並發清理垃圾物件(標記清除演算法) | No | 慢 |
总之:
业务系统,延迟敏感的推荐CMS;
大内存服务,要求高吞吐的,采用G1回收器!
一般的思路为:
首先,JVM最重要最核心的参数是去评估内存和分配,第一步需要指定堆内存的大小,这个是系统上线必须要做的,-Xms 初始堆大小,-Xmx 最大堆大小,后台Java服务中一般都指定为系统内存的一半,过大会佔用服务器的系统资源,过小则无法发挥JVM的最佳性能。
其次,需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大,虽然sun官方推荐为3/8大小,但是要根据业务场景来定,针对于无状态或者轻状态服务(现在最常见的业务系统如Web应用)来说,一般新生代甚至可以给到堆内存的3/4大小;而对于有状态服务(常见如IM服务、网关接入层等系统)新生代可以按照默认比例1/3来设置。服务有状态,则意味著会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。
最后,是设置-Xss栈内存大小,设置单个线程栈大小,默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那麽栈内存这边也会佔用了几百M的大小。
JVM參數 | #描述 | 預設 | ##推薦|
---|---|---|---|
Java堆記憶體的大小 | OS記憶體64/1 | OS記憶體一半 | |
Java堆記憶體的最大大小 | OS記憶體4/1 | OS記憶體一半 | |
Java堆記憶體中的新生代大小,扣除新生代剩下的就是老年代的記憶體大小了 | #跌認堆的1/3 | ##sun推薦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% ,
这样就降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题,
对象进入老年代的动态年龄判断规则(动态晋升年龄计算阈值):Minor GC 时,Survivor 中年龄 1 到 N 的对象大小超过 Survivor 的 50% 时,则将大于等于年龄 N 的对象放入老年代。
核心的优化策略是:是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc。
这里特别说一下,JVM最重要最核心的参数是去评估内存和分配,
第一步需要指定堆内存的大小,这个是系统上线必须要做的,-Xms 初始堆大小,-Xmx 最大堆大小,
后台Java服务中一般都指定为系统内存的一半,过大会佔用服务器的系统资源,过小则无法发挥JVM的最佳性能。
其次需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大,虽然sun官方推荐为3/8大小,但是要根据业务场景来定:
服务有状态,则意味著会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。
-Xss栈内存大小,设置单个线程栈大小,默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那麽栈内存这边也会佔用了几百M的大小。
假设一次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
对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),一般可以结合自己系统看下有没有什么大对象 生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过1M的大对象,
所以:可以适当调整JVM参数如下:
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M
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,而不是用時才分配。
#額外增加了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这一层是稳定可控的,
在这里给大家总结了一份模板:
基于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
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 | 当整个堆的空间使用百分比超过这个值时,就会融发MixGC | 45 |
針對-XX:MaxGCPauseMillis
來說,參數的設定帶有明顯的傾向性:調低↓:延遲更低,但MinorGC頻繁,MixGC回收老年代區減少,增大Full GC的風險。調高↑:單次回收更多的對象,但係統整體反應時間也會被拉長。
針對InitiatingHeapOccupancyPercent
來說,調參大小的效果也不一樣:調低↓:更早觸發MixGC,浪費cpu。調高↑:堆積過度代回收region,增加FullGC的風險。
系統在上線前的綜合調優想法:
1、業務預估:根據預期的並發量、平均每個任務的記憶體需求大小,然後評估需要幾台機器來承載,每台機器需要什麼樣的配置。
2、容量預估:依照系統的任務處理速度,然後合理分配Eden、Surivior區大小,老年代的記憶體大小。
3、回收機選型:回應優先的系統,建議採用ParNew CMS回收器;吞吐優先、多核心大記憶體(heap size≥8G)服務,建議採用G1回收機。
4、優化思路:讓短命對像在MinorGC階段就被回收(同時回收後的存活對象 5、到目前為止,總結到的調優的過程主要基於上線前的測試驗證階段,所以我們盡量在上線之前,就將機器的JVM參數設定到最優! JVM調優只是一個手段,但不一定所有問題都可以透過JVM進行調優解決,大多數的Java應用不需要進行JVM最佳化,我們可以遵循以下的一些原則: 透過上述原則,我們發現,其實最有效的最佳化手段是架構和程式碼層面的最佳化,而JVM優化則是最後不得已的手段,也可以說是伺服器配置的最後一次「壓榨」。 ZGC (Z Garbage Collector)是一款由Oracle公司研發的,以低延遲為首要目標的一款垃圾收集器。 它是基於動態Region記憶體佈局,(暫時)不設年齡分代,使用了讀取屏障、染色指標和記憶體多重映射等技術來實現可並發的標記-整理演算法的收集器。 在 JDK 11 新加入,還在實驗階段, 主要特點是:回收TB級記憶體(最大4T),停頓時間不超過10ms。 優點:低停頓,高吞吐量, ZGC 收集過程中額外耗費的記憶體小 #缺點:浮動垃圾 目前使用的非常少,真正普及還是需要寫時間的。 在真實場景中該如何去選擇呢,下面給幾個建議,希望對你有幫助: 1、如果你的堆大小不是很大(例如100MB ),選擇串行收集器一般是效率最高的。參數: 2、如果你的應用運行在單核的機器上,或者你的虛擬機核數只有單核,選擇串行收集器仍然是合適的,這時候啟用一些並行收集器沒有任何收益。參數: 3、如果你的應用程式是「吞吐量」優先的,並且對較長時間的停頓沒有什麼特別的要求。選擇並行收集器是比較好的。參數: 4、如果你的應用程式對回應時間要求較高,想要較少的停頓。甚至 1 秒的停頓都會引起大量的請求失敗,那麼選擇 G1 、 ZGC 、 CMS 都是合理的。雖然這些收集器的 GC 停頓通常都比較短,但它需要一些額外的資源來處理這些工作,通常吞吐量會更低。參數: 選擇性其實就集中在 CMS、G1、ZGC 上。而對於某些定時任務,使用並行收集器,是比較好的選擇。 什麼是元空間?什麼是永久代?為什麼用元空間代替永久代? 我們先回顧一下方法區吧,看看虛擬機器運行時資料記憶體圖,如下: 方法區和堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類別資訊、常數、靜態變數、即時編譯後的程式碼等數據。 什麼是永久代?它和方法區有什麼關係呢? 如果在HotSpot虛擬機器上開發、部署,很多程式設計師都把方法區稱為永久代。 可以說方法區是規範,永久代是Hotspot針對該規範所進行的實作。 在Java7及以前的版本,方法區都是永久代實現的。 什麼是元空間?它和方法區有什麼關係呢? 對於Java8,HotSpots取消了永久代,取而代之的是元空間(Metaspace)。 換句話說,就是方法區還是在的,只是實作變了,從永久代變成元空間了。 為什麼使用元空間取代了永久代? 永久代的方法區,和堆使用的實體記憶體是連續的。 永久代是透過以下這兩個參數來配置大小的~ 對於永久代,如果動態產生很多class的話,就很可能出現java.lang.OutOfMemoryError :PermGen space錯誤,因為永久代空間配置有限嘛。最典型的場景是,在web開發比較多jsp頁面的時候。 JDK8之後,方法區存在於元空間(Metaspace)。 物理記憶體不再與堆連續,而是直接存在於本地記憶體中,理論上機器記憶體有多大,元空間有多大。 可以透過以下的參數來設定元空間的大小: 所以,為什麼要用元空間替換永久代? 表面上看是為了避免OOM異常。 因為通常使用PermSize和MaxPermSize設定永久代的大小就決定了永久代的上限,但是不是總能知道應該設定為多大合適, 如果使用預設值很容易遇到OOM錯誤。 當使用元空間時,可以載入多少類別的元資料就不再由MaxPermSize控制, 而由系統的實際可用空間來控制啦。 在進行垃圾回收的過程中,會涉及物件的移動。 為了確保物件引用更新的正確性,必須暫停所有的使用者線程,像這樣的停頓,虛擬機器設計者形象描述為Stop The World。也簡稱為STW。 在HotSpot中,有個資料結構(映射表)稱為OopMap。 一旦類別載入動作完成的時候,HotSpot就會把物件內什麼偏移量上是什麼類型的資料計算出來,記錄到OopMap。 在即時編譯過程中,也會在特定的位置產生 OopMap,記錄下堆疊上和暫存器裡哪些位置是引用。 這些特定的位置主要在:1.循環的末端(非 counted 迴圈) 2.方法臨回前 / 呼叫方法的call指令後 3.可能拋異常的位置 這些位置就叫作安全點(safepoint)。 使用者程式執行時並非在程式碼指令流的任意位置都能夠在停頓下來開始垃圾收集,而是必須是執行到安全點才能夠暫停。
什麼是ZGC?
如何選擇垃圾收集器?
-XX: UseSerialGC
。 -XX: UseSerialGC
。 -XX: UseParallelGC
。 -XX: UseConcMarkSweepGC
、 -XX: UseG1GC
、 -XX: UseZGC
等。從上面這些出發點來看,我們平常的 Web 伺服器,都是對回應性要求非常高的。 Hotspot為什麼要用元空間取代了永久代?
-XX:PremSize
:設定永久代的初始大小-XX:MaxPermSize
: 設定永久代的最大值,預設是64M
-XX:MetaspaceSize
,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當地提高該值。 -XX:MaxMetaspaceSize
,最大空間,預設是沒有限制的。 -XX:MinMetaspaceFreeRatio
,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集-XX:MaxMetaspaceFreeRatio
,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集什麼是Stop The World ? 什麼是OopMap?什麼是安全點?
以上是阿里終面:每天100w次登陸請求, 8G 內存該如何設定JVM參數?的詳細內容。更多資訊請關注PHP中文網其他相關文章!