1. 程式人生 > 實用技巧 >java堆記憶體設定優化

java堆記憶體設定優化

為什麼80%的碼農都做不了架構師?>>> hot3.png

在前天我在公司內部做了一個分享,好久沒有更新部落格,主要是工作太忙,沒有時間去總結,這篇部落格也是這次分享時內容。

對於java堆記憶體設定,首先需要對java記憶體解構有所瞭解,對於linux平臺,java的底層又是c寫的,因此java記憶體結構又是在c的記憶體結構之上,所以我準備從c的記憶體結構講起。

對於java的特點,如跨平臺、支援多執行緒、自動記憶體回收等,我們也能分析到java到底怎麼去執行,比如,如果需要跨平臺,意味著java語言編譯後的指令必須與平臺無關,然後由一個解釋執行引擎把平臺無關的指令翻譯成平臺相關的指令,再由cpu執行。對於支援多執行緒,java必須有排程的功能,而且必須為每個執行緒維護一個pc資訊(類似於系統程序資訊一樣),儲存當前執行指令位置等相關資訊。對於記憶體回收,java必須有自己的記憶體回收機制和策略,而記憶體回收對於每個java使用者來說,應該瞭解怎樣去控制,針對業務調優。

首先我們瞭解一下c的記憶體結構。

以上幾個塊就是c執行主要的幾個段,畫的簡單,還有很多其它的段,如rodata等,而且數量也有多個的,這裡不作介紹,c在載入到記憶體時,text、data、bss這幾個段在編譯時,記憶體大小都已確定,執行時不能調整,唯一由程式設計師控制的記憶體區域就是堆(heap)段,而棧(stack)是運時儲存區域性內量,和跳轉現場的區域,自身沒有限制,但作業系統會對這個棧大小做限制,如linux限制在10M,freebsd限制512M,而c的運行當前指令由cpu暫存器的cs和pc決定,函式跳轉需要的堆疊由cpu暫存器SP(棧頂),BP(棧底)去實現,剛才提到java底層也是c寫的,而java底層也是這樣的結構,因此java所有的資料都儲存在底層結構的heap段中(PS,其實java結構我覺得也有點模擬底層的結構)。

然後我們再看看java的記憶體結構。

對於java的執行,需要以完成以下幾步:1,class回載到記憶體(class loader)。2,資料區域,儲存執行資料(runtime data area)。3,執行引擎,解析翻譯執行指令,這個也是體現各個jdk的關鍵的地方(execution engine)。4,底層本地庫的呼叫,在linux上就是so結尾的庫檔案(native interface)。

對於資料區域,必須有儲存class的方法區(method area),儲存物件的堆(heap),自身的棧(java stack),底層庫調時的棧(native method stack),保證執行緒資訊pc(program counter register)。從執行緒安全的角度,只能有方法區(method)和堆(heap)是可以執行緒共享的,其它的都是私有獨立的。堆的設定就關係到記憶體回

收,也是這篇部落格的重點。

在java中,對堆(heap)又進一步作了劃分,分為新生代(二個s區一個E區),老生代(old區),永久區(perm),對於記憶體的分段的優點我覺得主要有二點,1,各個物件生命週期不一樣,有些物件需要永久儲存在記憶體,有些物件是臨時使用的。不能對等對待。2,有些區域需要保護,而且只讀,避免溢位覆蓋,影響穩定性,最明顯就是位元組碼區域和資料區域不能混合。對於記憶體回收,要為小回收(回收新生代)和大回收full gc(回收老生代和永久區)

首先是新生代,大小引數如-XX:MaxNewSize=2g -XX:NewSize=2g來指定,一個是指定是最大空間,一個是初始空間,而S區和E的比例則由-XX:SurvivorRatio來指定時,計算是要考慮S的二個,如-XX:SurvivorRatio=1時,意味著二個S的大小是整個新生代的2/3,不作任何配置,預設是單執行緒記憶體回收,也可以由-XX:+UseParNewGC指定多個執行緒回收,回收的執行緒數量由-XX:ParallelGCThreads=8(指定8個執行緒)設定。

對於永久區,配置如-XX:PermSize=512M -XX:MaxPermSize=512M,永久區的空間只有一個用途,儲存方法和靜態變數等,一般情況下,執行時,永久區的大小不會發生變化,通常大小我設定為所有jar包的總和的3/2,通過jstat檢視時,保證比例為70%-80%就行了,但是有些情況,如有些java框架會動態生類,永久區的大小會不停的增漲,而這種框架有一種特點,他自身會不停的呼叫system.gc()觸發full gc,上面的方法就不能用了,必須配置大一點的永久區,同時關閉system.gc()()新增-XX:+DisableExplicitGC。

對於老生代的大小,不需要去指定,剩餘空間就是老生代,對記憶體回收預設是單執行緒,可以指定多執行緒-XX:+UseParallelOldGC,執行緒數量也是ParallelGCThreads指定,對於老生代回收,回收的方法不像新生使用的copy的方式,它使用的是mark的方式,而且空間大小通常比新生代大,即使是多執行緒回收,回收的時間也是比較長,在一些實時互動的程式,回收時間過長比較影響業務,java1.6中,提供了一種新的方法remark回收老生代-XX:+UseConcMarkSweepGC,它使用二mark的方式回收,每次mark的時間都非常短,在實時互動的程式記憶體回收時,效果比較好,也可以使用多執行緒如-XX:ParallelCMSThreads=8(8個執行緒),使用remark時,另一個引數非常重要-XX:CMSInitiatingOccupancyFraction=70,指定達到多大的比例觸發回收,這通常要結合業務特點。

怎樣的回收方式的合理的呢,我總結一下。1,新生代的回收,也就是小回收,一次回收的時間不能過長,因為新生代的回收比較頻繁,只能通過控制單次回收的時間來保障效能,如下是一個合理的配置,約8秒觸發一次小gc,每次的時間約為22毫秒,22毫秒的中斷,我認為還是可以接受的:


但是有的時候,新生代單次回收的時間不長,但時會出現連續回收的情況,這種方式也是不合理的,通常發生E區和S區比例不對和-XX:MaxTenuringThreshold=設定不合理,如下就是連續回收的情況:


對於老生代和新生代,觸發的都是大回收,應該儘量控制發生的次數,具體策略如減少從新生代拷貝到老生代的策略,如設定-XX:MaxTenuringThreshold=,讓回收時新生物件在新生代多呆一段時間,把新生代設定適當的大一點,也可以使用remark的方式,筆者的生產環境全部使用的是remark的方式,當然還要注意一點,永久區也可以觸發大回收,還有些框架也會顯式呼叫system.gc(),主要避免是設定永久區不能過小,關閉system.gc()。另外還有重要一點,要定期檢視大回收的情況,用大回收的總時間除以大回收的次數,算一下平均每次的時間,如果過大,意味你的業務出現長時間的停頓。如下:609/35=17.4,平均一次大回收耗時17.4秒,這種情況是絕對不能接受的:


轉載於:https://my.oschina.net/beiyou/blog/122394