一、虛擬機類型
Sun HotSpot VM(主流)、BEA .JRockit VM、IBM .J9VM、Azul VM、Apache Harmony、Google Dalvik VM、Microsoft JVM
查看虛擬機類型 :java -version
二、JVM整體架構
JVM的內部體系結構分為三部分,分別是:類裝載器(ClassLoader)子系統,運行時數據區,和執行引擎
三、類加載器
三種類加載器
引導類加載器(BootStrapClassLoader)
負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
擴展類加載器(Extension ClassLoader)
負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
應用程式類加載器(Application ClassLoader)
負責加載用戶路徑(classpath)上的類庫。
JVM通過雙親委派模型進行類的加載,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類加載器。
雙親委派模型
當某個加載器需要加載某個.class文件時,它首先把這個任務委託給他的上級類加載器,如果父類加載器還存在其父類加載器,則遞歸這個操作,如果父類加載器可以完成類加載任務,就成功返回,如果上級的類加載器沒有加載,自己才會去加載這個類
優點:
避免了重複的加載
保護程序的安全,防止API被串改(如手動寫java.lang包String類)
類加載
驗證(Verify)
目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。
主要包括四種驗證,文件格式驗證,源數據驗證,字節碼驗證,符號引用驗證。
準備(Prepare)
為類變量分配內存並且設置該類變量的默認初始值,即零值;
這裡不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯式初始化;
類不會為實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨著對象一起分配到java堆中。
解析(Resolve)
將常量池內的符號引用轉換為直接引用的過程。
四、Java內存模型
本地方法棧(線程私有):提供native方法(通常用C編寫),在Execution Engine執行時加載本地方法庫
程序計數器(線程私有):每個線程啟動的時候,都會創建一個PC(Program Counter,程序計數器)寄存器,是一個指針,指向方法區中的字節碼(用來存儲指向下一條指令的地址,也是即將要執行的指令)由執行引擎讀取下跳指令,是一個非常小的空間,可以忽略不計
JVM Stack(虛擬機棧 線程私有):虛擬機棧有以下特點
1.內存小,跨平臺性,可以少量存儲一些變量,內存地址,用來管理Java方法的調用。
2.棧也就是方法,一個線程就是一個棧,所以說是線程安全的,是線程私有的
3.先進後出(執行完就出)棧幀相當於執行方法,調用完就出
4.當棧分配的空間不足時,那麼會出現StackOverflowError,通過設置-Xss:如-Xss128k調整棧大小
一個線程就是一個棧, 棧中包含棧幀:棧幀也就是方法---棧幀中包、局部變量表、操作數棧、動態連結、方法返回地址
局部變量表:變量表由若干個Slot組成,每個Slot 32位,double long因為是8個字節64位佔用2個Slot ,單個Slot可以存儲一個boolean、byte、char、short、float、reference和returnAddress的數據,兩個可以存儲一個long,或者double的數據
操作數棧:在變量進入局部變量表時都會經過操作數棧
動態連結:引用調用常量池中的數據。動態連結:引用調用常量池中的數據 方法的調用:靜態連結:早期綁定---在編譯時就確定了調用的方法,如static final private 修飾的方法 動態連結:晚期綁定:在運行時才確定了方法 比如接口
方法返回地址:
1.存放調用該方法的PC寄存器的值。
2.一個方法的結束,有兩種方式:正常執行完成,出現未處理的異常,非正常退出。
3.無論通過哪種方式退出,在方法退出後都返回該方法被調用的位置,方法正常退出時,調用pc計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址。
4.如果異常退出,返回地址是通過異常表來確定,棧幀中一般不會保存這部分信息。
5.正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他上一層調用者產生任何返回值
以Math.java為例,計算(a+b)*10
首先使用javap -c 對字節碼文件進行反彙編(javap -c Math.class > Math.txt),下圖截取於Math.txt部分內容
線程首先執行main方法,生成main方法棧幀,然後執行calculate方法生成棧幀,下圖是calculate方法執行完第6行時
第9行同理計算彈出10與5後相乘壓入棧底,第10行返回50,返回後calculate()方法出棧,main方法繼續執行。
堆(Heap 線程共享)
虛擬機啟動時創建,用於存放對象實例,所有的對象(包含常量池)都分配在堆內存,當對象無法在該空間申請到內存時則OOM,同時也是GC的主要區域,可通過-Xmx -Xms參數來分別指定最大堆和最小堆
①新生代
簡介
新生代使用了複製算法
新生代為gc的重點對象,經官方測試70%對象都生命周期都會在新生代中完結
新生代又分為了eden、survivor1、survivor2
內存比例分默認為:8:1:1;j8默認開通自適配比例可能有所變化6:2:2
新生代收集器:Minor GC/Young GC
eden(新生區)
當初始加載對象時會進入新生區
survivor(倖存區)
倖存區又分為from 和 to 誰為空誰為to ,始終都會有一個區域為空。
倖存區不會主動進行垃圾回收,只會eden回收時才會附帶進行gc
當在倖存區中的閾值達到了15後(默認15可修改)會自動進入老年代
當新生區(eden)出現了內存不足時,會進行YGC,那麼會將沒有指針的對象回收,還有指針引向的對象放入survivor1或者survivor2區域中,eden清空,數據放入一個survivor中。—當第二次進行gc那麼會將eden數據放入另一個空的survivor中,並且將當前survivor中有效數據,放入空的survivor中,以此類推。
TLAB(快速分配策略)
由於堆中的空間都是共享的,所以存在線程安全的問題,這時候就出現了TLAB
緩衝區的線程私有的 TLAB ,保證了安全性,是在eden 中只佔1%內存可能成功也可能失敗,快速分配策略
聲明
在一個對象進入內存時 會進入eden,如果滿了(YGC進行回收沒有引用的,如果還有引用的)會放入s1或者s0這就涉及到to from哪個為空就是to,(下次eden再次滿了會將有數據的【舉例s1】中的數據放入s0,並且進行迭代版本)以此類推,當某個對象迭代閾值的次數達到默認15此後,(當然也會有特殊的優化:如當survivor區域中相同年齡的內存總和大於survivor的一半內存,會將大於等於平均年齡的對象提前放入老年代)會放入老年代 關於YGC 全程(YoungGC) 也可以為(Minor GC) s1,0是不會有單獨的gc回收只會被動的依賴於eden的gc當eden進行gc時會自動回收s1,s0
②老年代
特性
較大的對象數據會放入老年代
老年代的數據都是相對於持久的不會頻繁的gc
(MajorGC / Old GC) 在進行majorgc時會至少進行一次minorGc ,而且majorgc的效率是比minorGc 慢10倍的
老年代收集器:MajorGC / Old GC 要區分與Full GC
Full GC :是進行整堆的回收
③逃逸分析什麼是逃逸?
也就是如果在方法內創建對象,並且return進行傳出,或者賦值到外部的變量,那麼就進行了逃逸。
-XX:+DoEscapeAnalysis (JDK1.8之後默認開啟)
-XX:+DoEscapeAnalysis(關閉)
逃逸分析包括以下
棧上分配
也就是將對象直接分配到棧上,跟隨棧的消亡而消亡,減少了gc(棧中沒有gc),提高了性能、速度。
同步省略
因為是每個棧獨有的,一個棧也就是一個線程所以不存在同步安全的問題。
分離對象或者標量替換
擴充:一個類代表一個:聚合量,標量是無法分析的最小數據,聚合量可以分析為標量,也就是分析屬性
也就是當加載一個pojo類時,不會創建對象而是,標量替換進行分析成一個個小的屬性,減少了內存,提高了性能。
但是基於hotSpot 虛擬機這項技術並不成熟,因為還需要進行判斷是否 屬於逃逸,如果沒有逃逸,可能會浪費了判斷的時間等一些問題。
但是最後標量替換還是引用到了hotSpot虛擬機中
簡單說,說有的定義的方法信息都存在該區域,包含靜態常量、常量、類信息(構造方法、接口定義)、運行時常量
方法區在Hotspot中又稱永久代、元空間(非堆)[這裡的永久代、元空間在JVM虛擬機規範中是不等價的]
在jdk1.7(包含7)以前稱為永久代,並且方法區是在JVM中
在jdk1.8以後稱為元空間,並將方法區移除JVM的約束
使用垃圾收集器:FullGC
其中內含了常量池、域(Field)信息、已裝載類信息、方法信息、JIT代碼緩存
方法區是直接內存(也就是直接分配在內存上的)
存儲遷移過程
為什麼要移除堆空間?
顧名思義(永久代)即經常不會被回收的,跟隨電腦的內存進行擴展,可以減少gc,提高性能,並減少了內存溢出的風險。
常量池
常量池包含各種字面量和對類型、域和方法的符號引用。
參數設置
初始值 -XX:MetaspaceSize=100m
最大值 -XX:MaxMetaspaceSize=100m
建議不要隨意修改設置,因可以跟隨本地內存變化而進行擴充變化
移除永久代原因:為融合HotSpot JVM與JRockit VM(新JVM技術)而做出的改變,因為JRockit沒有永久代。
有了元空間就不再會出現永久代OOM問題了!
新生成的對象首先放到年輕代Eden區,當Eden空間滿了,觸發Minor GC,存活下來的對象移動到Survivor0區,Survivor0區滿後觸發執行Minor GC,Survivor0區存活對象移動到Suvivor1區,這樣保證了一段時間內總有一個survivor區為空。經過多次Minor GC仍然存活的對象移動到老年代。
老年代存儲長期存活的對象,佔滿時會觸發Major GC=Full GC,GC期間會停止所有線程等待GC完成,所以對響應要求高的應用儘量減少發生Major GC,避免響應超時。
Minor GC :清理年輕代
Major GC :清理老年代
Full GC :清理整個堆空間,包括年輕代和永久代
所有GC都會停止應用所有線程。
將對象根據存活概率進行分類,對存活時間長的對象,放到固定區,從而減少掃描垃圾時間及GC頻率。針對分類進行不同的垃圾回收算法,對算法揚長避短。
為什麼survivor分為兩塊相等大小的倖存空間?主要為了解決碎片化。如果內存碎片化嚴重,也就是兩個對象佔用不連續的內存,已有的連續內存不夠新對象存放,就會觸發GC。
JVM堆內存常用參數參數描述-Xms堆內存初始大小,單位m、g-Xmx(MaxHeapSize)堆內存最大允許大小,一般不要大於物理內存的80%-XX:PermSize非堆內存初始大小,一般應用設置初始化200m,最大1024m就夠了-XX:MaxPermSize非堆內存最大允許大小-XX:NewSize(-Xns)年輕代內存初始大小-XX:MaxNewSize(-Xmn)年輕代內存最大允許大小,也可以縮寫-XX:SurvivorRatio=8年輕代中Eden區與Survivor區的容量比例值,默認為8,即8:1-Xss堆棧內存大小垃圾回收算法(GC,Garbage Collection)紅色是標記的非活動對象,綠色是活動對象。
標記-清除(Mark-Sweep)
GC分為兩個階段,標記和清除。首先標記所有可回收的對象,在標記完成後統一回收所有被標記的對象。同時會產生不連續的內存碎片。碎片過多會導致以後程序運行時需要分配較大對象時,無法找到足夠的連續內存,而不得已再次觸發GC。
複製(Copy)
將內存按容量劃分為兩塊,每次只使用其中一塊。當這一塊內存用完了,就將存活的對象複製到另一塊上,然後再把已使用的內存空間一次清理掉。這樣使得每次都是對半個內存區回收,也不用考慮內存碎片問題,簡單高效。缺點需要兩倍的內存空間。
標記-整理(Mark-Compact)
也分為兩個階段,首先標記可回收的對象,再將存活的對象都向一端移動,然後清理掉邊界以外的內存。此方法避免標記-清除算法的碎片問題,同時也避免了複製算法的空間問題。
一般年輕代中執行GC後,會有少量的對象存活,就會選用複製算法,只要付出少量的存活對象複製成本就可以完成收集。而老年代中因為對象存活率高,沒有額外過多內存空間分配,就需要使用標記-清理或者標記-整理算法來進行回收。
串行收集器(Serial)
比較老的收集器,單線程。收集時,必須暫停應用的工作線程,直到收集結束。
並行收集器(Parallel)
多條垃圾收集線程並行工作,在多核CPU下效率更高,應用線程仍然處於等待狀態。
CMS收集器(Concurrent Mark Sweep)
CMS收集器是縮短暫停應用時間為目標而設計的,是基於標記-清除算法實現,整個過程分為4個步驟,包括:
初始標記(Initial Mark)
並發標記(Concurrent Mark)
重新標記(Remark)
並發清除(Concurrent Sweep)
其中,初始標記、重新標記這兩個步驟仍然需要暫停應用線程。初始標記只是標記一下GC Roots能直接關聯到的對象,速度很快,並發標記階段是標記可回收對象,而重新標記階段則是為了修正並發標記期間因用戶程序繼續運作導致標記產生變動的那一部分對象的標記記錄,這個階段暫停時間比初始標記階段稍長一點,但遠比並發標記時間段。
由於整個過程中消耗最長的並發標記和並發清除過程收集器線程都可以與用戶線程一起工作,所以,CMS收集器內存回收與用戶一起並發執行的,大大減少了暫停時間。
初始標記與CMS一樣,標記一下GC Roots能直接關聯到的對象。並發標記從GC Root開始標記存活對象,這個階段耗時比較長,但也可以與應用線程並發執行。而最終標記也是為了修正在並發標記期間因用戶程序繼續運作而導致標記產生變化的那一部分標記記錄。最後在篩選回收階段對各個Region回收價值和成本進行排序,根據用戶所期望的GC暫停時間來執行回收。
垃圾收集器參數參數描述-XX:+UseSerialGC串行收集器-XX:+UseParallelGC並行收集器-XX:+UseParallelGCThreads=8並行收集器線程數,同時有多少個線程進行垃圾回收,一般與CPU數量相等
-XX:+UseParallelOldGC指定老年代為並行收集-XX:+UseConcMarkSweepGCCMS收集器(並發收集器)-XX:+UseCMSCompactAtFullCollection開啟內存空間壓縮整和,防止過多內存碎片
-XX:CMSFullGCsBeforeCompaction=0表示多少次Full GC後開始壓縮和整理,0表示每次Full GC後立即執行壓縮和整理
-XX:CMSInitiatingOccupancyFraction=80%表示老年代內存空間使用80%時開始執,行CMS收集,防止過多的Full GC
-XX:+UseG1GCG1收集器-XX:MaxTenuringThreshold=0在年輕代經過幾次GC後還存活,就進入老年代,0表示直接進入老年代
為什麼會堆內存溢出?在年輕代中經過GC後還存活的對象會被複製到老年代中。當老年代空間不足時,JVM會對老年代進行完全的垃圾回收(Full GC)。如果GC後,還是無法存放從Survivor區複製過來的對象,就會出現OOM(Out of Memory)。
OOM(Out of Memory)異常常見有以下幾個原因:
1)老年代內存不足:java.lang.OutOfMemoryError:Javaheapspace
2)永久代內存不足:java.lang.OutOfMemoryError:PermGenspace
3)代碼bug,佔用內存無法及時回收。
OOM在這幾個內存區都有可能出現,實際遇到OOM時,能根據異常信息定位到哪個區的內存溢出。
可以通過添加個參數-XX:+HeapDumpOnOutMemoryError,讓虛擬機在出現內存溢出異常時Dump出當前的內存堆轉儲快照以便事後分析。
熟悉了JAVA內存管理機制及配置參數,下面是對JAVA應用啟動選項調優配置:
JAVA_OPTS="-server -Xms512m -Xmx2g -XX:+UseG1GC -XX:SurvivorRatio=6 -XX:MaxGCPauseMillis=400 -XX:G1ReservePercent=15 -XX:ParallelGCThreads=4 -XX:
ConcGCThreads=1 -XX:InitiatingHeapOccupancyPercent=40 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:../logs/gc.log"
設置堆內存最小和最大值,最大值參考歷史利用率設置
設置GC垃圾收集器為G1
啟用GC日誌,方便後期分析