Java相關內容:JVM

要想進階成Java中級開發工程師,有一些東西總是繞不過去的,Java的知識體系裏,像IO/NIO/AIO,多線程,JVM,網絡編程,數據庫,框架等必須有一定了解才行。最近在準備面試,所以對這些東西也做個記錄。本篇記錄的是JVM相關。

 

注:本篇博客寫得有點沉悶,建議可以看看下面【老劉 碼農翻身】寫的這篇,很佩服能寫成這麼有意思的小故事。

https://mp.weixin.qq.com/s/SsctgcbwhCRbxTnbaZyx_A

 

1.JVM是什麼?

要點:JVM概念/作用 類加載 運行時數據區 垃圾回收

JVM就是Java 虛擬機。主要功能就是將class字節碼文件解釋成機器碼讓計算機能識別運行。我們說Java程序是"一次編譯,導出運行"的原因,就是只要在不同的操作系統上安裝不同的虛擬機後,就能識別我們的class字節碼文件,從而轉換爲不同平臺的機器碼來執行。

具體細節就是,當我們的程序運行時,JVM首先要通過類加載子系統(類加載器)加載所需要的類的字節碼,class文件被jvm裝載以後,最後由執行引擎會配合一些本地方法最終完成class文件的執行。然後在執行的過程中,需要一段內存空間來存儲數據,JVM可以對這段內存空間進行分配和釋放,這段內存空間我們稱爲運行時數據區。

JVM主要就是包含了類加載子系統、執行引擎、運行時數據區這三個內容。還有垃圾回收器和本地方法接口等模塊。

這個過程可以由下面這張圖說明

 

 

2.JVM如何加載字節碼文件?

要點: 類加載過程 類初始化場景 類加載器 雙親委派機制

類加載過程

之前說過,通過類加載子系統可以把我們的class字節碼文件加載到JVM中。那類加載子系統又什麼?

類加載子系統概念:在JAVA虛擬機中,存在着多個類裝載器,稱爲類加載子系統。也就是說實際上是類裝載器把字節碼文件加載到JVM中的。我們先不說類加載器,先說類加載器加載類的過程。類的生命週期是從被加載到虛擬機內存中開始,到卸載出內存結束。對應的過程有 加載----驗證----準備----解析-----初始化----使用-----卸載 共七個階段。從加載到初始化都屬於類加載的過程。下面說一下這五個過程的作用。 (驗證+準備+解析 = 連接)

加載:通過一個類的全限定名查找此類的二進制字節流,並將這些靜態數據轉換成方法區中的運行時數據結構,而且在Java堆中也創建一個java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據。

運行時數據結構就是該二進制字節流代表的類的類信息、常量、靜態變量、靜態方法、即時編譯器編譯後的代碼等

驗證:驗證Class文件的字節流中包含信息符合當前虛擬機要求,並且不會危害虛擬機自身安全。

用命令  javap -v class路徑  可以打開class的內容。程序按照其中的順序執行的。

準備:爲類變量在方法區分配內存,和給類變量賦予初始值。

如原句是static int a = 5; 的話先給a賦值爲0。注意只是類變量,即static修飾的變量,final static修飾的在編譯的時候就會分配了,不包含實例對象(沒有static修飾的),實例變量將會在對象實例化時隨着對象一起分配在java堆中

解析:將常量池內類、字段、方法等的符號引用替換爲直接引用的過程

符號引用以一組符號來描述所引用的目標,包括1.類和方法的全限定名 2.字段的名稱和描述符 3.方法的名稱和描述符。目標不一定在內存中。直接引用是能夠找到目標的指針或句柄,此時引用的目標必定已經在內存中

初始化:執行靜態變量的初始化,包括靜態變量的賦值和靜態初始化塊的執行。

初始化的場景有且只有以下5種情況:
①.當虛擬機啓動,需要執行main()的主類,JVM首先初始化該類。

②.當初始化一個類時,父類沒有初始化,則先初始化父類。

③.創建新對象(new)

④.調用類的靜態成員和靜態方法。

⑤.使用java.lang.reflect包的方法對類進行反射調用,如果該類沒有初始化,則初始化。

類加載到JVM後,就會爲其中的對象分配內存空間。一個對象需要多大的內存空間在類加載完成後就確定了。除了堆外。棧和TLAB也能分配空間存儲對象。然後一般就是開始按main()方法執行我們的程序了。

 

類加載器

上面知道了類加載的過程,也知道是通過類加載器來進行類加載的。現在說一下類加載器。

JVM預定義的三種類型類加載器,當一個 JVM啓動的時候,Java缺省開始使用如下三種類型類裝入器:

 啓動(Bootstrap)類加載器:啓動類裝入器負責將jre/lib/rt.jar下一些核心類庫加載到內存中。由C++實現,屬於虛擬機實現的一部分,它並沒有繼承java.lang.ClassLoader類。開發者不可以直接使用。

 擴展(Extension)類加載器:擴展類加載器責將jre/lib/ext下的一些擴展性的類庫加載到內存中。開發者可以直接使用。

 系統(System)類加載器:系統類加載器負責將系統類classpath路徑下的類庫加載到內存中。是程序默認類加載器。開發者可以直接使用,也叫 Application ClassLoader。

 自定義類加載器(custom class loader):除了系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類加載器,以滿足一些特殊的需求。

用 class對象的getClassLoader()可以得到類加載器。

自定義類加載器的方法:通過繼承java.lang.ClassLoader,然後重寫findClass()方法實現自定義的類加載器。

protected Class<?> findClass(String name) throws ClassNotFoundException {
	try {
		byte[] data = loadByte(name);   //name爲class文件所在位置,讀成一個byte[]
		return defineClass(name, data, 0, data.length);    //define類對象
	} catch (Exception e) {
		e.printStackTrace();
		throw new ClassNotFoundException();
	}
}

 

類加載雙親委派機制

JVM在加載類時默認採用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。

這種機制能夠讓越基礎的類越由頂層加載器加載,這樣就能保證類的一致性, 防止內存中出現多份同樣的字節碼。

相同類名稱的類如果由不同的類加載器加載,也認爲是兩個不同的類

 

3.運行時數據區

當把需要執行的類加載進JVM後,在程序執行的過程中,需要一段內存空間來存儲數據,JVM可以對這段內存空間進行分配和釋放,這段內存空間我們稱爲運行時數據區。JVM規範中運行時數據區分爲五個區域: 程序計數器,虛擬機棧,本地方法棧,堆,方法區。下面說一下這五個區域的作用。

程序計數器:指向當前線程正在執行的字節碼指令的地址(行號),保證在線程切換後也能繼續從原來的位置執行下去。分支、循環、跳轉、異常處理等基礎功能也需要依賴這個計數器來完成

該區域中,JVM規範沒有規定任何OutOfMemoryError情況。

虛擬機棧:存放當前線程運行的方法所需要的數據、指令、返回值和返回地址。基本單位是棧幀。

當線程調用一個方法時,jvm就會壓入一個新的棧幀到這個線程的棧中。棧幀中包括一些局部變量表、操作數棧、動態鏈接方法、返回值、返回地址等信息。(通過 -Xss 參數設置)

如果方法的嵌套調用層次太多(如遞歸調用),棧的深度大於虛擬機所允許的深度,會產生StackOverflowError溢出異常。
如果java程序啓動一個新線程時沒有足夠的空間分配,即擴展時無法申請到足夠的內存,則拋出OutOfMemoryError異常。

本地方法棧:與虛擬機棧類似,但它是爲虛擬機用到的本地方法服務。一般情況下,我們無需關心此區域。

虛擬機通過調用本地方法接口,實現了Java和其他語言的通信(主要是C&C++)

堆:由所有線程共享,用於存放實例對象。

堆內存就是JVM管理的內存中最大的一塊,堆中分爲新生代和老年代,是垃圾回收的主要區域。

堆內存超過限制時,拋出OutOfMemoryError異常。  (通過-Xms -Xmx參數設置)

方法區:存儲已經被虛擬機加載的類信息、常量、靜態變量、靜態方法、運行時常量池、即時編譯器編譯後的代碼等

類信息如類的完整有效名稱,類的修飾符等等,方法區內存超過限制 OutOfMemoryError異常。 

 

4.分代、分配對象內存、垃圾回收

上面說的運行時數據區中,只是簡單說了堆是用來存放實例對象的, 也說了堆是垃圾回收的主要內容。那麼,堆中是怎麼分代的呢?它又是怎麼樣給對象分配內存的呢?這些對象又是怎麼樣回收的呢? 且看下文。

分代

爲什麼要分代呢?因爲我們的對象都是存在堆中的,堆的容量是有限的,所以要把堆中的一些沒用的對象進行回收。那什麼回收什麼樣的對象,什麼時候回收多久回收,怎麼高效地回收都是不小問題。JVM的開發人員就提出了分代的思想。分代後,把不同生命週期的對象放在不同代上,不同代上採用適合這個代的回收算法,這樣就能比較高效地回收資源、控制內存了。

下面左邊這塊散發着綠光的就是線程共享的堆了。堆中分爲年輕代和老年代。

其中年輕代中又分爲Eden區和兩個大小相等的Survivor區。Java 中的大部分對象都是朝生夕滅,所以大部分新創建的對象都會被分配到Eden區,當Eden區當在Eden申請空間失敗時,就會出發Minor GC,然後將Eden和其中一個Survivor區上還存活的對象複製到另外一個Survivor區中,然後一次性清除Eden代和這個Survivor區,也就是說兩個Survivor中總有一個是空的。

然後Survivor 區中的對象每經過一次 Minor GC年齡就會 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲),這些對象就會成爲老年代。或者Survivor空間中相同年齡的對象大小總和大於Survivor空間的一半,則年齡大於或等於該年齡的對象就可以進入老年代。然後一些比較大的對象則是直接進入到老年代。但是老年代的內存也不是無限的啊,萬一從年輕代過來的對象老年代也裝不下呢,所以JVM中還有一個空間分配擔保,就是在發生Minor GC之前,虛擬機會檢查老年代中的最大的可用連續內存空間如果大於之前每次晉升老年代對象的平均大小或者新生代中存活對象的總大小,則Minor GC是安全的,老年代還裝得下,觸發一次Minor GC就行;否則說明老年代也裝不下了,這時候就會進行一次Full GC。

另外,HotSpot虛擬機中除了堆中的年輕代和老年代外,JDK1.8前還有一個永久代,在1.7之前在的實現中,HotSpot 使用永久代實現方法區,HotSpot 使用 GC分代來實現方法區內存回收,Java8元空間(metaSpaces)取代了永久代。元空間並不在虛擬機中,而是使用本地內存。元空間優勢:不存在永久代溢出的問題,並且不再需要調整和監控永久代的內存空間。提高了內存利用率,且更利於垃圾回收。但是

-Xms  -Xmx   最大堆大小,最大堆大小

-XX:NewSize  XX:MaxNewSize  設置年輕代大小,建議設爲整個堆大小的1/3或者1/4,兩個值設爲一樣大。

-XX:NewRatio=n  設置年老代和年輕代的比值,默認爲2. 即 年輕代: 老年代 = 2:1。典型設置爲4。

-XX:SurvivorRatio=n  年輕代中Eden區與兩個Survivor區的比值,默認爲 8:1:1

-XX:MaxTenuringThreshold  設置Survivor中可以進入老年代的對象年齡值,默認15

-XX:PretenureSizeThreshold  設置可以直接進入老年代的大小,默認3M

-XX:PermSize 和 -XX:MaxPermSize  1.8中永久代的已經失效

在元空間中:
-XX:MetaspaceSize,表示初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

 

分配對象內存

類加載完成後,會爲其中的對象分配內存空間。一個對象需要多大的內存空間在類加載完成後就確定了。我們說堆是用來存放對象的。那除了堆之外,還有哪裏能分配對象嗎? 答案是有的。那就是棧上分配TLAB

棧上分配

JVM允許將線程私有的對象打散分配在棧上,而不是分配在堆上。然後對象可以在函數調用結束後自行銷燬,垃圾收集系統的壓力將會小很多,從而提高系統性能。 

要做到棧上分配依賴兩個技術:逃逸分析和標量替換。逃逸分析可以分析一個新對象的使用範圍。就是你在方法中定義了一個變量後,它可能會作爲調用參數傳遞到其他方法中(方法逃逸)。也可能被外部線程訪問到(線程逃逸)。這時候這個對象就相當於逃逸了,如果這個變量沒有逃逸的話,證明這個變量是線程私有的。這時候就允許將對象打散分配在棧上。比如若一個對象擁有兩個字段,會將這兩個字段視作局部變量進行分配,即標量替換。

-XX:+DoEscapeAnalysis  啓用逃逸分析 (默認打開)

-XX:+EliminateAllocations 開啓標量替換 (默認打開)

TLAB

TLAB(Thread Local Allocation Buffer 線程本地分配緩衝區)指JVM在新生代Eden區中爲每個線程開闢的私有區域。默認佔用Eden Space的1%。因爲TLAB是私有的,沒有鎖的開銷,所以Java程序中很多不存在線程共享的小對象,通常是在TLAB上優先分配,這些小對象也適合被快速地回收。

 

所以給對象分配內存的過程如下:

1)編譯器通過逃逸分析,確定對象是在棧上分配還是在堆上分配。如果是堆上分配則繼續嘗試。

2)嘗試TLAB上能夠直接分配對象則直接在TLAB上分配,如果現有的TLAB不足以存放當前對象則重新申請一個TLAB,並再次嘗試存放當前對象。如果還放不下則繼續嘗試。

3)嘗試對象能否直接進入老年代,如果能夠進入則直接在老年代分配。否則再繼續嘗試。

4)最後選擇是年輕代的Eden。(具體分配方式:指針碰撞 / 空閒列表)

堆是所有線程共享的,因此在堆上分配內存需要加鎖,就多了鎖的開銷。無論是TLAB還是棧都是線程私有的,私有即避免了競爭(當然也可能產生額外的問題例如可見性問題),這是典型的用空間換效率的做法。

PS: 在堆上分配內存的方式或策略,有指針碰撞和空閒列表。
指針碰撞:爲對象分配內存空間時,如果被內存空間是規整的,只要把空閒指針向空閒內存方向挪動即可。
空閒列表:爲對象分配內存空間時,如果內存空間不是規整的,需要有一個“空閒列表”用於記錄哪些內存是可用的,並從可用內存中分配足夠大小的內存出來,並修改“空閒列表”;

另外補充一個內容,上面說的都是對象的分配,我們也說java是面向對象編程,那麼這個對象又是怎樣的呢?它有什麼結構?我們怎麼找到這個對象? 下面補充一些對象的內容。

對象結構: 對象頭、實例數據、對齊填充

對象頭:由兩部分組成,第一部分存儲對象自身的運行時數據:哈希碼、GC分代年齡、鎖標識狀態、線程持有的鎖、偏向線程ID(一般佔32/64 bit)。第二部分是指針類型,指向對象的類元數據類型(即對象代表哪個類)。如果是數組對象,則對象頭中還有一部分用來記錄數組長度。

實例數據:用來存儲對象真正的有效信息,包括父類繼承下來的和自己定義的。這是我們定義和關心的。

對齊填充:jvm要求對象起始地址必須是8字節的整數倍(8字節對齊)

對象分配過程:
   
1)當JVM遇到new指令時,首先JVM需要在運行時常量池查找這個類對應的類符號引用,並且檢查這個符號代表的類是否被加載,解析和初始化過,如果沒有找到就需要把這個類加載到運行時常量池。 (1.7的運行時常量池在方法區,1.8後在堆)
    2)根據類的相關信息,爲這個要創建的對象分配一定大小的內存。
    3)JVM將對象的內存空間除對象頭外初始化爲0,這就是爲什麼JAVA代碼中的全局變量可以不用初始化也可以使用的原因。此外,JVM還會爲對象設置對象頭。如設置對象所屬的類,對象哈希碼,鎖標識狀態,GC分代年齡等。
    經過上面幾步,虛擬機認爲一個對象已經創建完畢,但是從程序來看,對象還沒有初始化,因此需要根據代碼初始化各個變量。初始化變量後,一個可用的對象就創建好了。

對象定位方式:句柄池 / 直接指針

 

垃圾回收

垃圾回收,就包括兩個內容,一個是垃圾,一個是回收,怎樣纔是垃圾? 垃圾怎樣回收?

垃圾

JVM中,關於“垃圾”的判斷,大部分對象根據"判斷算法"判定,少部分對象根據"引用"來判定。

“判斷算法” 判斷垃圾:引用計數法可達性分析法。

      引用計數法就是給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;在任何時刻計數器的值爲0的對象就是不可能使用的,也就是可被回收的對象。引用計數法比較簡單、高效。但無法解決對象相互引用的問題。所以主流的JVM並沒有選用這種算法,而是採用可達性分析法。

      可達性分析法是把內存中的每一個對象都看作一個節點,並且定義了一些對象作爲根節點“GC Roots”。如果一個對象中有另一個對象的引用,那麼就認爲第一個對象有一條指向第二個對象的邊。JVM會起一個線程從所有的GC Roots開始往下遍歷,當遍歷完之後如果發現有一些對象不可到達,那麼就認爲這些對象已經沒有用了,需要被回收。

可達性分析法的關鍵就在於GC Roots的定義,在java中,可以作爲GC Roots的對象有四種:
1)虛擬機棧中的引用的對象
2)類中static修飾的靜態變量引用的對象 
3)類中static final修飾的常量引用的對象
4)本地方法棧中引用的對象
注:JDK1.7靜態變量和常量池在方法區,1.8後移動至堆

“引用” 判斷垃圾:強引用,軟引用,弱引用,虛引用

      強引用:類似於“Object obj = new Object()”的引用,只要obj的生命週期沒結束,或者沒有顯示地把obj指向爲null,那麼JVM就永遠不會回收這種對象。

方法中的強引用,在方法運行完成後就退出方法棧,對應堆中對象會被回收
全局變量中的強引用,爲其賦值爲null且再沒有調用過,才能幫助回收此對象

      軟引用:JVM正常運行時,軟引用和強引用沒什麼區別,但是當內存不夠用時,瀕臨逸出的情況下,JVM的垃圾收集器就會把軟引用的對象回收。在JDK中提供了SoftReference類來實現軟引用。

軟引用可用來實現內存敏感的高速緩存,例如網頁緩存、圖片緩存、瀏覽器的後退按鈕,當按後退時,如果內存吃緊,這時候就對軟引用的對象回收了,只要重新構建。如果內存還充足,那這個軟引用還沒有被回收,我們可以直接從這個軟引用獲取內容。(如  Object obj = new Object();  SoftReference<Object> softReference = new SoftReference<Object>(obj); )

      弱引用:弱引用的對象將會在下一次的gc被回收,不管JVM內存被佔用多少。在JDK中使用WeakReference來實現弱引用。

當你想引用一個對象,但是這個對象有自己的生命週期,你不想介入這個對象的生命週期,這時候你就是用弱引用。因爲弱引用不會影響它的引用對象在垃圾回收判斷中的判斷。

      虛引用:虛引用是最脆弱的引用,我們沒有辦法通過一個虛引用來獲得對象,即使在沒有gc之前。虛引用必須和一個引用隊列配合使用。在JDK中使用PhantomReference來實現虛引用。

虛引用主要用來跟蹤對象被垃圾回收器回收的活動。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊 列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。

這裏補充一個:
1) system.gc()並不是你調用就馬上執行的, 而是根據虛擬機的各種算法來來計算出執行垃圾回收的時間,另外,程序自動結束時不會執行垃圾回收的。 
2) 對象被回收時,要經過兩次標記,第一次標記,如果finalize()被重寫,那麼垃圾回收並不會去執行finalize,第二次標記,如果對象不能在finalize中成功拯救自己,那真的就要被回收了。

 

回收

說完垃圾判斷,最後就到如何回收的內容了。包括垃圾回收的算法,垃圾回收的節點,具體的垃圾回收器和垃圾回收的其它內容

垃圾回收算法:標記 - 清除算法,複製算法,標記 - 整理算法,分代收集算法(大部分JVM的垃圾收集器使用)

      標記 - 清除算法:標記清除算法(Mark-Sweep)是最基礎的收集算法,其他收集算法都是基於這種思想。標記清除算法分爲“標記”和“清除”兩個階段:首先標記出需要回收的對象,標記完成之後統一清除被標記的對象。這種算法實現簡單,運行高效,但清除之後會產生大量不連續的內存碎片。

      複製算法:複製算法(Copying)將可用內存容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊用完之後,就將還存活的對象複製到另外一塊上面,然後在把已使用過的內存空間一次清理掉。複製算法實現簡單,運行高效,不會產生碎片。
但是每次相當於只能使用內存的一半,不能充分使用內存。

      標記 - 整理算法:標記整理算法(Mark-Compact)標記操作和“標記-清除”算法一致,後續操作不只是直接清理對象,而是在清理無用對象完成後讓所有存活的對象都向一端移動,並更新引用其對象的指針。這種算法能充分利用內存且不會產生內存碎片。但在標記-清除的基礎上還需進行對象的移動,成本相對較高。

      分代收集算法:分代收集算法是目前大部分JVM的垃圾收集器採用的算法,根據對象存活的生命週期將內存劃分爲若干個不同的區域。一般情況下將堆區劃分爲新生代和老年代。新生代中對象的存活率低,存活期會相對會比較短一些,選用複製算法來進行內存回收。老生代中,對象的存活率比較高,並且相對存活期比較長一些,一般使用的是標記 - 整理算法

注:新生代的空間並不是按照1:1的比例來劃分,而是分爲一塊較大的Eden空間和兩塊較小的Survivor空間(8:1:1)

 

垃圾回收的節點:
 
1)當應用程序空閒時,即沒有應用線程在運行時,GC會被調用。
  2)如果Eden區內存已滿,會進行一次minor gc
  3)如果老年代內存不足,會進行一次full gc
  4)1.7 持久代(Perm)被寫滿,也可能導致full gc
        1.8 達到元空間(MataSpace)初始空間大小,也會觸發full gc進行類型卸載
  5)手動調用System.gc(); 此時也有可能調用full gc

 

垃圾回收器

有 Serial 、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1 七種垃圾收集器。可見Serial 、ParNew、Parallel Scavenge是屬於新生代收集器;Serial Old、Parallel Old、CMS屬於老年代收集器。G1在整堆

上圖連線的收集器可搭配使用
jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Serial Old(老年代)
jdk1.9 默認垃圾收集器G1 (未來的趨勢)

Serial收集器:Serial是最基本也是發展最悠久的收集器。它作用於年代代,是一種單線程垃圾收集器,採用複製算法,這就意味着在其進行垃圾收集的時候需要暫停其他的線程,也就是之前提到的”Stop the world“。

優點:簡單而高效,對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得較高的收集效率。此外,到目前爲止,Serial收集器依然是Client模式下的默認的新生代垃圾收集器。

ParNew收集器:可以把這個收集器理解爲Serial收集器的多線程版本。即年輕代+多線程+複製算法+stop-the-world。一般用ParNew配合老年代的CMS收集器使用。

-XX:+UseParNewGC 表示要強制使用parNew收集器在新生代回收空間
-XX:+UseConcMarkSweepGC  設置老年代使用CMS。
-XX:+ParallelGCThreads  設置執行垃圾收集的線程數

Parallel Scavenge:ParallelScavenge收集器作用於新生代,它採用複製算法,是並行的多線程收集器,但是用戶仍處於等待狀態。目標則是達到一個可控制的吞吐量。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。而高吞吐量爲目標,就是減少垃圾收集時間,讓用戶代碼獲得更長的運行時間。主要適合在後臺運算而不是太多交互的任務,比如需要與用戶交互的程序,良好的響應速度能提升用戶的體驗。

-XX:MaxGCPauseMillis  設置每次年輕代垃圾回收的最長時間(最大暫停時間);
-XX:GCTimeRatio  設置垃圾回收時間佔程序運行時間的百分比;
-XX:+UseAdaptiveSizePolicy  自動選擇年輕代區大小和相應的Survivor區比例;

Serial Old:Serial Old是Serial的老年代版本,也是老年代的默認收集器,它是一個單線程的,使用標記整理算法,工作過程中也會stop-the-world。

 -XX:+UseParallelGC:JVM參數中默認選擇年輕代垃圾收集器爲並行收集器,而年老代仍舊使用串行Serial Old收集器。

Parallel Old:Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法進行垃圾回收。其通常與Parallel Scavenge收集器配合使用,“吞吐量優先”收集器是這個組合的特點,在注重吞吐量和CPU資源敏感的場合,都可以使用這個組合。(需要手動開啓)

 -XX:+UseParallelOldGC:配置年老代垃圾收集方式爲並行收集。JDK6.0後支持的對年老代並行Parallel Old收集器

CMS:CMS收集器(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務器的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。CMS收集器主要優點:併發收集,低停頓。但CMS有三個明顯的缺點:(1)CMS收集器對CPU資源非常敏感。CPU個數少於4個時,CMS對於用戶程序的影響就可能變得很大;(2)CMS收集器無法處理浮動垃圾 ;(3)CMS是基於“標記-清除”算法實現的收集器,會產生垃圾碎片。

-XX:CMSInitiatingOccupancyFraction : 1.6後CMS收集器的啓動閥值已經提升至92%。即老年代使用92%內存後Full GC
-XX:+UseCMSCompactAtFullCollection:開關參數(默認開啓的),用於在進行FullGC時開啓內存碎片合併整理過程
-XX:CMSFullGCsBeforeCompaction:用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的 (默認值爲0, 每次)

G1:G1 GC是Jdk7的新特性之一,且未來計劃替代CMS。G1將堆空間劃分成了大小相等互相獨立的heap區塊。每塊區域既有可能屬於Old區、也有可能是Young區。 G1在全局標記階段(global marking phase)併發執行, 以確定堆內存中哪些對象是存活的。標記階段完成後,G1就可以知道哪些heap區哪裏垃圾最多。它會首先回收這些區,通常會得到大量的自由空間. 這也是爲什麼這種垃圾收集方法叫做Garbage-First(垃圾優先)的原因:第一時間處理垃圾最多的區塊。

-XX:+UseG1GC  使用G1垃圾回收器。 使用場景:
1).服務端多核CPU、JVM內存佔用較大的應用(至少大於4G)
2).應用在運行過程中會產生大量內存碎片、需要經常壓縮空間
3).想要更可控、可預期的GC停頓週期;防止高併發下應用雪崩現象

 

垃圾回收其它內容就暫時不細寫了:

如何減少垃圾回收:儘量減少臨時對象的使用,對象不用時最好顯式置爲Null ,使用StringBuffer,分散對象創建或刪除的時間等

如何查看GC日誌: jps jmap jstat 等命令

如何進行垃圾回收調優
      通過-Xns -Xnm合理控制堆的大小,通過-Xsn控制年輕代和老年代大小。
      通過NewRatio控制新生代老年代比例(默認是1:2)
      通過MaxTenuringThreshold控制進入老年前生存次數(默認是15次)

以4核8G內存的服務器爲例,可對tomcat做以下配置:
 -Xms256m    #最小堆內存
 -Xmx2048m    #最大堆內存
 -Xss512k    #每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小爲1M。
 #對堆區的進一步細化分:新生代、中生代、老生代
 -XX:NewSize=256m    #表示新生代初始內存的大小
 -XX:MaxNewSize=512m    #表示新生代可被分配的內存的最大上限;當然這個值應該小於 -Xmx的值;
 -XX:PermSize=128m    表示非堆區初始內存分配大小,permanent size(持久化內存)
 -XX:MaxPermSize=256m    #表示對非堆區分配的內存的最大上限
 -XX:+UseBiasedLocking    #使用偏見的鎖,使得鎖更偏愛上次使用到它線程。在非競爭鎖的場景下,即只有一個線程會鎖定對象,可以實現近乎無鎖的開銷。

linux下查找java進程佔用CPU過高原因

1. 查找進程

top查看進程佔用資源情況

 

明顯看出java的兩個進程22714,12406佔用過高cpu.

 

2.查找線程

使用top -H -p <pid>查看線程佔用情況

 

3.查找java的堆棧信息

將線程id轉換成十六進制

#printf %x 15664

#3d30

 

然後再使用jstack查詢線程的堆棧信息

語法:jstack <pid> | grep -a 線程id(十六進制)

jstack <pid> | grep -a 3d30

 

剩下的就是分析原因和修改代碼了

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