JVM、GC看这一篇就够了!
Class類加載
- 加載Loading
通過全限定類名,把一個類從二進制的Class文件加載到內存當中
- 類加載器
- Bootstrap:啟動類加載器。加載lib/rt.jar charset.jar核心類 C++實現
- Extension:擴展類加載器。加載擴展jar包 jre/lib/ext/*.jar
- Application/System:系統類加載器。加載classpath指定內容,他是我們java中默認的類加載器,我們寫的class都是用他加載的
- Custom ClassLoader:用戶類加載器。自定義ClassLoader
各個加載器之間并不是繼承的關系,只是一個上級下級的關系
- 雙親委派
當一個調用一個類的時候,會先去檢查該類是否已經加載過,如果加載了就直接調用;否則會找相對應的類加載器來加載該類。因此分為檢查過程和加載過程。
檢查過程是一個自底向上的過程,這也是比較好理解的,因為越低層,就代表可控性越高,只有比較核心底層的才會放在頂層的加載器。
當詢問到最頂層Bootstrap加載器都沒有加載過這個類,那么就認為該類沒有被加載過。
加載過程是一個自頂向下的過程,每一個類加載器都有自己負責的部分,如果這個類不屬于該加載器負責的范圍,則會向下詢問。
- 雙親委派的意義
- 連接Linking
- 校驗 verification
檢驗二進制流Class文件是否符合jvm虛擬機的要求 主要包括四種驗證:文件格式的驗證,元數據的驗證,字節碼驗證,符號引用驗證。
- 準備 Preparation
內存開辟空間,并且給成員變量賦默認值 注意:這里是賦默認值,并不是賦初始值,比如int a = 1; 在這個過程中是對a賦0,并不是賦值為1,在初始化過程的時候才是賦值為1 這也是為什么DCL問題中,需要加volatile關鍵字的原因
- 解析 Resolution
將符號引用轉換為直接引用
- 初始化Initializing
給變量賦初始值
以JDK1.8為例,JVM的組成部分如圖
指令重排序問題
我們知道CPU的執行速度是遠比內存執行的速度高的,我們的代碼本質上就是一條一條的指令,或者是多條指令組合而成
我們通過debug調試的時候,知道代碼都是一行一行執行的,但是在CPU中,指令是按順序一條一條的執行嗎?
答案是不一定!
int a = 1; int b = 2; 復制代碼比如在這個例子中,定義了兩個變量,兩行代碼之間沒有關系,CPU為了追求速度,可以不按順序地去執行。
不管指令最后怎么排序,最后要保證該代碼在單機(單線程)的情況下,運行的結果一致,也就是as-if-serial原則
- as-if-serial
不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。
- DCL單例是否需要添加volatile
public class Instance {private int a = 1;//是否添加volatile 關鍵字private volatile static Instance ins = null;public static Instance getInstance(){if (ins == null){synchronized (Instance.class){if (ins == null){ins = new Instance();}}}return ins;} } 復制代碼答案是需要的!
我們對象創建的時候,要經過三個步驟。
當線程1調用DCL單例創建實例對象的時候,當他完成了第一步,給成員變量賦默認值。此時該實例對象是可以被獲取到的。
也就是說在DCL最外層判斷 ins 是不等于null的,因此他會直接返回ins。
但是這個ins只完成了賦默認值里面的 a = 0
假設我們一個訂單號a是從1000開始的,而當前獲取到的a是0,那么就會引發很多問題。
加了volatile之后,就能保證線程的可見性,并且禁止了指令重排序。至于感興趣的小伙伴可以去搜索關鍵字內存屏障,volatile是通過LLLS、SSSL的內存屏障組合保證指令的禁止重排序。這里就點到為止,不做過多介紹。
JVM中各個部分的組成說明
堆
用來存放對象實例,包括數組(jdk1.7之后,字符串常量池從永久代剝離出來,存放在堆中)
堆空間的默認內存分配:
- 老年代占三分之二
- 新生代占三分之一
- 其中伊甸(Eden)區占十分之八
- from區、to區各自占十分之一
開發者可以通過參數對分區大小進行配置
-XX:SurvivorRatio=8 (新生代分區比例 8:2)
同時堆也是垃圾回收器(GC)發生的地方,當年輕代滿的時候會發生YGC,當老年代滿的時候會發生FGC,后面會介紹每種垃圾回收器的各個算法
- 字符串常量池
在jdk1.7之前,存在于方法區中
在jdk1.7及以后,搬到了堆中
1.7之前String pool里面存放的都是字符串常量,在1.7之后,里面存放的是字符串對象的引用。
String s1 = "abc"; String s2 = "abc"; 復制代碼他會先去常量池中找,是否存在有"abc"的對象引用,如果有則返回該地址;否則才會自己創建一個"abc"的String對象。
- 靜態常量池
- 靜態常量池,也叫做Class常量池,里面存放著一些class文件相關的信息,比如版本、字段、方法、接口等描述信息
- 每個class文件都有一個class常量池
- 運行時常量池
- 運行時常量池存在于內存當中,里面的符號引用可以被解析為直接引用
- 當類加載之后,常量池中的數據會在運行時常量池中存放
- 年輕代
指剛new出來的對象,尚未經過GC過程的對象,存放在伊甸區(Eden) 年輕代會頻繁發生FGC,當發生FGC時,會把Eden區的對象通過復制算法(copy)拷貝到from區。 當發生下一次FGC的時候,會把Eden區和from區的對象拷貝到to區;再再下次FGC的時候會把Eden區和to區拷貝到from區,然后重復上面的過程。每次都會計數+1,默認情況下加到15的時候(用戶可以設置)就會變成老年代的對象。
- 老年代
老年代是指經過多次GC之后,依舊幸存下來的對象。 老年代的對象,要么是年輕代的對象轉變過來。要么是一些大的對象直接變成老年代。 如果老年代滿了之后,會觸發FGC,FGC的頻率并不高。
本地方法棧
本地方法棧為虛擬機使用到的native方法服務 虛擬機棧是為虛擬機執行java方法服務
- native方法
Native Method就是一個java調用非java代碼的接口。一個Native Method是這樣一個java的方法:該方法的實現由非java語言實現,比如C。
虛擬機棧
虛擬機棧,本質上就是一個棧的結構。它里面的每一個棧幀,其實就是一個方法。 我們可以看到棧幀的主要組成部分是4個,分別是局部變量表、操作數棧、動態鏈接和方法出口信息。
- 局部變量表
- 八大原始類型(java中小寫開頭的):boolean、bye、char、short、int、float、long、double以及對象引用
- 對象引用
對象引用分為直接指針引用和間接(句柄)引用
- 直接指針引用
直接指引引用,指向對方起始地址的引用指針
優點:
- 訪問速度快,直接到達
缺點:
當對象發生移動的時候,需要更改引用指針,比如發生GC的時候
- 句柄引用
指向某個數據結構,里面包含了該對象的實例數據指針和類型數據指針
優點:
- 當對象發生移動的時候,只需要改結構體中的實例數據指針即可,在GC的時候
缺點:
- 需要開辟額外的空間存放結構體
#-## 操作數棧
操作數棧也有的叫操作棧 比如執行一個簡單的兩個參數相加的函數 需要先從操作數棧中將2個數值出棧,然后運算完了之后再將結果入棧 其中涉及到的指令: iload iload iadd istore之類的
- 動態鏈接
每一個棧幀內部都包含一個指向運行時常量池中該棧幀所屬方法的引用。 包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接( Dynamic Linking)。比如: invokedynamic指令(在1.7新增的指令)
在Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用( symbolic Reference)保存在class文件的常量池里。 比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那么動態鏈接的作用就是為了將這些符號引用轉換為調用方法的直接引用。
- 方法出口信息
每個java方法在被調用的時候,都會創建一個棧幀并入棧 一旦完成調用,會自動出棧,因此不需要gc進行回收,堆就需要gc進行回收
程序計數器
jvm中占用空間極小,幾乎可以忽略不計,但是計算速度也是最快的區域
用于存儲指向下一條指令的地址,也就是即將執行的指令代碼。
直接內存(堆外內存)
屬于堆外內存,可以通過native方法分配
直接內存IO讀寫的性能要優于普通的堆內存
不受GC的影響,需要手動回收
直接內存是通過Unsafe類來實現的
方法區
元數據區和永久代本質上都是方法區的實現,方法區是一個邏輯上的抽象概念,每個版本的jvm都有其不同的實現方式,元數據區是hotspot的實現方式
- 永久代(Permanent)
在java1.7版本時,hotspot中方法區位于永久代中,同時永久代和堆是相互隔離的,但是他們使用的物理內存是連續的
相關的jvm參數:
- -XX:PermSize:表示非堆區初始內存分配大小,其縮寫為permanent size(持久化內存)
- -XX:MaxPermSize:表示對非堆區分配的內存的最大上限
java1.8后都是用本地內存(native memory) 來實現方法區,并將方法區的常量池放在堆中
放棄永久代的原因:維護和合并
- 手動設置永久代的大小,一方面如果小了點的話,會出現異常(OOM),如果大了的話就會浪費空間,不太利于維護。使用元空間時,是由系統實際可用的空間來控制加載類的數據
- 更深層次的原因是,hotspot要合并jrockit的代碼,jrockit沒用所謂的永久代,但是性能也非常好
- 元空間(MetaSpace)
用來存放:
- 類的方法代碼
- 變量名
- 方法名
- 訪問權限
比較表面的東西
方法區存在于元空間,元空間不再與堆連續,而是存在于本地內存 (Native memory)
GC(Garbage collector)
到底什么是垃圾?為什么要垃圾回收器?想要搞清楚垃圾回收機制,那么我們首先要清楚,jvm對于垃圾的定義。
引用計數法(Reference Count)
直觀的,我們認為,如果一個對象沒有其他對象引用他,那么他就是垃圾。因此我們每當另外一個對象對該對象產生引用的時候,就計數。
bug: 引用計數法,無法解決循環依賴的問題。
根可達算法(RootSearching)
根可達算法,就是對象到達GcRoot的路徑是否還有可達,即是否有可引用鏈,如果有,這表明對象還存在著引用, 如果沒有,則表明該對象沒有引用,在下一次垃圾回收時就會被回收
- GcRoot的種類
1.虛擬機棧:棧幀中的局部變量表引用的對象
2.native方法引用的對象
3.方法區中的靜態變量和常量引用的對象
清除算法
- 復制算法(Copying)
年輕代的算法,將可用內存分為相同的兩塊,每次只用一塊,當用完時,將存活的對象復制到另一塊上,再把已用的空間清理掉。第一次掃描eden區的對象,活下來的復制到survivor1,然后釋放eden區的。第二次掃描eden,survivor1區,活下來的對象放survivor2,然后釋放survivor1和eden區。再下一次就掃eden和survivor2,存survivor1,交叉復制這樣。
- 標記清除算法(Mark-Sweep)
標記清除:先標記,再統一回收,會產生大量不連續的內存碎片
- 標記整理算法(Mark-Compact)
標記整理:先標記,讓所有存活的對象向一端移動,然后清理掉邊界外的內存沒有碎片
垃圾回收器
技術的驅動力是業務,脫離了業務,講技術,都是耍流氓。
隨著我們的業務發展,jvm產生的垃圾,也會慢慢變多。 最開始可能只有幾兆~幾十兆,到后面幾十G以上。 并且垃圾回收器是搭配著用的,年輕代 + 老年代各用一款 比如Serial + Serial Old
- Serial
特點:單線程、STW 在執行垃圾回收的期間,采用單線程的方法,通過STW進行垃圾回收。通常搭配Serial Old使用。
如果垃圾太多,那么STW過程會很長,只適合輕量級系統使用
STW(stop the word)
Stop一the一World,簡稱STW,指的是Gc事件發生過程中,會產生應用程序的停頓。停頓產生時整個應用程序線程都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。
- Parallel
特點:多線程、STW
Serial算法的瓶頸在于他是單線程進行的,Parallel則是發采用多線程進行,但是還是會有STW過程。
通常搭配:Parallel Scavenge + Parallel Old
JDK 1.8 默認就是 PS + PO
即便是采用了多線程,但還是會有瓶頸,同時回收的時候會影響業務無法進行
- CMS
CMS(concurrent mark sweep) 老年代
可以和年輕代地算法 ParNew一起使用 ParNew是Parallel 為了適合cms兼容后的算法
并發地能在不停止業務的前提下,進行回收
- 初始標記:也是STW,不過這個暫停的不是整個內存,而是從root根出發去找,因此花費的時間就比較少。
- 并發標記:這里會出現錯標。比如一個對象被標記了垃圾,但是后面又有對象指向了這個對象。又或者是這個對象本來不是垃圾,后面又變成了垃圾。
- 重新標記:STW階段,修正第二部的
- 并發清理:用的是標記清理算法,mark sweep
- 三色標記法
黑:自己已經被標記,并且引用對象也被標記完 灰:自己已經被標記,但是引用對象每被標記完 白:沒有被標記到的
- Incremental Update
設想:三色標記法一定能保證不會出現漏標情況嗎?
首先GC掃了A對象,及其引用對象B,所以A是黑色的 GC掃了B,但是沒有掃他的引用對象C,所以B是灰色的 C還沒有被掃描所以是白色的
此時由于業務需求,B對C的引用取消了,但是A對C建立了引用。
問題:但是由于A被標記為了黑色,因此GC不會再去掃A和A的引用。但是C被標記為白色,因此會被回收掉,如果A調用C,則會出現NullPointException
解決辦法:重新標記,當建立新的引用的時候,把A標記為灰色,即可解決這個漏標的問題。
CMS的缺點: 由于采用的是Mark Sweep標記清楚算法,因此會產生內存碎片垃圾
- G1
G1垃圾回收器,摒棄了以往的分代算法,采用分區(Region)算法 雖然物理上不分代,但是邏輯上依舊分代。保留了Eden區、Old區的叫法。
分區算法 Region
每個區都可能是年輕代、老年代,但是在同一時刻都只屬于某個代
分區算法指的是,有的分區垃圾對象特別多,有的分區垃圾對象比較少,那么G1就會優先把垃圾對象特別多的分區給回收掉,這樣就能減少回收等待的時間
原因:
G1:
- 年輕代:每一次回收年輕代,都會把所有的年輕代回收掉(YGC)
- 老年代:G1自帶壓縮的收集器,在回收老年代的分區時,是將存活的對象從一個分區拷貝到另一個可用分區,這個過程實現了局部的壓縮。每一個分區的大小都是1到32M不等,都是2的冪次方。
RSet
remember set
存在著于 Region里面的某個區域
記錄其他Region引用當前Region
當引用消失的時候,會把這條引用記錄到RSet里面取
CSet
clean set
記錄當前GC需要清理的Region
YGC
發生YGC的時候,會把E區和S(from)區的對象合并拷貝到新的一個S(to)區
MixGC
G1沒有單獨的OGC,因此傳統的OGC是和YGC一起清理,稱為MixGC
- 初次標記:STW階段
- 并發標記:同cms相同,只不過遍歷范圍縮小,只需要去遍歷Rset中記錄過的Region區
- 重新標記:同cms相同,只不過用了SATB也是SWT階段
- 清理:只選出垃圾清理較多的Region
SATB
snapshot ai the beginning
在并發標記的時候做一個快照,在重新標記的時候處理RSet中的引用
灰色執行白色的引用消失的時候?
當B->D的引用消失的時候,把這個引用存到GC的堆棧,保證D還能被GC掃描到
配合RSet,只用掃描哪些Region引用到D這個Region了
當GC回來之后,發現有引用增加,代表有對象的引用消失了,那么就去掃一下那個D,看看是不是垃圾
相關參數
-XX:+UseG1GC 開啟G1垃圾回收器
-XX:GCHeapRegionSize 分區大小
-XX:InitializingHeapOccupancyPercent=30 觸發G1回收的最大堆內存占百分比
-XX:MaxGCPauseMillis 最大暫停時間的目標
-XX:GCPauseIntervalMillis GC的間隔時間
總結
以上是生活随笔為你收集整理的JVM、GC看这一篇就够了!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nginx 301跳转踩坑总结
- 下一篇: MongoDB中如何优雅地删除大量数据