jvm調優

         Jvm與c++不同,它實現了內存的自動分配和回收,這種機制使程序員無需關注內存的使用和回收,正是這種情況往往造成程序與jvm內存的分配和回收機制不匹配,從而出現各種問題。比如程序代碼量很大,虛擬機永久區內存不足,會拋出outofmemoryerror錯誤,代碼中全局大對象很多導致垃圾回收時間變長,應用出現卡頓現象等。當這二者不匹配時,有時候可能是代碼編寫不合理造成的,例如上面第二個現象。也有時候需要我們調節jvm各個參數,優化內存的分配和回收,使jvm滿足我們程序的要求,比如程序代碼量很大,我們要調大永久區,纔不會拋出outofmemoryerror。總之,無論哪種現象都需要我們充分理解jvm的內存機制的基礎上,才能解決問題,上一篇講了jvm的垃圾回收機制,這一篇我們要充分理解下jvm的內存區域和jvm調優時用到的各個參數。

         根據jvm規範的規定,jvm的內存區域包括:程序計數器、棧、堆、方法區、運行時常量池。這幾塊區域可以分爲三部分:線程堆棧(程序計數器、棧)、堆、非堆(包括方法區、運行時常量池)。

         堆棧

         線程堆棧是表示jvm給每個線程分配的內存大小,jdk5.0以後每個線程分配的內存大小是1         M,這個值的調節是通過-Xss設置。在線程中,每個方法使用和結束就意味着一個棧幀的入棧和出棧,若方法中使用較大深度的遞歸或者較大循環時,就會導致大量棧幀入棧,內存空間不足,拋出stackoverflowerror錯誤。因此,若jvm拋出stackoverflowerror錯誤時,就需要調節-xss參數,擴大棧的空間,或者檢查代碼,查看是否有較大(或無限)深度遞歸或循環,如果是代碼有問題,修改代碼,若代碼無問題,需通過-Xss參數擴大棧空間。在jvm默認參數下,棧深度在大多數情況下達到1000到2000完全沒有問題,對於正常的方法調用,這個深度是完全夠用的。若每個棧的空間變大,在操作系統物理空間內存不變的情況下的,可生成的線程數量就會變少。在32位操作系統下,每個進程可分配的內存大概是2GB,64位操作系統無限制,但是物理內存大小是固定的,是有限制的。因此,當在多線程應用中,每增加一個線程就需要多佔用物理內存1M,若物理內存或者進程分配的內存不足時,這是jvm生成新的線程時就會產生outofmemoryerror錯誤。因爲堆或非堆內存不足也會拋出outofmemoryerror錯誤,因此當出現這種問題,要具體分析是哪部分空間不足,若物理內存(或進程分配的內存)已佔滿,堆內存和非堆內存空間都在正常的使用範圍內,那麼問題就出現在堆棧空間不足,要麼增加物理內存,要麼減少堆空間(這種方式可能不合理)。所以這一部分,如果jvm沒有出現錯誤,保持默認配置即可。

         堆內存

         大家更關注的往往是堆內存的分配和回收,這部分是jvm需要優化的主戰場,垃圾回收主要是針對堆內存,jvm垃圾回收的頻率和時間是程序運行好壞的重要指標,因爲垃圾回收要終止其它正常的工作線程,導致程序停頓。影響Jvm垃圾回收的頻率和時間因素,除了垃圾收集器自身的實現機制外,堆內存的大小或各帶(堆分代設置)比例設置等也是重要的因素。調節堆內存的參數如下:

-Xms 1024M  //表示堆內存初始化大小,默認是物理內存1/64;

-Xmx 1024M  //表示堆內存最大值,默認是物理內存1/4;

-Xmn 512M  //表示堆中新生代大小;

-XX:NewRadio  //表示新生代與老年代的比例,-XX:NewRadio =2表示比例爲1:2;

-XX:SurvivorRadio   //表示新生代中eden區與survivor區的比例,默認比例爲8;

        Jvm中堆內存分爲新生代和老年代,新生代又分爲eden區和兩個survivor區,其中eden區和survivor區默認比例是8:1。新生代中垃圾回收採用複製算法,每次對eden區和其中一個survivor區執行垃圾回收,然後將存活的對象複製到另一個survivor區中,因此,新生代只能使用90%的空間。一般來說,對象首先進入新生代的eden區,經過若干次(默認是15次)回收後,若還存活,則進入老年代,這個回收次數相關的參數設置是:

-XX:MaxTenuringThreshold  //表示經過多少次回收,對象進入老年代,默認是15;

對於這個參數,對象並不是都必須經過15次回收後進入老年代,若survivor區中的經過相同回收次數的對象佔survivor空間的一半以上時,大於或等於這個次數的對象就可以直接進入老年代。但是,對於一些大對象可以設置直接進入老年代,以避免對新生代中eden區和兩個survivor區之間發生大量的內存複製,因爲大對象需要足夠大的空間,導致新生代空間還相對充足時就提前執行垃圾回收來獲取足夠的連續空間來安置大對象。Jvm提供了一個參數用來設置大對象直接進入老年代:

-XX:PretenureSizeThreshold  //直接進入老年代對象的值的閾值,-XX:PretenureSizeThreshold=3M,表示大於3M的對象可以直接 進入老年代

        以上是jvm中堆內存設置相關的部分參數,在實際的應用中有些設置可以參考很多的經驗總結,比如xms與xmx值設置相同,性能才能最好,因爲這樣可以避免每次垃圾回收進行內存重新分配,使更多的資源用在具體的應用中。然而也有些設置需要參考具體應用,比如eden區、survivor區和老年代等各空間的大小和比例,是否能滿足當前的應用,都需要根據具體應用調整各個區內存的大小和比例。現今,硬件可以說已經不是阻礙程序提高性能的瓶頸了,尤其是內存,目前使用的操作系統基本上都是64位了,16G、32G的內存條已經很普遍了。若堆中某區因空間不足而出現outofmemoryerror錯誤或者程序反應很慢,我們完全可以增大相應的內存來解決。當然一味的增加內存,使jvm不會拋出outofmemroryerror錯誤,但是有可能會造成一次需要回收的對象增多,回收時間變長,程序出現明顯卡頓,從而影響應用的使用,也是不合理的。因此,我們在對jvm堆內存調優時,在保證內存足夠用的同時,還要關注的就是各帶之間的比例關係,eden區和survivor區之間的默認比例是8:1,一般按照這個比例設置即可。需要關注的是新生代與老年代之間內存大小比例,新生代不能太小否則會頻繁執行minor gc,老年代也不能太小,否則新生代中的對象無法進入老年代,只好頻繁執行full gc,總之這個比例,查看了很多資料也沒有確定的值,還是要根據實際的應用來確定。

        非堆內存

    code cache

         對於非堆區,jdk7及以前版本包括:code cache和perm gen,jdk8中包括code cache、metaspace和compressed class space。它們有共同的部分就是codecache,首先理解下code cache。在jvm調優時一般不太關注code cache,很少因爲它的空間不足造成性能下降,理解code cache,先理解java編譯器。對於java編譯器,把java文件編譯成爲class文件,我們稱爲前端編譯,對應的,將class文件中的字節碼編譯稱爲機器碼稱爲後端編譯,最初,hotspot虛擬機運行時,是對字節碼逐條解析,這種方式比較慢,後臺hotspot引入了jit編譯器,對於那些被頻繁使用的方法,即熱點代碼,jit編譯器將這些熱點代碼編譯成本地機器碼,保存jvm內存中,虛擬機運行時需要調用這些方法時,直接從內存中獲取,這樣效率就會更高。上面所講的被保存的機器碼就是code cache。 code cache大小在client模式是32M,在server模式是48M,但是,這個數值在jdk7或以前版本中是這麼規定的,在jdk8中好像不是這麼規定的,通過jconsole查看code cache大小:

Code cache的大小並不是48M。當code cache佔滿時,會停止方法的編譯,進而影響性能。

    永久區         

        對於perm gen,也就是被稱爲永久代,在jdk8中已經移除了永久帶,但在jdk7中是存在的,若應用還在使用jdk7時,對永久帶的調優參數如下:

-XX:PermSize=512M  //永久區初始大小

-XX:MaxPermSize=512M  //永久區最大值

實際應用中,也是將permsize與maxpermsize設置相同值,性能達到最好。知道永久區調節參數後,我們要明白永久區的作用,jvm運行時哪些內容存儲在永久區中,這樣永久區出現內存溢出時,才能明確地調整。在Hotspot中,一般把方法區稱爲永久代,方法區中存放兩部分內容,一個是類信息(類版本、字段、方法、接口等描述信息),另一個就是常量池,包括字面量(字符串、被final修飾的常量值等)和符號引用。可知,永久區主要存儲的就是class文件相關的內容,所以若應用的工程很大,代碼量很大,那麼永久區被佔的空間就很大。永久區內存不足時,jvm會拋出outofmemoryerror:permgenspace錯誤。需要注意的是在jdk7中已將將字符串常量移動到堆中,不會因字符串常量很大,使永久區不足。永久區中的垃圾回收主要針對兩種類型:被廢棄的常量和已卸載的類,被廢棄的常量就是沒有任何地方引用這個字面量了,這個比較簡單。已卸載的類相對來說就很複雜了,一個類被稱爲已卸載的類須滿足三個條件:1.該類所有實例已經被回收;2.加載該類的classloader已經被回收;3.該類對應的java.lang.Class對象沒有在任何地方被引用。所以,永久區垃圾回收比較困難,所以jvm在對永久區垃圾回收採用的策略是永久區被使用空間達到maxPermSize時,執行full gc,只要永久區在正確的大小值時不執行垃圾回收。總之,永久代、老年代和新生代都稱爲jvm內存,它們的內存分配和回收受jvm控制,除非當內存不足時,需要向操作系統擴張空間時。

    metaspace區         

        在jdk8中已經不存在永久區了,因此關於永久區調優的參數已經不再適用了,即使用了,jvm會報警但不會出錯。代替永久區的是metaspace。原先在永久代中的常量池中的字面量移動到堆中,類信息和符號引用被保留在現在被稱爲metaspace的區域中,這樣做的好處,我想更有利於jvm的內存分配和回收,常量的回收條件畢竟比類的回收更簡單。Metaspace內存調節參數:

-XX:MetaspaceSize=100M  //表示metaspace使用空間達到這個值時,執行垃圾回收

-XX:MaxMetaspaceSize=100M  //metaspace的最大值

默認情況下,metaspace是沒有最大值的,因爲metaspace不是jvm內存的一部分,它是屬於本地內存,內存的分配由操作系統負責。一般來說,metaspace不需要設置相關參數,若這個區域內存溢出,說明物理內存已經不足,需要增加內存卡了。

    直接內存         

    Jvm還有一部分內存稱爲直接內存(direct memory),這部分內存不是jvm規範的內存區域,也不在堆內存中。這一部分內存主要是爲nio(new input/output)操作準備的,使用native函數分配堆外內存,然後通過存儲在java堆中的directbytebuffer對象作爲對這塊內存的引用進行操作。直接內存不屬於堆內存,這部分若內存不足也會拋出outofmemoryerror:direct buffer memory錯誤,直接內存通過下面參數調整大小:

-XX:MaxDirectMemorySize  //調整直接內存大小

    垃圾收集器         

        Jvm調優無非就是兩點,一是jvm不能拋出異常,比如stackoverflowerror、outofmemoryerror等,二是jvm性能要滿足實際的應用,不能出現卡段或長時間不反應等現象,尤其是高併發時。上面講的內容已經解決了第一點的問題,對於第二點的問題,是關於jvm內存各區之間的內存比例設置和各區垃圾收集器的使用。各區之間的內存比例需要根據實際的情況具體分析,沒有一個放之四海皆準的比例參數供我們選擇。另一篇文章講了各個垃圾收集器的原理,本文再次對垃圾收集器進行歸納總結下,hotspot虛擬機使用的垃圾收集器可分爲三種類型:

串行收集器,單線程收集器,收集器工作需要停止工作線程;

並行收集器,多線程收集器,收集器工作需要停止工作線程;

併發收集器,多線程收集器,在收集器工作的部分階段需要停止工作線程;

在hotspot虛擬機中垃圾收集器按照這三種虛擬機類型劃分:

串行收集器:serial收集器(用於client模式,基本上被棄用),serial old收集器(用於老年代,採用標記整理算法,也被稱爲ps marksweep收集器,若老年代採用此收集器,那麼新生代只能使用parallel scavenge收集器)。

並行收集器:parnew收集器(用於新生代,採用複製算法,若老年代使用cms收集器那麼新生代只能使用這個收集器)、parallelscavenge收集器(用於新生代,採用複製算法,與serialold收集器配合使用)、parallel old收集器(parallel scavenge收集器用於老年代的版本,採用標記-整理算法)。

併發收集器:cms收集器(用於老年代,標記清除算法)。

垃圾收集器調節相關參數:

-XX:+UseSerialGC:在新生代和老年代使用串行收集器

-XX:+UseParNewGC:在新生代使用並行收集器

-XX:+UseParallelOldGC:老年代使用並行回收收集器

-XX:ParallelGCThreads:設置用於垃圾回收的線程數。通常情況下可以和CPU數量相等,但在CPU數量較多的情況下,設置相對較小的數值也是合理的。

-XX:MaxGCPauseMillis:設置最大垃圾收集停頓時間。他的值是一個大於0的整數。收集器在工作時,會調整Java堆大小或者其他參數,儘可能把停頓時間控制在MaxGCPauseMillis以內。

-XX:GCTimeRatio:設置吞吐量大小。它是0-100的整數。假設GCTimeRatio的值爲n,那麼系統將花費不超過1/(1+n)的時間用於垃圾收集。

-XX:+UseAdaptiveSizePolicy:打開自適應GC策略。在這種模式下,新生代的大小、eden和survivor的比例、晉升老年代的對象年齡等參數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。

-XX:+UseConcMarkSweepGC:新生代使用並行收集器,老年代使用CMS併發收集器

-XX:ParallelCMSThreads:設定CMS的線程數量

-XX:CMSInitiatingOccupancyFraction:設置CMS收集器在老年代空間被使用多少後觸發,默認爲68%

-XX:+UseCMSCompactAtFullCollection:設置CMS收集器完成垃圾收集後是否要進行一次內存碎片的整理

-XX:CMSFullGCsBeforeCompaction:設定進行多少次CMS垃圾回收後,進行一次內存壓縮

-XX:+CMSClassUnloadingEnabled:允許對類元數據區進行回收

-XX:CMSInitiatingPermOccupancyFraction:當永久區佔用率達到這一百分比時,啓動CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)

-XX:UseCMSInitiatingOccupancyOnlyn:表示只在到達閾值的時候才進行CMS回收

-XX:+CMSIncrementalMode:使用增量模式,比較適合單CPU。增量模式在JDK 8中標記爲廢棄,並將在JDK 9中徹底移除。

-XX:+UseG1GC:表示使用g1收集器,g1收集器不同於其它收集器,不區分年輕代和老年代。

    實際參數設置

        在實際中對jvm的調優參數設置,以tomcat爲例,在bin文件下創建setenv.bat(window操作系統)文件,然後在文件輸入以下相應內容格式即可:

setjava_home=D:\Program Files\Java\jdk1.7.0_80

setjava_opts=%java_opts% -server -Xms1024M -Xmx1024M -Xss1M

rem-XX:MetaspaceSize=100M -XX:MaxMetaspaceSize=100M

 

        

 


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