1、JVM內存模型
1.1 JVM內存模型圖解
Java虛擬機在執行Java程序的過程中,會把它所管理的內存划分為若干個不同的數據區。這些區域有各自的用途,以及創建和銷毀的時間,有的區域隨着虛擬機進程的啟動而存在,有的區域則依賴用戶線程的啟動和結束而建立和銷毀,我們可以將這些區域統稱為Java運行時數據區域。
如下圖是java虛擬機運行時數據區:
該區域一共分為5個區域:堆(heap),棧(stack)、本地方法棧(native method area),方法區(method area),程序計數器(program count register)
1.2 程序計數器
程序計數器(program count register) 是一塊較小的內存空間,可以看做當前線程所執行的字節碼的信號指示器。
1.3 堆(Heap)
Java 堆是java虛擬機所管理內存中最大的一塊。該區域被所有線程共享,在虛擬機啟動時創建,用來存放對象的實例,幾乎所有的對象以及數組都在這里分配內存。
Java堆是GC管理的主要區域。
按分代收集算法:分為新生代和老年代
新生代又分為:Eden區,From Survivor區,To Survivor區
Java堆可以處於物理上不連續的內存空間,只要邏輯連續即可
通過-Xmx和-Xms控制
例如:-Xms8g -Xmx8g 初始堆內存8g,最大堆內存也是8g
1.4 棧(Stack)
Java虛擬機棧是線程私有的,生命周期與線程相同,每個方法執行時都會創建一個棧幀(Stack Frame),描述的是java方法執行的內存模型,用於存儲局部變量,操作數棧,方法出口等。每個方法的調用都對應的出棧和入棧
圖中可以看到每一個棧幀中都有局部變量表。局部變量表存放了編譯期間的各種基本數據類型,對象引用等信息。
1.5 本地方法棧(Native Stack)
本地方法棧(Native Stack)與Java虛擬機站(Java Stack)所發揮的作用非常相似,他們之間的區別在於虛擬機棧為虛擬機棧執行java方法(也就是字節碼)服務,而本地方法棧則為使用到Native方法服務。
1.6 方法區(Method Area)
方法區(Method Area)與堆(Java Heap)一樣,是各個線程共享的內存區域,它用於存儲虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯后的代碼等數據。
也稱為 Non-Heap(非堆)為與堆區分來
叫法:Permanent generation(永久代),是因為GC通過使用永久代來實現方法區的收集,希望通過管理堆一樣管理這部分內存,其實兩者並不等價,或者說使用永久代來實現方法區而已.
通過-XX:MaxPermSize來設置上限
例如:-XX:MaxPermSize=256m 方法區的上限是256M
1.7 直接內存(Direct memory)
直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機定義內存的一部分,該內存也被頻繁的使用,也可能導致OutOfMemoryError異常
直接內存不會受到受java堆限制,但會受本機總內存大小限制,還是會根據內存設置-Xmx,若忽略直接內存,使得各個內存總和大約物理內存限制,從而導致動態擴容的時候出現OutOfMemory異常
1.8 對象創建過程
當遇到new的指令時,首先去常量池中檢查一下該類的符號被引用,並且檢查該符號的類是否被加載,解析和初始化過,若沒有就要先執行類加載的過程,
類加載檢查通過之后,就已經確定了對象所需內存,虛擬機將為新生對象分配內存,
內存分配完成之后,虛擬機將內存空間初始化為零值,
虛擬機將該類的元數據,hash碼,GC分代年齡等信息放入對象頭,
執行<init>方法,一個真正可用的對象產生出來了。
2、GC算法
垃圾收集(garbage collection,GC)
Java虛擬機並沒有引用計數法算法來管理內存,主要原因是因為很難解決對象之間的相互引用。
可達性分析算法:通過GC Roots的對象作為起點,從這些節點往下搜索,搜索走過的路徑被稱為引用鏈,當一個對象到GC Roots沒有引用鏈,則稱為該對象不可達
2.1 標記—清除算法(mark-sweep)
1、標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象
2、在標記完成后統一回收所有被標記的對象
缺點:一個是效率問題,標記和清除兩個過程的效率都不高;
另一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中
需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
2.2 復制算法(Copying)
1、將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。
2、當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。
優點:內存分配不用考慮內存碎片的情況
缺點:1、代價是將內存縮小為原來的一半
2、復制收集算法在對象存活率較高時就要進行較多的復制操作。效率會變低,由於需要額外的空間進行擔保,所以老年代不能直接選用這種算法
現在商業虛擬機都采用這種收集算法來回收新生代
IBM公司研究表明:新生代的西愛過你98%都是“朝生夕死”,所以並沒必要1:1分配內存,而是將內存分為一塊較大的Eden區和兩塊較小的Survivor區,每次使用Eden和其中一塊Survivor,當回收的時候,將Eden和Survivor中還存活的對象一次性的復制到另外的Survivor上,最后清理Eden區和剛才使用的Survivor區,HotSpot虛擬機默認Eden:Survivor=8:1 ,也就是每次新生代中可用內存空間為整個新生代容量的90%,只有10%會浪費,
由於我們沒法保證每次回收都之后不超過10%的對象存活,當Survivor空間不夠,則需要依賴老年代,進行分配擔保(handle promotion)
2.3 標記-整理算法(mark-compact)
1、標記
2、讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存
2.4 分代收集算法
1、根據對象存活周期的不同將內存划分為幾塊。
2、一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
3、在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
4、老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。
3、垃圾回收器
如果有一種放之四海皆准,任何場景下都適合的完美收集器存在,那么hotspot虛擬機就沒必要實現那么多不同的收集器
3.1 新生代收集器
3.1.1 Serial收集器
1、是一個單線程的收集器,“Stop The World”
2、對於運行在Client模式下的虛擬機來說是一個很好的選擇
3、簡單而高效
3.1.2 ParNew收集器
1、Serial收集器的多線程版本
2、單CPU不如Serial
3、Server模式下新生代首選,目前只有它能與CMS收集器配合工作
4、使用-XX:+UseConcMarkSweepGC選項后的默認新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。
5、-XX:ParallelGCThreads:限制垃圾收集的線程數。
6、除了Serial收集器外,只 有ParNew才能與CMS收集器配合工作
3.1.3 Parallel Scavenge 收集器
1、吞吐量優先”收集器
2、新生代收集器,復制算法,並行的多線程收集器
3、目標是達到一個可控制的吞吐量(Throughput)。
4、吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鍾,其中垃圾收集花掉1分鍾,那吞吐量就是99%。
5、兩個參數用於精確控制吞吐量:
-XX:MaxGCPauseMillis是控制最大垃圾收集停頓時間
-XX:GCTimeRatio直接設置吞吐量大小
-XX:+UseAdaptiveSizePolicy:動態設置新生代大小、Eden與Survivor區的比例、晉升老年代對象年齡
6、並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。
7、並發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另一個CPU上。
8、停頓時間越短越適合需要用戶交互的程序,良好的響應速度能提升用戶體驗,而吞吐量則可以高效的利用cpu時間,盡快的完成運算任務,主要適合后台不需要太多交互任務
3.2 老年代收集器
3.2.1 Serial Old收集器
1、Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。
2、主要意義也是在於給Client模式下的虛擬機使用。
3、如果在Server模式下,那么它主要還有兩大用途:
一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用[1],
另一種用途就是作為CMS收集器的后備預案,在並發收集發生Concurrent Mode Failure時使用。
3.2.2 Parallel Old收集器
1、Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。
2、在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。
3.2.3 CMS收集器
1、以獲取最短回收停頓時間為目標的收集器。
2、非常符合互聯網站或者B/S系統的服務端上,重視服務的響應速度,希望系統停頓時間最短的應用
3、基於“標記—清除”算法實現的
4、CMS收集器的內存回收過程是與用戶線程一起並發執行的
5、它的運作過程分為4個步驟,包括:
初始標記,“Stop The World”,只是標記一下GC Roots能直接關聯到的對象,速度很快
並發標記,並發標記階段就是進行GC RootsTracing的過程
重新標記,Stop The World”,是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,但遠比並發標記的時間短
並發清除(CMS concurrent sweep)
6、優點:並發收集、低停頓
7、缺點:
對CPU資源非常敏感。
無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。
一款基於“標記—清除”算法實現的收集器
3.3 G1收集器
1、當今收集器技術發展的最前沿成果之一
2、G1是一款面向服務端應用的垃圾收集器。
3、優點:
並行與並發:充分利用多CPU、多核環境下的硬件優勢
分代收集:不需要其他收集器配合就能獨立管理整個GC堆
空間整合:“標記—整理”算法實現的收集器,局部上基於“復制”算法不會產生內存空間碎片
可預測的停頓:能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒
4、G1收集器的運作大致可划分為以下幾個步驟:
初始標記:標記一下GC Roots能直接關聯到的對象,需要停頓線程,但耗時很短
並發標記:是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序並發執行
最終標記:修正在並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄
篩選回收:對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計划
5、它將整個java堆划分成了多個大小相等的獨立區域(region)雖然還保留有新生代和老年代的概念,不再是物理隔離,都是region集合
3.4 垃圾收集器參數總結(*)
垃圾收集器參數總結
收集器設置:
-XX:+UseSerialGC:年輕串行(Serial),老年串行(Serial Old) (Serial+Serial Old)
-XX:+UseParNewGC:年輕並行(ParNew),老年串行(Serial Old) (ParNew+Serial Old)
-XX:+UseConcMarkSweepGC:年輕並行(ParNew),老年串行(CMS),備份(Serial Old)(ParNew+CMS)
-XX:+UseParallelGC:年輕並行吞吐(Parallel Scavenge),老年串行(Serial Old)
-XX:+UseParalledlOldGC:年輕並行吞吐(Parallel Scavenge),老年並行吞吐(Parallel Old)
垃圾回收統計信息:
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
並行收集器設置:
-XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
-XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)
-XX:+CMSIncrementalMode:設置為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設置並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。
4、JVM參數列表(*)
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-Xmx3550m:最大堆內存為3550M。
-Xms3550m:初始堆內存為3550m。
此值可以設置與-Xmx相同,以避免每次垃圾回收完成后JVM重新分配內存。
-Xmn2g:設置年輕代大小為2G。
整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代后,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。
-Xss128k設置每個線程的堆棧大小。:
JDK5.0以后每個線程堆棧大小為1M,在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在 3000~5000左右。
-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設置為4,則年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5
-XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的大小比值。
設置為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6
-XX:MaxPermSize=16m:設置持久代大小為16m。
-XX:MaxTenuringThreshold=15:設置垃圾最大年齡。
如果設置為0的話,則年輕代對象不經過Survivor區,直 接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值設置為一個較大值,則年輕代對象會在Survivor區進行多次復制,這樣可以增加對象 再年輕代的存活時間,增加在年輕代即被回收的概論。
5、理解GC日志
閱讀GC日志是處理java虛擬機內存問題的基礎技能,如下:
- 33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
- 100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
最前面的數字“33.125:”和“100.667:”代表了GC發生的時間
GC日志開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發生了Stop-The-World的。
接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發生的區域,這里顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱就會變為“[ParNew”,意為“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。
后面方括號內部的“3324K->152K(3712K)”含義是“GC前該內存區域已使用容量-> GC后該內存區域已使用容量 (該內存區域總容量)”。而在方括號之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆總容量)”。
再往后,“0.0025925 secs”表示該內存區域GC所占用的時間,單位是秒。有的收集器會給出更具體的時間數據
[Full GC 283.736: [ParNew: 261599K->261599K(261952K), 0.0000288 secs]
新生代收集器ParNew的日志也會出現“[Full GC”(這一般是因為出現了分配擔保失敗之類的問題,所以才導致STW)。如果是調用System.gc()方法所觸發的收集,那么在這里將顯示“[Full GC (System)”。
6、JVM案例演示
6.1 java堆OOM
/** * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * -XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機在出現內存異常時Dump出當前的內存堆 * 轉儲快照以便事后進行分析 * @author ll-t150 * */ public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } } }
結果:
解決方法:
這個時候需要明確是內存泄露(memory Leak)還是內存溢出(Memory OverFlow)
如果是內存泄露,可以進一步通過工具查看泄露對象到GC Roots的引用鏈,找到對象的類型信息,比較好定位泄露代碼的位置
若不存在泄漏,就應該檢查堆參數(-Xms與-Xmx),與機器物理內存對比是否可以調大
6.2 虛擬棧和本地方法棧OOM
拋出StackOverflowError異常,:線程請求的棧深度大於虛擬機所允許的深度
拋出OutOfMemoryError異常:虛擬機可以動態擴展,當擴展無法申請到足夠的內存
/** * 虛擬機棧和本地方法棧OOM測試 * VM Args:-Xss128k */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }
運行結果:
原因:在單線程下,無論是幀棧太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機就會拋出StackOverFlowError異常
創建線程導致內存溢出異常:
如果不是單線程,通過不斷地建立線程的方式可能產生內存溢出異常(Exception in thread “main” java.lang.OutOfMemoryError:unable to create new native thread)
但是這樣產生的內存溢出異常與棧空間是否足夠大不存在任何聯系,該情況下,為每個線程的棧分配的內存越大,反而越容易產生內存溢出異常
產生異常的原因:操作系統分配給每個線程的內存是有限制的,虛擬機提供了參數來控制java堆和方法區這兩部分內存的最大值
譬如:虛擬機可用內存是2GB,減去Xmx(最大堆內存),再減去MaxPermSize(最大方法區內存),程序計數器消耗內存很小,可以忽略掉,剩下的內存就分配給虛擬機棧和本地方法棧,每個線程分配到的棧容量越大,可以建立的線程數量自然就越少,建立線程時容易將剩下的內存耗盡
解決方法:如果建立過多線程導致的內存溢出,在不能減少線程數或者更換64位虛擬機情況下,只能通過減少最大堆和減少棧容量來換取更多的線程
可以通過Kylin處理的例子為例
7、JVM監控工具
例如:Jconsole
jconsole是一種集成了上面所有命令功能的可視化工具,可以分析jvm的內存使用情況和線程等信息。
通過JDK/bin目錄下的“jconsole.exe”啟動Jconsole后,將自動搜索出本機運行的所有虛擬機進程,不需要用戶使用jps來查詢了,雙擊其中一個進程即可開始監控。也可以“遠程連接服務器,進行遠程虛擬機的監控。”
查看某個進程的內存消耗 jmap -heap Pid;
8、關於JVM應用場景
1、關於String的一些知識
字符串是常量,存在於方法區,創建之后不能更改,且是共享區域
字符串池,初始為空,它由類String私有維護,它把程序中的String對象放到池中,只要為們用到值相同的String,就是同一個String對象,便於節省空間,但也不是所有時候所有String對象都在這個池中,有可能在堆中
理解字面常量:
例如:int i=6 ;6就是一個整數型字面常量
String s=”abc”; “abc” 就是一個字符串字面常量,
所有的字符串字面常量都在字符串常量池中,可以共享
例如:String s1=”abc”;String s2 =”abc” ;s1和s2都引用同一個字符串對象,且這個對象在常量池中,以此 s1==s2
字符串字面常量在類加載的時候實例化放到字符串池中
1、任何類任何包,值相同的字符串常數都引用同一個對象
2、常量表達式(constant expression)計算出來的字符串,也在常量池中
3、在程序運行時通過連接符(+)計算出來的字符串對象,是新創建的,他們不是字符串字面常量,不再池中,而是在堆內存中,因此引用對象不同
4、String類的intern方法,返回一個值相同的String對象,然后放入常量池中
什么是常量表達式:簡而言之,編譯時能確定的就是,運行時才能確定的就不是
什么是常量變量:被final修飾的,並且通過常量表達式初始化的變量
final String s2 = getA();//s2 不是常量變量,但是s2引用的“a”在常量池中
public String getA(){return "a";} // s2不是常量變量,因為getA()不是常量表達式
String s3=s2+”abc”;//此時s3不算常量表達式,因為s2不是常量變量
綜合例子:
public class StringTest { public static void main(String[] args) { String s = new String("abc"); //創建了幾個對象 String s1="abc"; String s2="a"; final String s21 ="a"; //這個時候 s21是一個常量變量 final String s22 = getA(); //s22不是常量變量 String s3 =s2+"bc"; String s31 =s21+"bc"; String s32 =s22+"bc"; String s4 = "a"+"bc"; //編譯時確定 String s5 =s3.intern(); //返回一個值相同的String對象,放入常量池中 System.out.println("s1==s3:"+(s1==s3));//false 運行時確定,new的對象放入堆中 System.out.println("s1==s31:"+(s1==s31));//true System.out.println("s1==s32:"+(s1==s32));//false System.out.println("s1==s4:"+(s1==s4));//true System.out.println("s1==s5:"+(s1==s5));//true } public static String getA(){ return "a"; } }