JVM和垃圾回收機制
- JVM的作用:解釋運行字節碼程序,消除平臺相關性。JVM將Java字節碼解釋為具體平臺的具體指令。一般的高級語言如要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入JVM后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。
- JVM常見問題:https://mp.weixin.qq.com/s/Xo3_ZTVruhFSTMSrmJLvrw
- JVM知識結構圖
- 程序計數器:內存空間小,線程私有。字節碼解釋器工作是就是通過改變這個計數器的值來選取下一條需要執行指令的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴計數器完成。唯一一個在 Java 虛擬機規范中沒有規定任何 OutOfMemoryError 情況的區域。
- 虛擬機棧:線程私有,生命周期和線程一致。描述的是 Java 方法執行的內存模型,每個方法在執行時都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行結束,就對應著一個棧幀從虛擬機棧中入棧到出棧的過程。
- 本地方法棧:Java 虛擬機棧為虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。
- 堆:對于絕大多數應用來說,這塊區域是 JVM 所管理的內存中最大的一塊。線程共享,主要是存放對象實例和數組。內部會劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer, TLAB)。可以位于物理上不連續的空間,但是邏輯上要連續。
- 方法區:屬于共享內存區域,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
- 運行時常量池:屬于方法區一部分,用于存放編譯期生成的各種字面量和符號引用。編譯器和運行期(String 的 intern() )都可以將常量放入池中。內存有限,無法申請時拋出 OutOfMemoryError。
- 直接內存:非虛擬機運行時數據區的部分
- DR 數據寄存器、IR 指令寄存器、PC 程序計數器
- 一條指令分為:操作+地址,IR保存一條指令,用于發后續信號以便于進行真正的執行、PC用于保存下一條指令的地址
- 在執行一條指令的時候,先把指令存內存取到DR,然后再取到IR,然后再交給指令譯碼器來轉換指令,再向操作控制器發出對應的信號。為了保證程序的順利執行,所以才需要PC來保存下一條指令的地址,由于大部分指令地址都是連續的,所以+1即可
本地內存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。其關系模型圖如下圖所示:
- jps -v 可以查看 jvm 進程顯示指定的參數
- 使用 -XX:+PrintFlagsFinal 可以看到 JVM 所有參數的值
- jinfo 可以實時查看和調整虛擬機各項參數
- jstat -gc 12538 5000即會每5秒一次顯示進程號為12538的java進程的GC情況
當JVM因為沒有足夠的內存來為對象分配空間并且垃圾回收器也已經沒有空間可回收時,就會拋出 java.lang.OutOfMemoryError。
- 加載了大量的Class(類)
- 采用cglib等反射機制
- 過多的常量也會導致方法區溢出,尤其是字符串
- 在單一的Tomcat實例下運行多個Web應用程序(大量jsp頁面)
- 在運行的Tomcat實例中反復"熱部署"Web應用程序
- 先查看應用進程號pid:ps -ef | grep 應用名
- 查看pid垃圾回收情況:jstat -gc pid 5000(時間間隔)
- dump 查看方法棧信息:jstack -l pid
- dump 查看JVM內存分配以及使用情況:jmap -heap pid
- dump jvm二進制的內存詳細使用情況
http://www.rzrgm.cn/zhengbin/p/6490953.html
一個對象本身的內在結構需要一種描述方式,這個描述信息是以字節碼的方法存儲在方法區中的。在 HotSpot 虛擬機中,對象在內存中存儲布局分為 3 塊區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。
- 對象頭
- 第一部分用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳、對象分代年齡,這部分信息稱為"Mark Word";Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息,它會根據自己的狀態復用自己的存儲空間。
- 第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例;
- 如果對象是一個 Java 數組,那在對象頭中還必須有一塊用于記錄數組長度的數據。因為虛擬機可以通過普通 Java 對象的元數據信息確定 Java 對象的大小,但是從數組的元數據中無法確定數組的大小。
在 64 位系統及 64 位 JVM 下,開啟指針壓縮,那么頭部存放 Class 指針的空間大小還是4字節,而 Mark Word 區域會變大,變成 8 字節,也就是頭部最少為 12 字節,如下表所示:
- 實例數據
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在 Java 源碼中定義順序的影響。
- 對齊填充
對齊填充不是必然存在的,沒有特別的含義,它僅起到占位符的作用。由于 HotSpot VM 的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,也就是說對象的大小必須是 8 字節的整數倍。對象頭部分是 8 字節的倍數,所以當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
JVM逃逸分析
逃逸是指在某個方法之內創建的對象,除了在方法體之內被引用之外,還在方法體之外被其它變量引用到;這樣帶來的后果是在該方法執行完畢之后,該方法中創建的對象將無法被GC回收,由于其被其它變量引用。正常的方法調用中,方法體中創建的對象將在執行完畢之后,將回收其中創建的對象;故由于無法回收,即稱為逃逸。
String test2;
/**
* 逃逸
*/
void test02() {
test2 = "test2";
}
逃逸分析參數設置:
-XX:+DoEscapeAnalysis//使用
-XX:-DoEscapeAnalysis//不用
類加載
- 類加載器:
- 類加載過程:加載,驗證,準備,解析,初始化
- 加載:加載是類加載過程中的一個階段,這個階段會在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的入口。
- 驗證:確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
- 準備:準備階段是正式為類變量分配內存并設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。
- 解析:解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。
- 初始化:初始化階段是執行類構造器<client>方法的過程。到了初始階段,才開始真正執行類中定義的Java程序代碼。
- 雙親委派模型
- 定義:除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器,這里類加載器之間的父子關系一般通過組合(Composition)關系來實現,而不是通過繼承(Inheritance)的關系實現。
- 工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載,而是把這個請求委派給父類加載器,每一個層次的加載器都是如此,依次遞歸,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成此加載請求(它搜索范圍中沒有找到所需類)時,子加載器才會嘗試自己加載。
- 優點:使用雙親委派模型來組織類加載器之間的關系,使得Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放再rt.jar中,無論哪個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。
- 打破雙親委派機制的方法:重寫loadclass()方法
- 打破雙親委派機制的例子:
- Tomcat,應用的類加載器優先自行加載應用目錄下的 class,并不是先委派給父加載器,加載不了才委派給父加載器。打破的目的是為了完成應用間的類隔離。
- JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,當平臺及應用程序類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統模塊中,如果可以找到這樣的歸屬關系,就要優先委派給負責那個模塊的加載器完成加載。打破的原因,是為了添加模塊化的特性。
- 沙箱安全機制:防止惡意代碼污染java源代碼。
符號引用與直接引用
- 符號引用 :符號引用以一組符號來描述所引用的目標。符號引用可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,符號引用和虛擬機的布局無關。個人理解為:在編譯的時候一個每個java類都會被編譯成一個class文件,但在編譯的時候虛擬機并不知道所引用類的地址,就用符號引用來代替,而在這個解析階段就是為了把這個符號引用轉化成為真正的地址的階段。
- 直接引用 :直接引用和虛擬機的布局是相關的,不同的虛擬機對于相同的符號引用所翻譯出來的直接引用一般是不同的。如果有了直接引用,那么直接引用的目標一定被加載到了內存中。直接引用可以是:
- 直接指向目標的指針。(個人理解為:指向對象,類變量和類方法的指針)
- 相對偏移量。(指向實例的變量,方法的指針)
- 一個間接定位到對象的句柄。
JVM加載class文件的原理
JVM中類的裝載是由ClassLoader和它的子類來實現的,Java ClassLoader 是一個重要的Java運行時系統組件,它負責在運行時查找和裝入類文件的類。
- Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬盤讀取到內存中。在寫程序的時候,我們幾乎不需要關心類的加載,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。
- 類裝載方式,有兩種
- 隱式裝載,程序在運行過程中當碰到通過new 等方式生成對象時,隱式調用類裝載器加載對應的類到jvm中
- 顯式裝載,通過class.forname()等方法,顯式加載需要的類,隱式加載與顯式加載的區別:兩者本質是一樣的。
Java類的加載是動態的,它并不會一次性將所有類全部加載后再運行,而是保證程序運行的基礎類(像是基類)完全加載到jvm中,至于其他類,則在需要的時候才加載。這當然就是為了節省內存開銷。
Java程序的運行過程
Java 程序的運行必須經過編寫、編譯和運行 3 個步驟。
- 編寫:是指在 Java 開發環境中進行程序代碼的輸入,最終形成后綴名為 .java 的 Java 源文件。
- 編譯:是指使用 Java 編譯器對源文件進行錯誤排査的過程,編譯后將生成后綴名為 .class 的字節碼文件,不像C語言那樣生成可執行文件。
- 運行:是指使用 Java 解釋器將字節碼文件翻譯成機器代碼,執行并顯示結果。
GC Roots
作為GCRoots的對象包括下面幾種:
- 虛擬機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。
- 方法區中的類靜態屬性引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧中JNI(Native方法)引用的對象。
對象可達性分析
- 如果對象在進行可達性分析后發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記并進行一次篩選,篩選條件為是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了,則均視作不必要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法并且該finalize方法并沒有被執行過,那么,這個對象會被放置在一個叫F-Queue的隊列中,之后會由虛擬機自動建立的、優先級低的Finalizer線程去執行,而虛擬機不必要等待該線程執行結束,即虛擬機只負責建立線程,其他的事情交給此線程去處理。
- 對F-Queue中對象進行第二次標記,如果對象在finalize方法中拯救了自己,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其他變量,那么在第二次標記的時候該對象將從"即將回收"的集合中移除,如果對象還是沒有拯救自己,那就會被回收。
垃圾回收機制
堆分為新生代和老年代,新生代默認占總空間的 1/3,老年代默認占 2/3。新生代使用復制算法,有 3 個分區:Eden、To Survivor、From Survivor,它們的默認占比是 8:1:1。當新生代中的 Eden 區內存不足時,就會觸發 Minor GC,過程如下:
- 在 Eden 區執行了第一次 GC 之后,存活的對象會被移動到其中一個 Survivor 分區;
- Eden 區再次 GC,這時會采用復制算法,將 Eden 和 from 區一起清理,存活的對象會被復制到 to 區;
- 移動一次,對象年齡加 1,對象年齡大于一定閥值會直接移動到老年代
- Survivor 區相同年齡所有對象大小的總和 > (Survivor 區內存大小 * 目標使用率)時,大于或等于該年齡的對象直接進入老年代。其中這個使用率通過
-XX:TargetSurvivorRatio 指定,默認為 50%
- Survivor 區內存不足會發生擔保分配
- 年齡超過指定大小的對象可以直接進入老年代
- Major GC,指的是老年代的垃圾清理,但并未找到明確說明何時在進行Major GC
- FullGC,整個堆的垃圾收集,觸發條件:
- 每次晉升到老年代的對象平均大小>老年代剩余空間
- MinorGC后存活的對象超過了老年代剩余空間
- 元空間不足
- System.gc():并不是立即執行* finalize方法,而是通知系統進行垃圾回收,但具體什么時候執行還是要由系統自行決定。
- CMS GC異常,promotion failed:MinorGC時,survivor空間放不下,對象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC時,同時有對象要放入老年代,而老年代空間不足造成
- 堆內存分配很大的對象
垃圾回收算法
- 引用計數法:給對象中添加一個引用計數器,每當一個地方引用這個對象時,計數器值+1;當引用失效時,計數器值-1。任何時刻計數值為0的對象就是不可能再被使用的。
- 缺點:(1)每次給對象賦值時都要維護引用計數器,且計數器本身也有一定的消耗;(2)較難處理循環引用
- 復制算法:(年輕代)
- 優點:(1)不產生內存碎片;(2)速度快
- 缺點:浪費10%的內存空間
- 標記清除算法(老年代):先標記出要回收的對象,然后統一回收這些對象
- 優點:節省空間
- 缺點:產生內存碎片;
- 標記整理算法(老年代):先標記清除,再次掃描并往一端滑動存活對象。
- 優點:不產生內存碎片;
- 缺點:需要移動對象的成本,耗時嚴重
垃圾收集器
- Serial收集器:新生代收集器,使用停止復制算法,使用一個線程進行GC,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內存回收(這也是虛擬機在Client模式下運行的默認值)
- Serial Old收集器:老年代收集器,單線程收集器,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,只留幸存的對象,壓縮是將移動對象,將空間填滿保證內存分為2塊,一塊全是對象,一塊空閑)算法,使用單線程進行GC,其它工作線程暫停(注意,在老年代中進行標 記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
- ParNew收集器:新生代收集器,使用停止復制算法,Serial收集器的多線程版,用多個線程進行GC,其它工作線程暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。
- Parallel Scavenge 收集器:新生代收集器,使用停止復制算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間,比如:JVM運行100分鐘,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行后臺運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合用戶交互,提高用戶體驗)。使用-XX:+UseParallelGC開關控制使用 Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間占總時間的比例,默認99,即 1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數只對Parallel Scavenge有效)
- Parallel Old收集器:老年代收集器,多線程,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復制到預先準備好的區域,而不是像Sweep(清 理)那樣清理廢棄的對象)算法,在Parallel Old執行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現后(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現Parallel Scavenge收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
- CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于獲取最短回收停頓時間,使用標記清除算法,多線程,優點是并發收集(用戶線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內存回收,優先使用ParNew+CMS,當用戶線程內存不足時,采用備用方案Serial Old收集。解決內存碎片問題:讓CMS在進行一定次數的Full GC(標記清除)的時候進行一次標記整理算法。
- 初始標記(阻塞):標記GC roots直接關聯的對象,速度快
- 并發標記:GC Roots Tracing過程
- 重新標記:修正并發標記期間用戶進程繼續運行而產生變化的標記,耗時比初始標記長,但遠小于并發標記
- 并發清除:清除標記的對象
- G1(Garbage First)收集器:區域化垃圾收集器,物理上不區分新生代和老年代。采用標記整理的算法,與CMS相比:(1)不會產生內存碎片;(2)用戶可以指定垃圾回收時間(錯峰垃圾回收)。
- 初始標記(InitingMark):標記GC Roots,會STW(Stop The World),一般會復用YoungGC的暫停時間。初始標記會設置好所有分區的NTAMS值。
- 根分區掃描(RootRegionScan):根據初始標記階段確定的GC根元素,掃描這些元素所在region,獲取對老年代的引用,并標記被引用的對象。該階段與應用線程并發執行,也就是說沒有STW停頓,必須在下一次年輕代GC開始之前完成。
- 并發標記(ConcurrentMark):遍歷整個堆,查找所有可達的存活對象。若發現區域對象中的所有對象都是垃圾,那這個區域會被立即回收。此階段與應用線程并發執行,也允許被年輕代GC打斷。
- 最終標記(Remark):此階段有一次STW暫停,以完成標記周期。G1會清空SATB緩沖區,跟蹤未訪問到的存活對象,并進行引用處理。
- 清除階段(Clean UP):這是最后的子階段,G1在執行統計和清理RSet時會有一次STW停頓。在統計過程中,會把完全空閑的region標記出來,也會標記出適合于進行混合模式GC的候選region。清理階段有一部分是并發執行的,比如在重置空閑region并將其加入空閑列表時。
- G1收集器相關配置如下:
-Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
- G1將新生代,老年代的物理空間劃分取消了。
- G1算法將堆劃分為若干個區域(Region),它仍然屬于分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作。H區域可以是連續的用來分配比較大的對象
- 可預測的停頓:這是G1相對于CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用這明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
- ZGC
ZGC給Hotspot Garbage Collectors增加了兩種新技術:著色指針和讀屏障。ZGC的標記分為三個階段。
- 第一階段是STW,其中GC roots被標記為活對象。
- 第二階段,同時遍歷對象圖并標記所有可訪問的對象。在此階段期間,讀屏障針使用掩碼測試所有已加載的引用,該掩碼確定它們是否已標記或尚未標記,如果尚未標記引用,則將其添加到隊列以進行標記。
- 在遍歷完成之后,有一個最終的,時間很短的的Stop The World階段,這個階段處理一些邊緣情況(我們現在將它忽略),該階段完成之后標記階段就完成了。
浙公網安備 33010602011771號