前言
Java的JVM,可能學Java的都知道這個名字,部落格或者百度也有一大堆,因為面試原因,大緻也能說得上一些東西,今天重新梳理一下,一方面複習,一方面加深了解
JVM的原理
JVM可以了解成一個虛構出來的計算機,一個特點是跨平台型,将源碼編譯成目标代碼,這個目标代碼就是位元組碼(也就是Java裡面的*.class檔案),在任何平台上,windows、linux、類unix、tru64等機器上,隻要安裝對應的jdk,即可實作在一個平台上編譯,不需要其他的修改。
資料類型
Java的類型分為兩大類:基礎資料類型、非基礎資料類型
基礎資料類型:
byte: 1位元組
short: 2位元組
int: 4位元組
long: 8位元組
float: 4位元組單精度浮點數
double: 8位元組雙精度浮點數
char: 2位元組無符号Unicode字元
非基礎資料類型(其他資料類型):
object:對象,所有的自己編寫的類都是對象:占用4個位元組,在Java虛拟機中的堆中存在(後面會深入講到)
編譯及運作
通過調用jre來進行将Java源碼編譯成*.class檔案
JVM加載機制如下

堆和棧
堆:是一個運作時資料區,類的執行個體(對象)和其他數組從中配置設定空間,它的管理是由垃圾回收來負責的:不給程式員顯式釋放對象的能力。Java不規定具體使用的垃圾回收算法,可以根據系統的需求使用各種各樣的算法。
(主要用于存放對象,存取速度慢,可以運作時動态配置設定記憶體,生存期不需要提前确定)
棧:一些基本類型的變量和對象的引用變量都是在函數的棧記憶體中配置設定。
(主要用來執行程式,存取速度快,大小和生存期必須确定,缺乏靈活性)
堆是應用程式在運作的時候請求作業系統配置設定給自己記憶體,由于從作業系統管理的記憶體配置設定,是以在配置設定和銷毀時都要占用時間,是以用堆的效率非常低.但是堆的優點在于,編譯器不必知道要從堆裡配置設定多少存儲空間,也不必知道存儲的資料要在堆裡停留多長的時間,是以,用堆儲存資料時會得到更大的靈活性。事實上,面向對象的多态性,堆記憶體配置設定是必不可少的,因為多态變量所需的存儲空間隻有在運作時建立了對象之後才能确定.在C++中,要求建立一個對象時,隻需用 new指令編制相關的代碼即可。執行這些代碼時,會在堆裡自動進行資料的儲存.當然,為達到這種靈活性,必然會付出一定的代價:在堆裡配置設定存儲空間時會花掉更長的時間!
總結:
- 堆區:
- 存儲的全部是對象,每個對象都包含一個與之對應的class的資訊。(class的目的是得到操作指令)
- jvm隻有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,隻存放對象本身
- 棧區:
- 每個線程包含一個棧區,棧中隻儲存基礎資料類型的對象和自定義對象的引用(不是對象),對象都存放在堆區中
- 每個棧中的資料(原始類型和對象引用)都是私有的,其他棧不能通路。
- 棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。
- 方法區:
- 又叫靜态區,跟堆一樣,被所有的線程共享。方法區包含所有的class和static變量。
- 方法區中包含的都是在整個程式中永遠唯一的元素,如class,static變量。
GC
1、GC不會立即收集這個我們都知道
2、GC主要是回收堆和方法區的記憶體
堆記憶體的存活
- 通過計數器:每一個對象有一個計數器,新增一個對象+1,釋放之後-1,計數為0時候可以回收。這個方式存在一個問題:假如B引用A,但是B=null;這個時候B是沒辦法回收的
- 可達性算法:GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鍊。當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的。
垃圾收集算法
-
标記清除算法
先标記,再清除
它的主要缺點有兩個:一個是效率問題,标記和清除過程的效率都不高;另外一個是空間問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻,當程式在以後的運作過程中需要配置設定較大對象時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
-
複制算法
它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。
這樣使得每次都是對其中的一塊進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。隻是這種算法的代價是将記憶體縮小為原來的一半,持續複制長生存期的對象則導緻效率降低
-
标記-壓縮算法
在對象存活率較高時就要執行較多的複制操作,效率将會變低。
标記過程仍然與“标記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體
-
分代收集算法
把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最适當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,隻有少量存活,那就選用複制算法,隻需要付出少量存活對象的複制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行配置設定擔保,就必須使用“标記-清理”或“标記-整理”算法來進行回收。
問題:
1.什麼是新生代(Young),老年代(Old),永久代(Perm)?
簡單的說:
GC會分區域回收垃圾,為了友善對記憶體進行管理,分為新生代、老年代和永久代
新生代就是:頻繁收集的區域就是新生代
老年代:收集頻率較少的區域
永久帶:基本上不回收的區域成為永久代
堆=新生代+老年代
2.新生代、老年代、永久代各儲存哪些資訊?jvm是怎麼劃分
新生代:新生代劃分三個區域,Eden、SurvivorA和SurvivorB,當對象第一次建立的時候,都是存放在新生代中的Eden中,當Eden沒有空間的時候,會執行一次GC,如果存在某些對象不能引用,會把這些對象複制到SurvivorA或者SurvivorB中的任意一個。但是肯定有一個Survivor是空的,當Survivor裡面的對象超過一定回收次數的閥值(年齡門檻值,可以通過-XX:MaxTenuringThreshold來設定)),這部分對象會被轉移到老年代
備注:
1)可以根據程式需要,增加設定多個Survivor區域,減少對象被放入到老年代
2)GC回收的時候程式會挂起,也就是這個時候程式會很”卡”,jvm調優應該盡量降低GC的時間,減少FULL GC的次數
老年代:當新生代不能回收的對象,就儲存在老年代中,在老年代回收的頻率會比較低
永久代:存在靜态檔案,靜态的東西,幾乎不會被GC
3.Minor GC、Major GC、Full GC到底有什麼差別?
Minor GC:就是對堆中的新生代進行一次GC
Major GC:對堆中老年代進行一次GC,速度一般比Minor GC慢10倍
Full GC:全部回收,對堆中的新生代和老年代都進行GC,并且也會棧執行GC,比Major GC更慢
4.Full GC是對堆全部GC,那麼會挂起時間比較長,Minor GC(新生代空間滿)和Major GC(一定的年齡閥值)都有一定的政策,那麼什麼情況下會進行Full GC呢?1)程式中顯示的調用System.gc()。當程式員在程式中調用System.gc()的時候,會産生full gc,應該禁止顯示調用System.gc(),讓虛拟機自己去管理它的記憶體,如果程式中有RMI接口或者,應該禁止遠端RMI調用GC,可通過通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc
2)老年代空間不足
當老年代空間不足的時候,會産生Full GC。但是當GC後仍然空間不足的話,會産生
Java.lang.OutOfMemoryError: Java heap space
是以在jvm調優中應該盡量讓對象在Major GC中被回收,以及不要建立過大的數組和對象
3)永久代空間不足
當永久代空間不足,并且沒有配置CMS GC的時候也會執行Full GC,當Full GC回收後,仍然不夠空間,會産生
java.lang.OutOfMemoryError: PermGen space
JVM參數:
堆設定
-Xms :初始堆大小
-Xmx :最大堆大小
-XX:NewSize=n :設定年輕代大小,設為整個堆大小的1/3或者1/4,和XX:MaxNewSize兩個值設為一樣大。
-XX:NewRatio=n: 設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
-XX:SurvivorRatio=n :年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
-XX:MaxPermSize=n :設定持久代大小
收集器設定
-XX:+UseSerialGC :設定串行收集器
-XX:+UseParallelGC :設定并行收集器
-XX:+UseParalledlOldGC :設定并行年老代收集器
-XX:+UseConcMarkSweepGC :設定并發收集器
垃圾回收統計資訊
-XX:+PrintHeapAtGC GC的heap詳情
-XX:+PrintGCDetails GC詳情
-XX:+PrintGCTimeStamps 列印GC時間資訊
-XX:+PrintTenuringDistribution 列印年齡資訊等
-XX:+HandlePromotionFailure 老年代配置設定擔保(true or false)
并行收集器設定
-XX:ParallelGCThreads=n :設定并行收集器收集時使用的CPU數。并行收集線程數。
-XX:MaxGCPauseMillis=n :設定并行收集最大暫停時間
-XX:GCTimeRatio=n :設定垃圾回收時間占程式運作時間的百分比。公式為1/(1+n)
并發收集器設定
-XX:+CMSIncrementalMode :設定為增量模式。适用于單CPU情況。
-XX:ParallelGCThreads=n :設定并發收集器年輕代收集方式為并行收集時,使用的CPU數。并行收集線程數。
JVM的任何一個參數都不是越大越好,也都不是越小越好,通過設定jvm的參數,我們要達到三個目的:
1)GC的時間足夠的小
2)GC的次數足夠的少
3)發生Full GC的周期足夠的長
1和2是想反的,我們設想,要想GC時間少,那麼堆肯定要設定小一點,但是要保證GC時間足夠的少,我們又要保證堆空間大一點,是以,我們必須根據實際情況平衡
(1)針對JVM堆的設定,一般可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而産生額外的時間,我們通常把最大、最小設定為相同的值
(2)年輕代和年老代将根據預設的比例(1:2)配置設定堆記憶體,可以通過調整二者之間的比率NewRadio來調整二者之間的大小,也可以針對回收代,比如年輕代,通過 -XX:newSize -XX:MaxNewSize來設定其絕對大小。同樣,為了防止年輕代的堆收縮,我們通常會把-XX:newSize -XX:MaxNewSize設定為同樣大小
- 更大的年輕代必然導緻更小的年老代,大的年輕代會延長普通GC的周期,但會增加每次GC的時間;小的年老代會導緻更頻繁的Full GC
- 更小的年輕代必然導緻更大年老代,小的年輕代會導緻普通GC很頻繁,但每次的GC時間會更短;大的年老代會減少Full GC的頻率
- 如何選擇應該依賴應用程式對象生命周期的分布情況:如果應用存在大量的臨時對象,應該選擇更大的年輕代;如果存在相對較多的持久對象,年老代應該适當增大。但很多應用都沒有這樣明顯的特性,在抉擇時應該根據以下兩點:(A)本着Full GC盡量少的原則,讓年老代盡量緩存常用對象,JVM的預設比例1:2也是這個道理 (B)通過觀察應用一段時間,看其他在峰值時年老代會占多少記憶體,在不影響Full GC的前提下,根據實際情況加大年輕代,比如可以把比例控制在1:1。但應該給年老代至少預留1/3的增長空間
(3)在配置較好的機器上(比如多核、大記憶體),可以為年老代選擇并行收集算法: -XX:+UseParallelOldGC ,預設為Serial收集
(4)線程堆棧的設定:每個線程預設會開啟1M的堆棧,用于存放棧幀、調用參數、局部變量等,對大多數應用而言這個預設值太了,一般256K就足用。理論上,在記憶體不變的情況下,減少每個線程的堆棧,可以産生更多的線程,但這實際上還受限于作業系統。
整理:
1、多數的Java應用不需要在伺服器上進行GC優化;
2、多數導緻GC問題的Java應用,都不是因為我們參數設定錯誤,而是代碼問題;
3、在應用上線之前,先考慮将機器的JVM參數設定到最優(最适合);
4、減少建立對象的數量;
5、減少使用全局變量和大對象;
6、GC優化是到最後不得已才采用的手段;
7、在實際使用中,分析GC情況優化代碼比優化GC參數要多得多;
JVM監控
JDK自帶工具jstat使用
1)先确定tomcat或者jetty等運用伺服器的pid
windows:使用netstat -ano |findstr “8080”(假如伺服器端口是8080)
linux:linux使用ps aux|grep “tomcat”指令(根據實際情況修改)
2)上面我在windows上面已經确定了pid是6396,那麼在指令行輸入:
jstat -gcutil 6396 1000 5表示監控6396端口,每1000毫秒執行一次列印,總共列印五次
Options — 選項,我們一般使用 -gcutil 檢視gc情況
vmid — VM的程序号,即目前運作的java程序号
interval– 間隔時間,機關為秒或者毫秒
count — 列印次數,如果預設則列印無數次
如圖:
S0 — Heap上的 Survivor space 0 區已使用空間的百分比
S1 — Heap上的 Survivor space 1 區已使用空間的百分比
E — Heap上的 Eden space 區已使用空間的百分比
O — Heap上的 Old space 區已使用空間的百分比
P — Perm space 區已使用空間的百分比
YGC — 從應用程式啟動到采樣時發生 Young GC 的次數
YGCT– 從應用程式啟動到采樣時 Young GC 所用的時間(機關秒)
FGC — 從應用程式啟動到采樣時發生 Full GC 的次數
FGCT– 從應用程式啟動到采樣時 Full GC 所用的時間(機關秒)
GCT — 從應用程式啟動到采樣時用于垃圾回收的總時間(機關秒)
我們可以對每個參數進行分析
JDK自帶工具jconsole(windows)
在windows目錄有C:\Program Files\Java\jdk1.7.0_55\bin\jconsole.exe
輕按兩下jconsole.exe,可以看到
裡面有