文章目錄
- 1. JVM優化的作用
- 2. 如何監控JVM狀态
-
-
- 2.1 我們以管理者身份運作**DOS**
- 2.2 輸入jvisualvm,将Java VisualVM啟動
-
- 3. JVM的組成
-
-
- 3.1 從圖上可以看到,大緻分為以下元件:
- 3.2 類加載器子系統
-
- 3.21類加載的過程
- 3.22類與類加載器
- 3.23雙親委派機制
- 4. 運作時資料區
-
- 4.1程式計數器
- 4.2java虛拟機棧
- 4.3本地方法棧
- 4.4方法區
- 4.5堆記憶體
- 4.6元空間
- 5. 執行引擎
-
- 5.1 即時編譯器(JIT,Just-In-Time Compiler)
- 5.2 JVM記憶體溢出
-
- 5.21 堆記憶體溢出
- 5.22 虛拟機棧/本地方法棧溢出
-
- 5.23 方法區溢出
- 5.24 本機直接記憶體溢出
-
- 4. JVM垃圾回收
-
- 4.1 判斷對象是否已死
-
- (1)引用計數算法
- (2)可達性分析算法
- (3)方法區回收
- 4.2 常用的垃圾算法
-
- (1)标記-清除算法:
- (2)複制算法:
- (3)标記-整理算法:
- (4)分代收集算法:
-
- 在這些區域的垃圾回收大概有如下幾種情況:
- 4.3 選擇垃圾收集的時間
- 4.4 常見的垃圾收集器
-
- (1)Serial 收集器
- (2)ParNew 收集器
- (3)Parallel Scavenge 收集器
- (4)Serial Old收集器
- (5)CMS(Concurrent Mark Sweep) 收集器
- (6)Parallel Old 收集器
- (7)G1 收集器
- 5.JVM優化
-
- 5.1JIT優化
- 5.2 JVM記憶體分區優化
- 5.3 JVM調優經驗
- 5.4 常用JVM參數參考:
- 5.5 設定jvm參數的幾種方式
1. JVM優化的作用
我們的Java代碼都是運作在JVM中的,而部署的硬體及應用場景有所不同時,仍然采用預設的配置不見得能起到最好的效果,甚至可能會導緻運作效率更差,又或者面臨高并發情況下,想讓程式平穩順暢的運作,是以我們需要針對實際的需要來進行優化.
2. 如何監控JVM狀态
我們隻知道有JVM的存在,但它的運作對于我們來說感覺像是摸不着看不見的,是以我們需要借助工具來監控它的一個實時狀态,
就像Windows的性能螢幕一樣,JDK也有自己的可視化工具.
2.1 我們以管理者身份運作DOS

2.2 輸入jvisualvm,将Java VisualVM啟動
在這裡我們可以看到
本地清單中有多個條目,而一眼也可以看到我們SpringBoot項目的main方法,直接輕按兩下
經過短時間的加載後,得到這樣一個界面
這個是概述頁面,可以得到很多資訊,但對于我們分析JVM的運作還是沒有什麼幫助,是以我們切換到監視頁
監視頁展示的就是實時的JVM資訊
應該還是很直覺的
現在安裝插件,插件的安裝屬于VisualVM的一個重要功能,憑借插件我們可以将這個工具的功能變得更強大。
打開工具->插件
選擇“可用插件”頁
我們在這裡安裝一個Visual GC,友善我們看到記憶體回收以及各個分代的情況
打上勾之後點選安裝,就是正常的next以及同意協定等
網絡不是很穩定,有時候可能需要多嘗試幾次。
安裝完成後我們将目前監控頁關掉,再次打開,就可以看到Profiler後面多了一個Visual GC頁。
在這裡我們可以看到JIT活動時間,類加載活動時間,GC活動時間以及各個分代的情況。
需要注意的是,目前課件使用的JDK版本為1.8,仍然自帶了VisualVM,從1.9開始的版本是沒有自帶的需要額外下載下傳,下載下傳的github位址:
https://visualvm.github.io/download.html
另外,如果開發工具使用的是Intellij IDEA的話,可以下載下傳一個插件,VisualVM Launcher,通過插件啟動可以直接到上述頁面,不用在左邊的條目中尋找自己的項目.
當然也有其他的工具,但這個在可預見的未來都會是主力發展的多合一故障處理工具.是以我們後面将會使用這個工具來分析我們的JVM運作情況,進而優化.而需要優化我們還需要對JVM的組成有進一步的了解.接下來我們來看一下JVM的組成
3. JVM的組成
3.1 從圖上可以看到,大緻分為以下元件:
①.類加載器子系統
②.運作時資料區
方法區 堆 虛拟機棧 本地方法棧 程式計數器
③.執行引擎
④.本地方法庫
而本地庫接口也就是用于調用本地方法的接口,在此我們不細說,主要關注的是上述的4個元件
3.2 類加載器子系統
顧名思義,這是用于類加載的一個子系統.
3.21類加載的過程
類加載的過程包括了加載,驗證,準備,解析和初始化這5個步驟
**①.加載:**找到位元組碼檔案,讀取到記憶體中.類的加載方式分為隐式加載和顯示加載兩種。隐式加載指的是程式在使用new關鍵詞建立對象時,會隐式的調用類的加載器把對應的類加載到jvm中。顯示加載指的是通過直接調用class.forName()方法來把所需的類加載到jvm中。
**②.驗證:**驗證此位元組碼檔案是不是真的是一個位元組碼檔案,畢竟字尾名可以随便改,而内在的身份辨別是不會變的.在确認是一個位元組碼檔案後,還會檢查一系列的是否可運作驗證,中繼資料驗證,位元組碼驗證,符号引用驗證等.Java虛拟機規範對此要求很嚴格,在Java 7的規範中,已經有130頁的描述驗證過程的内容.
**③.準備:**為類中static修飾的變量配置設定記憶體空間并設定其初始值為0或null.可能會有人感覺奇怪,在類中定義一個static修飾的int,并指派了123,為什麼這裡還是指派0.因為這個int的123是在初始化階段的時候才指派的,這裡隻是先把記憶體配置設定好.但如果你的static修飾還加上了final,那麼就會在準備階段就會指派.
**④.解析:**解析階段會将java代碼中的符号引用替換為直接引用.比如引用的是一個類,我們在代碼中隻有全限定名來辨別它,在這個階段會找到這個類加載到記憶體中的位址.
⑤.初始化:如剛才準備階段所說的,這個階段就是對變量的指派的階段.
3.22類與類加載器
每一個類,都需要和它的類加載器一起确定其在JVM中的唯一性.換句話來說,不同類加載器加載的同一個位元組碼檔案,得到的類都不相等.我們可以通過預設加載器去加載一個類,然後new一個對象,再通過自己定義的一個類加載器,去加載同一個位元組碼檔案,拿前面得到的對象去instanceof,會得到的結果是false.
3.23雙親委派機制
類加載器一般有4種,其中前3種是必然存在的
**①.啟動類加載器:**加載<JAVA_HOME>\lib下的
**②.擴充類加載器:**加載<JAVA_HOME>\lib\ext下的
**③.應用程式類加載器:**加載Classpath下的
④.自定義類加載器
而雙親委派機制是如何運作的呢?
我們以應用程式類加載器舉例,它在需要加載一個類的時候,不會直接去嘗試加載,而是委托上級的擴充類加載器去加載,而擴充類加載器也是委托啟動類加載器去加載.
啟動類加載器在自己的搜尋範圍内沒有找到這麼一個類,表示自己無法加載,就再讓擴充類加載器去加載,同樣的,擴充類加載器在自己的搜尋範圍内找一遍,如果還是沒有找到,就委托應用程式類加載器去加載.如果最終還是沒找到,那就會直接抛出異常了.
而為什麼要這麼麻煩的從下到上,再從上到下呢?
這是為了安全着想,保證按照優先級加載.如果使用者自己編寫一個名為java.lang.Object的類,放到自己的Classpath中,沒有這種優先級保證,應用程式類加載器就把這個當做Object加載到了記憶體中,進而會引發一片混亂.而憑借這種雙親委派機制,先一路向上委托,啟動類加載器去找的時候,就把正确的Object加載到了記憶體中,後面再加載自行編寫的Object的時候,是不會加載運作的.
4. 運作時資料區
運作時資料區分為虛拟機棧,本地方法棧,堆區,方法區和程式計數器.
4.1程式計數器
程式計數器是線程私有的,雖然名字叫計數器,但主要用途還是用來确定指令的執行順序,比如循環,分支,跳轉,異常捕獲等.而JVM對于多線程的實作是通過輪流切換線程實作的,是以為了保證每個線程都能按正确順序執行,将程式計數器作為線程私有.程式計數器是唯一一個JVM沒有規定任何OOM的區塊.
oom out of memory
程式計數器是一塊非常小的記憶體空間,可以看做是目前線程執行位元組碼的行号訓示器,每個線程都有一個獨立的程式計數器,是以程式計數器是線程私有的一塊空間,此外,程式計數器是Java虛拟機規定的唯一不會發生記憶體溢出的區域。
4.2java虛拟機棧
Java虛拟機棧也是線程私有的,每個方法執行都會建立一個棧幀,局部變量就存放在棧幀中,還有一些其他的動态連結之類的.通常有兩個錯誤會跟這個有關系,一個是StackOverFlowError,一個是OOM(OutOfMemoryError).前者是因為線程請求棧深度超出虛拟機所允許的範圍,後者是動态擴充棧的大小的時候,申請不到足夠的記憶體空間.而前者提到的棧深度,也就是剛才說到的每個方法會建立一個棧幀,棧幀從開始執行方法時壓入Java虛拟機棧,執行完的時候彈出棧.當壓入的棧幀太多了,就會報出這個StackOverflowError.
虛拟機會為每個線程配置設定一個虛拟機棧,每個虛拟機棧中都有若幹個棧幀,每個棧幀中存儲了局部變量表、操作數棧、動态連結、傳回位址等。一個棧幀就對應Java代碼中的一個方法,當線程執行到一個方法時,就代表這個方法對應的棧幀已經進入虛拟機棧并且處于棧頂的位置,每一個Java方法從被調用到執行結束,就對應了一個棧幀從入棧到出棧的過程。
4.3本地方法棧
本地方法棧與虛拟機棧的差別是,虛拟機棧執行的是Java方法,本地方法棧執行的是本地方法(Native Method),其他基本上一緻,在HotSpot中直接把本地方法棧和虛拟機棧合二為一,這裡暫時不做過多叙述。
https://xiaomogui.iteye.com/blog/857821
4.4方法區
方法區主要用于存儲虛拟機加載的類資訊、常量、靜态變量,以及編譯器編譯後的代碼等資料。在jdk1.7及其之前,方法區是堆的一個“邏輯部分”(一片連續的堆空間),但為了與堆做區分,方法區還有個名字叫“非堆”,也有人用“永久代”(HotSpot對方法區的實作方法)來表示方法區。
從jdk1.7已經開始準備“去永久代”的規劃,jdk1.7的HotSpot中,已經把原本放在方法區中的靜态變量、字元串常量池等移到堆記憶體中,(常量池除字元串常量池還有class常量池等),這裡隻是把字元串常量池移到堆記憶體中;在jdk1.8中,方法區已經不存在,原方法區中存儲的類資訊、編譯後的代碼資料等已經移動到了元空間(MetaSpace)中,元空間并沒有處于堆記憶體上,而是直接占用的本地記憶體(NativeMemory)。
去永久代的原因有:
(1)字元串存在永久代中,容易出現性能問題和記憶體溢出。
(2)類及方法的資訊等比較難确定其大小,是以對于永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導緻老年代溢出。
(3)永久代會為 GC 帶來不必要的複雜度,并且回收效率偏低。
gc garbage collection
4.5堆記憶體
堆記憶體
堆和方法區一樣(确切來說JVM規範中方法區就是堆的一個邏輯分區),就是一個所有線程共享的,存放對象的區域,也是GC的主要區域.其中的分區分為新生代,老年代.新生代中又可以細分為一個Eden,兩個Survivor區(From,To).Eden中存放的是通過new 或者newInstance方法建立出來的對象,絕大多數都是很短命的.正常情況下經曆一次gc之後,存活的對象會轉入到其中一個Survivor區,然後再經曆預設15次的gc,就轉入到老年代.這是正常狀态下,在Survivor區已經滿了的情況下,JVM會依據擔保機制将一些對象直接放入老年代。
堆記憶體主要用于存放對象和數組,它是JVM管理的記憶體中最大的一塊區域,堆記憶體和方法區都被所有線程共享,在虛拟機啟動時建立。在垃圾收集的層面上來看,由于現在收集器基本上都采用分代收集算法,是以堆還可以分為新生代(YoungGeneration)和老年代(OldGeneration),新生代還可以分為Eden、From Survivor、To Survivor
4.6元空間
上面說到,jdk1.8中,已經不存在永久代(方法區),替代它的一塊空間叫做“元空間”,和永久代類似,都是JVM規範對方法區的實作,但是元空間并不在虛拟機中,而是使用本地記憶體,元空間的大小僅受本地記憶體限制,但可以通過-XX:MetaspaceSize和-XX:MaxMetaspaceSize來指定元空間的大小
5. 執行引擎
執行引擎包含即時編譯器(JIT)和垃圾回收器(GC),對即時編譯器簡單介紹一下,主要重點在于垃圾回收器.
5.1 即時編譯器(JIT,Just-In-Time Compiler)
看到這個東西的存在可能有些人會感到疑問,不是通過javac指令就把我們的java代碼編譯成位元組碼檔案了嗎,這個即時編譯器又是幹嘛的?
我們需要明确一個概念就是,計算機實際上隻認識0和1,這種由0和1組成的指令集稱之為”機器碼”,而且會根據平台不同而有所不同,可讀性和可移植性極差.我們的位元組碼檔案包含的并不是機器碼,不能由計算機直接運作,而需要JVM”解釋”執行.JVM将位元組碼檔案中所寫的指令解釋成一個個計算機操作指令,再通知計算機進行運算.
JIT并不是Java虛拟機規範定義中規定必須存在的.但它又是JVM性能重要影響因素之一.
在上面的内容裡,提到了HotSpot這麼一個名字,它是我們一直使用的這款虛拟機的名稱.HotSpot中文意思是”熱點”,而HotSpot VM的特點之一也就是可以探測并優化熱點代碼,JIT就是它進行優化的方式.
HotSpot通過計數以及其他方式,監測到某些方法或者某些代碼塊執行的頻率很高,就會将其編譯成為平台相關的機器碼,甚至于在保證結果的情況下通過優化執行順序等方式進行優化,這種機器碼的執行效率比解釋執行要高出很多.而編譯完成後,會通過”棧上替換”等方式進行動态的替換,比如循環執行,循環一次JIT的計數器就+1,到了門檻值的時候就開始編譯重複執行的代碼,同時為了不影響系統的運作,原來的解釋執行仍然繼續,直到在第N次循環時,編譯完成,會在N+1次執行前替換成編譯後的機器碼執行.
計數器分為兩種,一種方法調用計數器,一種回邊計數器。
方法計數器就是用于統計方法的直接調用,而回邊計數器用于循環代碼的技術。檢測的是頻率,是以他們的計數值不會一直累加,而是在一定時間段内疊加,而超過時間段還沒有達到門檻值,就減半。這個減半稱為“熱度衰減”,而這個時間段被稱為“半衰周期”
但編譯成為機器碼需要時間,會導緻JVM啟動時間變長,記憶體消耗也會增加.是以需要根據實際情況權衡,在啟動時附加指令選擇執行模式.
①.純解釋執行模式:-Xint
②.純編譯執行模式:-Xcomp
③.混合模式:預設
JIT包含兩種編譯器,Client Compiler,Server Compiler.
Client Compiler,就是俗稱的C1編譯器.Server Compiler也就是俗稱的C2編譯器.JVM會根據版本及主控端的硬體性能來自動選擇,也可以通過附加指令”-client”或者”-server”手動選擇.
C1編譯器編譯速度快,但編譯後的品質可靠,但性能優化程度不高.
C2編譯器編譯速度慢,但編譯後的性能優化程度很高,有時候會根據性能的監控情況采取”激進”優化.當然,這種激進優化如果失敗了,仍然會”逆優化”回退到解釋執行來保證代碼的正常運作.
5.2 JVM記憶體溢出
5.21 堆記憶體溢出
堆記憶體中主要存放對象、數組等,隻要不斷地建立這些對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾收集回收機制清除這些對象,當這些對象所占空間超過最大堆容量時,就會産生OutOfMemoryError的異常。堆記憶體異常示例如下:
/**
* 設定最大堆最小堆:-Xms20m -Xmx20m
* 運作時,不斷在堆中建立OOMObject類的執行個體對象,且while執行結束之前,GC Roots(代碼中的oomObjectList)到對象(每一個OOMObject對象)之間有可達路徑,垃圾收集器就無法回收它們,最終導緻記憶體溢出。
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> oomObjectList = new ArrayList<>();
while (true) {
oomObjectList.add(new OOMObject());
}
}
}
運作後會報異常,在堆棧資訊中可以看到 java.lang.OutOfMemoryError: Java heap space 的資訊,說明在堆記憶體空間産生記憶體溢出的異常。
新産生的對象最初配置設定在新生代,新生代滿後會進行一次Minor GC,如果Minor GC後空間不足會把該對象和新生代滿足條件的對象放入老年代,老年代空間不足時會進行Full GC,之後如果空間還不足以存放新對象則抛出OutOfMemoryError異常。常見原因:記憶體中加載的資料過多如一次從資料庫中取出過多資料;集合對對象引用過多且使用完後沒有清空;代碼中存在死循環或循環産生過多重複對象;堆記憶體配置設定不合理;網絡連接配接問題、資料庫問題等。
5.22 虛拟機棧/本地方法棧溢出
(1)StackOverflowError:當線程請求的棧的深度大于虛拟機所允許的最大深度,則抛出StackOverflowError,簡單了解就是虛拟機棧中的棧幀數量過多(一個線程嵌套調用的方法數量過多)時,就會抛出StackOverflowError異常。最常見的場景就是方法無限遞歸調用,如下:
/**
* 設定每個線程的棧大小:-Xss256k
* 運作時,不斷調用doSomething()方法,main線程不斷建立棧幀并入棧,導緻棧的深度越來越大,最終導緻棧溢出。
*/
public class StackSOF {
private int stackLength=1;
public void doSomething(){
stackLength++;
doSomething();
}
public static void main(String[] args) {
StackSOF stackSOF=new StackSOF();
try {
stackSOF.doSomething();
}catch (Throwable e){//注意捕獲的是Throwable
System.out.println("棧深度:"+stackSOF.stackLength);
throw e;
}
}
}
上述代碼執行後抛出:Exception in thread “Thread-0” java.lang.StackOverflowError的異常。
OutOfMemoryError:如果虛拟機在擴充棧時無法申請到足夠的記憶體空間,則抛出OutOfMemoryError。我們可以這樣了解,虛拟機中可以供棧占用的空間≈可用實體記憶體 - 最大堆記憶體 - 最大方法區記憶體,比如一台機器記憶體為4G,系統和其他應用占用2G,虛拟機可用的實體記憶體為2G,最大堆記憶體為1G,最大方法區記憶體為512M,那可供棧占有的記憶體大約就是512M,假如我們設定每個線程棧的大小為1M,那虛拟機中最多可以建立512個線程,超過512個線程再建立就沒有空間可以給棧了,就報OutOfMemoryError異常了。
棧上能夠産生OutOfMemoryError的示例如下:
/**
* 設定每個線程的棧大小:-Xss2m
* 運作時,不斷建立新的線程(且每個線程持續執行),每個線程對一個一個棧,最終沒有多餘的空間來為新的線程配置設定,導緻OutOfMemoryError
*/
public class StackOOM {
private static int threadNum = 0;
public void doSomething() {
try {
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final StackOOM stackOOM = new StackOOM();
try {
while (true) {
threadNum++;
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
stackOOM.doSomething();
}
});
thread.start();
}
} catch (Throwable e) {
System.out.println("目前活動線程數量:" + threadNum);
throw e;
}
}
}
上述代碼運作後會報異常,在堆棧資訊中可以看到 java.lang.OutOfMemoryError: unable to create new native thread的資訊,無法建立新的線程,說明是在擴充棧的時候産生的記憶體溢出異常。
總結:線上程較少的時候,某個線程請求深度過大,會報StackOverflow異常,解決這種問題可以适當加大棧的深度(增加棧空間大小),也就是把-Xss的值設定大一些,但一般情況下是代碼問題的可能性較大;在虛拟機産生線程時,無法為該線程申請棧空間了,會報OutOfMemoryError異常,解決這種問題可以适當減小棧的深度,也就是把-Xss的值設定小一些,每個線程占用的空間小了,總空間一定就能容納更多的線程,但是作業系統對一個程序的線程數有限制,經驗值在3000~5000左右。在jdk1.5之前-Xss預設是256k,jdk1.5之後預設是1M,這個選項對系統硬性還是蠻大的,設定時要根據實際情況,謹慎操作。
5.23 方法區溢出
方法區主要用于存儲虛拟機加載的類資訊、常量、靜态變量,以及編譯器編譯後的代碼等資料,是以方法區溢出的原因就是沒有足夠的記憶體來存放這些資料。
由于在jdk1.6之前字元串常量池是存在于方法區中的,是以基于jdk1.6之前的虛拟機,可以通過不斷産生不一緻的字元串(同時要保證和GC Roots之間保證有可達路徑)來模拟方法區的OutOfMemoryError異常;但方法區還存儲加載的類資訊,是以基于jdk1.7的虛拟機,可以通過動态不斷建立大量的類來模拟方法區溢出。
5.24 本機直接記憶體溢出
本機直接記憶體(DirectMemory)并不是虛拟機運作時資料區的一部分,也不是Java虛拟機規範中定義的記憶體區域,但Java中用到NIO相關操作時(比如ByteBuffer的allocteDirect方法申請的是本機直接記憶體),也可能會出現記憶體溢出的異常。
4. JVM垃圾回收
垃圾回收,就是通過垃圾收集器把記憶體中沒用的對象清理掉。
垃圾回收涉及到的内容有:
1、判斷對象是否已死;
2、選擇垃圾收集算法;
3、選擇垃圾收集的時間;
4、選擇适當的垃圾收集器清理垃圾(已死的對象)。
4.1 判斷對象是否已死
判斷對象是否已死就是找出哪些對象是已經死掉的,以後不會再用到的,就像地上有廢紙、飲料瓶和百元大鈔,掃地前要先判斷出地上廢紙和飲料瓶是垃圾,百元大鈔不是垃圾。判斷對象是否已死有引用計數算法和可達性分析算法。
(1)引用計數算法
給每一個對象添加一個引用計數器,每當有一個地方引用它時,計數器值加1;每當有一個地方不再引用它時,計數器值減1,這樣隻要計數器的值不為0,就說明還有地方引用它,它就不是無用的對象。如下圖,對象2有1個引用,它的引用計數器值為1,對象1有兩個地方引用,它的引用計數器值為2 。
這種方法看起來非常簡單,但目前許多主流的虛拟機都沒有選用這種算法來管理記憶體,原因就是當某些對象之間互相引用時,無法判斷出這些對象是否已死,如下圖,對象1和對象2都沒有被堆外的變量引用,而是被對方互相引用,這時他們雖然沒有用處了,但是引用計數器的值仍然是1,無法判斷他們是死對象,垃圾回收器也就無法回收。
(2)可達性分析算法
了解可達性分析算法之前先了解一個概念——GC Roots,垃圾收集的起點,可以作為GC Roots的有虛拟機棧中本地變量表中引用的對象、方法區中靜态屬性引用的對象、方法區中常量引用的對象、本地方法棧中JNI(Native方法)引用的對象。
當一個對象到GC Roots沒有任何引用鍊相連(GC Roots到這個對象不可達)時,就說明此對象是不可用的,是死對象。如下圖:object1、object2、object3、object4和GC Roots之間有可達路徑,這些對象不會被回收,但object5、object6、object7到GC Roots之間沒有可達路徑,這些對象就被判了死刑。
上面被判了死刑的對象(object5、object6、object7)并不是必死無疑,還有挽救的餘地。進行可達性分析後對象和GC Roots之間沒有引用鍊相連時,對象将會被進行一次标記,接着會判斷如果對象沒有覆寫Object的finalize()方法或者finalize()方法已經被虛拟機調用過,那麼它們就會被行刑(清除);如果對象覆寫了finalize()方法且還沒有被調用,則會執行finalize()方法中的内容,是以在finalize()方法中如果重新與GC Roots引用鍊上的對象關聯就可以拯救自己,但是一般不建議這麼做.
(3)方法區回收
上面說的都是對堆記憶體中對象的判斷,方法區中主要回收的是廢棄的常量和無用的類。
判斷常量是否廢棄可以判斷是否有地方引用這個常量,如果沒有引用則為廢棄的常量。
判斷類是否廢棄需要同時滿足如下條件:
該類所有的執行個體已經被回收(堆中不存在任何該類的執行個體)
加載該類的ClassLoader已經被回收
該類對應的java.lang.Class對象在任何地方沒有被引用(無法通過反射通路該類的方法)
4.2 常用的垃圾算法
常用的垃圾回收算法有三種:标記-清除算法、複制算法、标記-整理算法。
(1)标記-清除算法:
分為标記和清除兩個階段,首先标記出所有需要回收的對象,标記完成後統一回收所有被标記的對象,如下圖。
缺點:标記和清除兩個過程效率都不高;标記清除之後會産生大量不連續的記憶體碎片。
(2)複制算法:
把記憶體分為大小相等的兩塊,每次存儲隻用其中一塊,當這一塊用完了,就把存活的對象全部複制到另一塊上,同時把使用過的這塊記憶體空間全部清理掉,往複循環,如下圖。
**缺點:**實際可使用的記憶體空間縮小為原來的一半,比較适合
(3)标記-整理算法:
先對可用的對象進行标記,然後所有被标記的對象向一段移動,最後清除可用對象邊界以外的記憶體,如下圖。
(4)分代收集算法:
把堆記憶體分為新生代和老年代,新生代又分為Eden區、From Survivor和To Survivor。一般新生代中的對象基本上都是朝生夕滅的,每次隻有少量對象存活,是以采用複制算法,隻需要複制那些少量存活的對象就可以完成垃圾收集;老年代中的對象存活率較高,就采用标記-清除和标記-整理算法來進行回收。
在這些區域的垃圾回收大概有如下幾種情況:
新生代使用時minor gc 老年代使用的full gc
- 大多數情況下,新的對象都配置設定在Eden區,當Eden區沒有空間進行配置設定時,将進行一次Minor GC,清理Eden區中的無用對象。清理後,Eden和From Survivor中的存活對象如果小于To Survivor的可用空間則進入To Survivor,否則直接進入老年代);Eden和From Survivor中還存活且能夠進入To Survivor的對象年齡增加1歲(虛拟機為每個對象定義了一個年齡計數器,每執行一次Minor GC年齡加1),當存活對象的年齡到達一定程度(預設15歲)後進入老年代,可以通過-XX:MaxTenuringThreshold來設定年齡的值。
- 當進行了Minor GC後,Eden還不足以為新對象配置設定空間(那這個新對象肯定很大),新對象直接進入老年代。
- 占To Survivor空間一半以上且年齡相等的對象,大于等于該年齡的對象直接進入老年代,比如Survivor空間是10M,有幾個年齡為4的對象占用總空間已經超過5M,則年齡大于等于4的對象都直接進入老年代,不需要等到MaxTenuringThreshold指定的歲數。
- 在進行Minor GC之前,會判斷老年代最大連續可用空間是否大于新生代所有對象總空間,如果大于,說明Minor GC是安全的,否則會判斷是否允許擔保失敗,如果允許,判斷老年代最大連續可用空間是否大于曆次晉升到老年代的對象的平均大小,如果大于,則執行Minor GC,否則執行Full GC。
- 當在java代碼裡直接調用System.gc()時,會建議JVM進行Full GC,但一般情況下都會觸發Full GC,一般不建議使用,盡量讓虛拟機自己管理GC的政策。
- **永久代(方法區)**中用于存放類資訊,jdk1.6及之前的版本永久代中還存儲常量、靜态變量等,當永久代的空間不足時,也會觸發Full GC,如果經過Full GC還無法滿足永久代存放新資料的需求,就會抛出永久代的記憶體溢出異常。
-
大對象(需要大量連續記憶體的對象)例如很長的數組,會直接進入老年代,如果老年代沒有足夠的連續大空間來存放,則會進行Full GC。
Minor GC和Full GC
在說這兩種回收的差別之前,我先來說一個概念,“Stop-The-World”。
如字面意思,每次垃圾回收的時候,都會将整個JVM暫停,回收完成後再繼續。如果一邊增加廢棄對象,一邊進行垃圾回收,完成工作似乎就變得遙遙無期了。
而一般來說,我們把新生代的回收稱為Minor GC,Minor意思是次要的,新生代的回收一般回收很快,采用複制算法,造成的暫停時間很短。而Full GC一般是老年代的回收,并伴随至少一次的Minor GC,新生代和老年代都回收,而老年代采用标記-整理算法,這種GC每次都比較慢,造成的暫停時間比較長,通常是Minor GC時間的10倍以上。
是以很明顯,我們需要盡量通過Minor GC來回收記憶體,而盡量少的觸發Full GC。畢竟系統運作一會兒就要因為GC卡住一段時間,再加上其他的同步阻塞,整個系統給人的感覺就是又卡又慢。
4.3 選擇垃圾收集的時間
當程式運作時,各種資料、對象、線程、記憶體等都時刻在發生變化,當下達垃圾收集指令後就立刻進行收集嗎?肯定不是。這裡來了解兩個概念:安全點(safepoint)和安全區(safe region)。
安全點:從線程角度看,安全點可以了解為是在代碼執行過程中的一些特殊位置,當線程執行到安全點的時候,說明虛拟機目前的狀态是安全的,如果有需要,可以在這裡暫停使用者線程。當垃圾收集時,如果需要暫停目前的使用者線程,但使用者線程當時沒在安全點上,則應該等待這些線程執行到安全點再暫停。舉個例子,媽媽在掃地,兒子在吃西瓜(瓜皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽等一下,讓我吃完這塊再掃。”兒子吃完這塊西瓜把瓜皮扔到地上後就是一個安全點,媽媽可以繼續掃地(垃圾收集器可以繼續收集垃圾)。理論上,解釋器的每條位元組碼的邊界上都可以放一個安全點,實際上,安全點基本上以“是否具有讓程式長時間執行的特征”為标準進行標明。
安全區:安全點是相對于運作中的線程來說的,對于如sleep或blocked等狀态的線程,收集器不會等待這些線程被配置設定CPU時間,這時候隻要線程處于安全區中,就可以算是安全的。安全區就是在一段代碼片段中,引用關系不會發生變化,可以看作是被擴充、拉長了的安全點。還以上面的例子說明,媽媽在掃地,兒子在吃西瓜(瓜皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽你繼續掃地吧,我還得吃10分鐘呢!”兒子吃瓜的這段時間就是安全區,媽媽可以繼續掃地(垃圾收集器可以繼續收集垃圾)。
4.4 常見的垃圾收集器
現在常見的垃圾收集器有如下幾種
**新生代收集器:**Serial、ParNew、Parallel Scavenge
**老年代收集器:**Serial Old、CMS、Parallel Old
**堆記憶體垃圾收集器:**G1
每種垃圾收集器之間有連線,表示他們可以搭配使用。
(1)Serial 收集器
Serial是一款用于新生代的單線程收集器,采用複制算法進行垃圾收集。Serial進行垃圾收集時,不僅隻用一條線程執行垃圾收集工作,它在收集的同時,所有的使用者線程必須暫停(Stop The World)。就比如媽媽在家打掃衛生的時候,肯定不會邊打掃邊讓兒子往地上亂扔紙屑,否則一邊制造垃圾,一遍清理垃圾,這活啥時候也幹不完。
如下是Serial收集器和Serial Old收集器結合進行垃圾收集的示意圖,當使用者線程都執行到安全點時,所有線程暫停執行,Serial收集器以單線程,采用複制算法進行垃圾收集工作,收集完之後,使用者線程繼續開始執行。
**适用場景:**Client模式(桌面應用);單核伺服器。可以用-XX:+UserSerialGC來選擇Serial作為新生代收集器。
(2)ParNew 收集器
ParNew就是一個Serial的多線程版本,其它與Serial并無差別。ParNew在單核CPU環境并不會比Serial收集器達到更好的效果,它預設開啟的收集線程數和CPU數量一緻,可以通過-XX:ParallelGCThreads來設定垃圾收集的線程數。
如下是ParNew收集器和Serial Old收集器結合進行垃圾收集的示意圖,當使用者線程都執行到安全點時,所有線程暫停執行,ParNew收集器以多線程,采用複制算法進行垃圾收集工作,收集完之後,使用者線程繼續開始執行。
**适用場景:**多核伺服器;與CMS收集器搭配使用。當使用-XX:+UserConcMarkSweepGC來選擇CMS作為老年代收集器時,新生代收集器預設就是ParNew,也可以用-XX:+UseParNewGC來指定使用ParNew作為新生代收集器。
(3)Parallel Scavenge 收集器
Parallel Scavenge也是一款用于新生代的多線程收集器,與ParNew的不同之處是,ParNew的目标是盡可能縮短垃圾收集時使用者線程的停頓時間,Parallel Scavenge的目标是達到一個可控制的吞吐量。吞吐量就是CPU執行使用者線程的的時間與CPU執行總時間的比值【吞吐量=運作使用者代代碼時間/(運作使用者代碼時間+垃圾收集時間)】,比如虛拟機一共運作了100分鐘,其中垃圾收集花費了1分鐘,那吞吐量就是99% 。比如下面兩個場景,垃圾收集器每100秒收集一次,每次停頓10秒,和垃圾收集器每50秒收集一次,每次停頓時間7秒,雖然後者每次停頓時間變短了,但是總體吞吐量變低了,CPU總體使用率變低了。
可以通過-XX:MaxGCPauseMillis來設定收集器盡可能在多長時間内完成記憶體回收,可以通過-XX:GCTimeRatio來精确控制吞吐量。
如下是Parallel收集器和Parallel Old收集器結合進行垃圾收集的示意圖,在新生代,當使用者線程都執行到安全點時,所有線程暫停執行,ParNew收集器以多線程,采用複制算法進行垃圾收集工作,收集完之後,使用者線程繼續開始執行;在老年代,當使用者線程都執行到安全點時,所有線程暫停執行,Parallel Old收集器以多線程,采用标記整理算法進行垃圾收集工作。
**适用場景:**注重吞吐量,高效利用CPU,需要高效運算且不需要太多互動。可以使用-XX:+UseParallelGC來選擇Parallel Scavenge作為新生代收集器,jdk7、jdk8預設使用Parallel Scavenge作為新生代收集器。
(4)Serial Old收集器
Serial Old收集器是Serial的老年代版本,同樣是一個單線程收集器,采用标記-整理算法。
如下圖是Serial收集器和Serial Old收集器結合進行垃圾收集的示意圖:
**适用場景:**Client模式(桌面應用);單核伺服器;與Parallel Scavenge收集器搭配;作為CMS收集器的後備預案。
(5)CMS(Concurrent Mark Sweep) 收集器
CMS收集器是一種以最短回收停頓時間為目标的收集器,以“最短使用者線程停頓時間”著稱。整個垃圾收集過程分為4個步驟
**① 初始标記:**标記一下GC Roots能直接關聯到的對象,速度較快
**② 并發标記:**進行GC Roots Tracing,标記出全部的垃圾對象,耗時較長
**③ 重新标記:**修正并發标記階段引使用者程式繼續運作而導緻變化的對象的标記記錄,耗時較短
**④ 并發清除:**用标記-清除算法清除垃圾對象,耗時較長
整個過程耗時最長的并發标記和并發清除都是和使用者線程一起工作,是以從總體上來說,CMS收集器垃圾收集可以看做是和使用者線程并發執行的。
CMS收集器也存在一些缺點:
- 對CPU資源敏感:預設配置設定的垃圾收集線程數為(CPU數+3)/4,随着CPU數量下降,占用CPU資源越多,吞吐量越小
- 無法處理浮動垃圾:在并發清理階段,由于使用者線程還在運作,還會不斷産生新的垃圾,CMS收集器無法在當次收集中清除這部分垃圾。同時由于在垃圾收集階段使用者線程也在并發執行,CMS收集器不能像其他收集器那樣等老年代被填滿時再進行收集,需要預留一部分空間提供使用者線程運作使用。當CMS運作時,預留的記憶體空間無法滿足使用者線程的需要,就會出現“Concurrent Mode Failure”的錯誤,這時将會啟動後備預案,臨時用Serial Old來重新進行老年代的垃圾收集。
- 因為CMS是基于标記-清除算法,是以垃圾回收後會産生空間碎片,可以通過-XX:UserCMSCompactAtFullCollection開啟碎片整理(預設開啟),在CMS進行Full GC之前,會進行記憶體碎片的整理。還可以用-XX:CMSFullGCsBeforeCompaction設定執行多少次不壓縮(不進行碎片整理)的Full GC之後,跟着來一次帶壓縮(碎片整理)的Full GC。
- 适用場景:重視伺服器響應速度,要求系統停頓時間最短。可以使用-XX:+UserConMarkSweepGC來選擇CMS作為老年代收集器。
(6)Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge的老年代版本,是一個多線程收集器,采用标記-整理算法。可以與Parallel Scavenge收集器搭配,可以充分利用多核CPU的計算能力。
**适用場景:**與Parallel Scavenge收集器搭配使用;注重吞吐量。jdk7、jdk8預設使用該收集器作為老年代收集器,使用 -XX:+UseParallelOldGC來指定使用Paralle Old收集器。
(7)G1 收集器
G1 收集器是jdk1.7才正式引用的商用收集器,現在已經成為jdk9預設的收集器。前面幾款收集器收集的範圍都是新生代或者老年代,G1進行垃圾收集的範圍是整個堆記憶體,它采用“化整為零”的思路,把整個堆記憶體劃分為多個大小相等的獨立區域(Region),在G1收集器中還保留着新生代和老年代的概念,它們分别都是一部分Region,如下圖:
每一個方塊就是一個區域,每個區域可能是Eden、Survivor、老年代,每種區域的數量也不一定。JVM啟動時會自動設定每個區域的大小(1M~32M,必須是2的次幂),最多可以設定2048個區域(即支援的最大堆記憶體為32M*2048=64G),假如設定-Xmx8g -Xms8g,則每個區域大小為8g/2048=4M。
為了在GC Roots Tracing的時候避免掃描全堆,在每個Region中,都有一個Remembered Set來實時記錄該區域内的引用類型資料與其他區域資料的引用關系(在前面的幾款分代收集中,新生代、老年代中也有一個Remembered Set來實時記錄與其他區域的引用關系),在标記時直接參考這些引用關系就可以知道這些對象是否應該被清除,而不用掃描全堆的資料。
G1收集器可以“建立可預測的停頓時間模型”,它維護了一個清單用于記錄每個Region回收的價值大小(回收後獲得的空間大小以及回收所需時間的經驗值),這樣可以保證G1收集器在有限的時間内可以獲得最大的回收效率。
如下圖所示,G1收集器收集器收集過程有初始标記、并發标記、最終标記、篩選回收,和CMS收集器前幾步的收集過程很相似:
**① 初始标記:**标記出GC Roots直接關聯的對象,這個階段速度較快,需要停止使用者線程,單線程執行
**② 并發标記:**從GC Root開始對堆中的對象進行可達新分析,找出存活對象,這個階段耗時較長,但可以和使用者線程并發執行
**③ 最終标記:**修正在并發标記階段引使用者程式執行而産生變動的标記記錄
**④ 篩選回收:**篩選回收階段會對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來指定回收計劃(用最少的時間來回收包含垃圾最多的區域,這就是Garbage First的由來——第一時間清理垃圾最多的區塊),這裡為了提高回收效率,并沒有采用和使用者線程并發執行的方式,而是停頓使用者線程。
**适用場景:**要求盡可能可控GC停頓時間;記憶體占用較大的應用。可以用-XX:+UseG1GC使用G1收集器,jdk9預設使用G1收集器。
5.JVM優化
JVM調優目标:使用較小的記憶體占用來獲得較高的吞吐量或者較低的延遲。
程式在上線前的測試或運作中有時會出現一些大大小小的JVM問題,比如cpu load過高、請求延遲、tps降低等,甚至出現記憶體洩漏(每次垃圾收集使用的時間越來越長,垃圾收集頻率越來越高,每次垃圾收集清理掉的垃圾資料越來越少)、記憶體溢出導緻系統崩潰,是以需要對JVM進行調優,使得程式在正常運作的前提下,獲得更高的使用者體驗和運作效率。
這裡有幾個比較重要的名額:
**- 記憶體占用:**程式正常運作需要的記憶體大小。
**- 延遲:**由于垃圾收集而引起的程式停頓時間。
**- 吞吐量:**使用者程式運作時間占使用者程式和垃圾收集占用總時間的比值。
當然,和CAP原則一樣,同時滿足一個程式記憶體占用小、延遲低、高吞吐量是不可能的,程式的目标不同,調優時所考慮的方向也不同,在調優之前,必須要結合實際場景,有明确的的優化目标,找到性能瓶頸,對瓶頸有針對性的優化,最後進行測試,通過各種監控工具确認調優後的結果是否符合目标。
(1)調優可以依賴、參考的資料有系統運作日志、堆棧錯誤資訊、gc日志、線程快照、堆轉儲快照等。
① 系統運作日志:系統運作日志就是在程式代碼中列印出的日志,描述了代碼級别的系統運作軌迹(執行的方法、入參、傳回值等),一般系統出現問題,系統運作日志是首先要檢視的日志。
② 堆棧錯誤資訊:當系統出現異常後,可以根據堆棧資訊初步定位問題所在,比如根據“java.lang.OutOfMemoryError: Java heap space”可以判斷是堆記憶體溢出;根據“java.lang.StackOverflowError”可以判斷是棧溢出;根據“java.lang.OutOfMemoryError: PermGen space”可以判斷是方法區溢出等。
③ GC日志:程式啟動時用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程式運作時把gc的詳細過程記錄下來,或者直接配置“-verbose:gc”參數把gc日志列印到控制台,通過記錄的gc日志可以分析每塊記憶體區域gc的頻率、時間等,進而發現問題,進行有針對性的優化。
④ 線程快照:顧名思義,根據線程快照可以看到線程在某一時刻的狀态,當系統中可能存在請求逾時、死循環、死鎖等情況是,可以根據線程快照來進一步确定問題。通過執行虛拟機自帶的“jstack pid”指令,可以dump出目前程序中線程的快照資訊,更詳細的使用和分析網上有很多例,這篇文章寫到這裡已經很長了就不過多叙述了,貼一篇部落格供參考:http://www.cnblogs.com/kongzhongqijing/articles/3630264.html
⑤ 堆轉儲快照:程式啟動時可以使用 “-XX:+HeapDumpOnOutOfMemory” 和 “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,當程式發生記憶體溢出時,把當時的記憶體快照以檔案形式進行轉儲(也可以直接用jmap指令轉儲程式運作時任意時刻的記憶體快照),事後對當時的記憶體使用情況進行分析。
JVM的優化我們可以從JIT優化,記憶體分區設定優化以及GC選擇優化三個方面入手。
5.1JIT優化
正如前面所說的,在系統啟動的時候,首先Java代碼是解釋執行的,當方法調用次數到達一定的門檻值的時候(client:1500,server:10000),會采用JIT優化編譯。而直接将JVM的啟動設定為-Xcomp并不會有想象中那麼好。沒有足夠的profile(側寫,可以大緻了解為分析結果),優化出來的代碼品質很差,甚至于執行效率還要低于解釋器執行,并且機器碼的大小很容易就超出位元組碼大小的10倍以上。
那麼我們能做的,就是通過附加啟動指令适當的調整這個門檻值或者調整熱度衰減行為,在恰當的時候觸發對代碼進行即時編譯。
**方法計數器門檻值:-**XX:CompileThreshold
回邊計數器門檻值:-XX:OnStackReplacePercentage(這并不是直接調整門檻值,回邊計數器的調整在此僅作簡單介紹,此計數器會根據是Client模式還是Server模式有不同的計算公式)
關閉熱度衰減:-XX:UseCounterDecay
**設定半衰周期:-**XX:CounterHalfLifeTime
而JIT也是一片廣闊的知識海洋,有興趣可以根據以下的優化技術名稱搜尋了解詳情,在此就不贅述了。
5.2 JVM記憶體分區優化
調優工具:
① 用 jps(JVM process Status)可以檢視虛拟機啟動的所有程序、執行主類的全名、JVM啟動參數,比如當執行了JPSTest類中的main方法後(main方法持續執行),執行 jps -l可看到下面的OOMTest類的pid為7480,加上-v參數還可以看到JVM啟動參數。
② 用jstat(JVM Statistics Monitoring Tool)監視虛拟機資訊
jstat -gc pid 500 10 :每500毫秒列印一次Java堆狀況(各個區的容量、使用容量、gc時間等資訊),列印10次
jstat還可以以其他角度監視各區記憶體大小、監視類裝載資訊等,具體可以google jstat的詳細用法。
③ 用jmap(Memory Map for Java)檢視堆記憶體資訊
執行jmap -histo pid可以列印出目前堆中所有每個類的執行個體數量和記憶體占用,如下,class name是每個類的類名([B是byte類型,[C是char類型,[I是int類型),bytes是這個類的所有示例占用記憶體大小,instances是這個類的執行個體數量:
執行jmap -dump 可以轉儲堆記憶體快照到指定檔案,比如執行 jmap -dump:format=b,file=/data/jvm/dumpfile_jmap.hprof 3361 可以把目前堆記憶體的快照轉儲到dumpfile_jmap.hprof檔案中,然後可以對記憶體快照進行分析。
④ 利用jvisualvm分析記憶體資訊(各個區如Eden、Survivor、Old等記憶體變化情況),如果檢視的是遠端伺服器的JVM,程式啟動需要加上如下參數:
“-Dcom.sun.management.jmxremote=true”
“-Djava.rmi.server.hostname=12.34.56.78”
“-Dcom.sun.management.jmxremote.port=18181”
“-Dcom.sun.management.jmxremote.authenticate=false”
“-Dcom.sun.management.jmxremote.ssl=false”
前面說到配置了 “-XX:+HeapDumpOnOutOfMemory” 參數可以在程式發生記憶體溢出時dump出目前的記憶體快照,也可以用jmap指令随時dump出當時記憶體狀态的快照資訊,dump的記憶體快照一般是以.hprof為字尾的二進制格式檔案。
可以直接用 jhat(JVM Heap Analysis Tool) 指令來分析記憶體快照,它的本質實際上内嵌了一個微型的伺服器,可以通過浏覽器來分析對應的記憶體快照,比如執行 jhat -port 9810 -J-Xmx4G /data/jvm/dumpfile_jmap.hprof 表示以9810端口啟動 jhat 内嵌的伺服器:
Reading from /Users/dannyhoo/data/jvm/dumpfile_jmap.hprof…
Dump file created Fri Aug 03 15:48:27 CST 2018
Snapshot read, resolving…
Resolving 276472 objects…
Chasing references, expect 55 dots…
Eliminating duplicate references…
Snapshot resolved.
Started HTTP server on port 9810
Server is ready.
在控制台可以看到伺服器啟動了,通路 http://127.0.0.1:9810/ 可以看到對快照中的每個類進行分析的結果。
jvisualvm也可以分析記憶體快照,在jvisualvm菜單的“檔案”-“裝入”,選擇堆記憶體快照,快照中的資訊就以圖形界面展示出來了,如下,主要可以檢視每個類占用的空間、執行個體的數量和執行個體的詳情等:
5.3 JVM調優經驗
我們依據Java Performance這本書的建議的設定原則進行設定,
Java整個堆大小設定,Xmx 和 Xms設定為老年代存活對象的3-4倍,即FullGC之後的老年代記憶體占用的3-4倍,Xmx和Xms的大小設定為一樣,避免GC後對記憶體的重新配置設定。而Full GC之後的老年代記憶體大小,我們可以通過前面在Visual VM中添加的插件Visual GC檢視。先手動進行一次GC,然後檢視老年代的記憶體占用。
新生代Xmn的設定為老年代存活對象的1-1.5倍。
老年代的記憶體大小設定為老年代存活對象的2-3倍。
JVM配置方面,一般情況可以先用預設配置(基本的一些初始參數可以保證一般的應用跑的比較穩定了),在測試中根據系統運作狀況(會話并發情況、會話時間等),結合gc日志、記憶體監控、使用的垃圾收集器等進行合理的調整,當老年代記憶體過小時可能引起頻繁Full GC,當記憶體過大時Full GC時間會特别長。
那麼JVM的配置比如新生代、老年代應該配置多大最合适呢?答案是不一定,調優就是找答案的過程,實體記憶體一定的情況下,新生代設定越大,老年代就越小,Full GC頻率就越高,但Full GC時間越短;相反新生代設定越小,老年代就越大,Full GC頻率就越低,但每次Full GC消耗的時間越大。建議如下:
-Xms和-Xmx的值設定成相等,堆大小預設為-Xms指定的大小,預設空閑堆記憶體小于40%時,JVM會擴大堆到-Xmx指定的大小;空閑堆記憶體大于70%時,JVM會減小堆到-Xms指定的大小。如果在Full GC後滿足不了記憶體需求會動态調整,這個階段比較耗費資源。
新生代盡量設定大一些,讓對象在新生代多存活一段時間,每次Minor GC 都要盡可能多的收集垃圾對象,防止或延遲對象進入老年代的機會,以減少應用程式發生Full GC的頻率。
老年代如果使用CMS收集器,新生代可以不用太大,因為CMS的并行收集速度也很快,收集過程比較耗時的并發标記和并發清除階段都可以與使用者線程并發執行。
方法區大小的設定,1.6之前的需要考慮系統運作時動态增加的常量、靜态變量等,1.7隻要差不多能裝下啟動時和後期動态加載的類資訊就行。
代碼實作方面,性能出現問題比如程式等待、記憶體洩漏除了JVM配置可能存在問題,代碼實作上也有很大關系:
避免建立過大的對象及數組:過大的對象或數組在新生代沒有足夠空間容納時會直接進入老年代,如果是短命的大對象,會提前出發Full GC。
避免同時加載大量資料,如一次從資料庫中取出大量資料,或者一次從Excel中讀取大量記錄,可以分批讀取,用完盡快清空引用。
當集合中有對象的引用,這些對象使用完之後要盡快把集合中的引用清空,這些無用對象盡快回收避免進入老年代。
可以在合适的場景(如實作緩存)采用軟引用、弱引用,比如用軟引用來為ObjectA配置設定執行個體:SoftReference objectA=new SoftReference(); 在發生記憶體溢出前,會将objectA列入回收範圍進行二次回收,如果這次回收還沒有足夠記憶體,才會抛出記憶體溢出的異常。
避免産生死循環,産生死循環後,循環體内可能重複産生大量執行個體,導緻記憶體空間被迅速占滿。
盡量避免長時間等待外部資源(資料庫、網絡、裝置資源等)的情況,縮小對象的生命周期,避免進入老年代,如果不能及時傳回結果可以适當采用異步處理的方式等。
5.4 常用JVM參數參考:
參數 說明 執行個體
-Xms 初始堆大小,預設實體記憶體的1/64 -Xms512M
-Xmx 最大堆大小,預設實體記憶體的1/4 -Xms2G
-Xmn 新生代記憶體大小,官方推薦為整個堆的3/8 -Xmn512M
-Xss 線程堆棧大小,jdk1.5及之後預設1M,之前預設256k -Xss512k
-XX:NewRatio=n 設定新生代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4 -XX:NewRatio=3
-XX:SurvivorRatio=n 年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:8,表示Eden:Survivor=8:1:1,一個Survivor區占整個年輕代的1/8 -XX:SurvivorRatio=8
-XX:PermSize=n 永久代初始值,預設為實體記憶體的1/64 -XX:PermSize=128M
-XX:MaxPermSize=n 永久代最大值,預設為實體記憶體的1/4 -XX:MaxPermSize=256M
-verbose:class 在控制台列印類加載資訊
-verbose:gc 在控制台列印垃圾回收日志
-XX:+PrintGC 列印GC日志,内容簡單
-XX:+PrintGCDetails 列印GC日志,内容詳細
-XX:+PrintGCDateStamps 在GC日志中添加時間戳
-Xloggc:filename 指定gc日志路徑 -Xloggc:/data/jvm/gc.log
-XX:+UseSerialGC 年輕代設定串行收集器Serial
-XX:+UseParallelGC 年輕代設定并行收集器Parallel Scavenge
-XX:ParallelGCThreads=n設定Parallel Scavenge收集時使用的CPU數。并行收集線程數。 -XX:ParallelGCThreads=4
-XX:MaxGCPauseMillis=n 設定Parallel Scavenge回收的最大時間(毫秒) -XX:MaxGCPauseMillis=100
-XX:GCTimeRatio=n設定Parallel Scavenge垃圾回收時間占程式運作時間的百分比。公式為1/(1+n) -XX:GCTimeRatio=19
-XX:+UseParallelOldGC 設定老年代為并行收集器ParallelOld收集器
-XX:+UseConcMarkSweepGC 設定老年代并發收集器CMS
-XX:+CMSIncrementalMode 設定CMS收集器為增量模式,适用于單CPU情況。
5.5 設定jvm參數的幾種方式
1、內建開發環境下啟動并使用JVM,如eclipse需要修改根目錄檔案eclipse.ini;
2、Windows伺服器下安裝版Tomcat,可使用Tomcat7w.exe工具(tomcat目錄下)和直接修改系統資料庫兩種方式修改Jvm參數;
3、Windows伺服器解壓版Tomcat注冊Windows服務,方法同上;
4、解壓版本的Tomcat, 通過startup.bat啟動tomcat加載配置的,在tomcat 的bin 下catalina.bat 檔案内添加;
5、Linux伺服器Tomcat設定JVM,修改TOMCAT_HOME/bin/catalina.sh;
6、windows環境下配置JAVA_OPTS
如果對你有幫助幫我點個贊把^ ^!!