推薦:
JVM的體系結構
簡化圖:
類載入器
類載入器作用:載入.class檔案
類載入流程(三個階段):
1.載入階段
將編譯好的class檔案載入到記憶體中(方法區),然後會生成一個代表這個類的Class物件。
2.連結階段
會為靜態變數分配記憶體並設定預設值。
3.初始化階段
執行類構造器()進行初始化賦值。
java自帶的類載入器:
- 啟動類載入器(Bootstrap ClassLoader):又名根類載入器或引導類載入器,負責載入%JAVA_HOME%\bin目錄下的所有jar包,或者是-Xbootclasspath引數指定的路徑,例:rt.jar
- 擴充類載入器(Extension ClassLoader):負責載入%JAVA_HOME%\bin\ext目錄下的所有jar包,或者是java.ext.dirs引數指定的路徑
- 系統類載入器(Application ClassLoader):又名應用類載入器,負責載入使用者類路徑上所指定的類庫,如果應用程式中沒有自定義載入器,那麼此載入器就為預設載入器
雙親委派機制
類載入器收到載入請求
1.不會自己先去載入,把請求委託給父類載入器,如果父類載入器還存在其父類載入器,則進一步向上委託,最終將到達頂層的啟動類載入器
2.如果父類可以完成載入任務,就成功返回
3.如果完不成,子載入器才會嘗試自己去載入
優點:避免重複載入 + 避免核心類篡改
Native
程式中使用:private native void start0();
1.凡是帶了native關鍵字的,說明java的作用範圍達不到了,回去呼叫底層c語言的庫!
2.會進入本地方法棧,然後去呼叫本地方法介面將native方法引入執行
本地方法棧(Native Method Stack)
記憶體區域中專門開闢了一塊標記區域: Native Method Stack,負責登記native方法,
在執行引擎( Execution Engine )執行的時候通過本地方法介面(JNI)載入本地方法庫中的方法
本地方法介面(JNI)
本地介面的作用是融合不同的程式語言為Java所用,它的初衷是融合C/C++程式,
Java在誕生的時候是C/C++橫行的時候,想要立足,必須有呼叫C、C++的程式,
然後在記憶體區域中專門開闢了一塊標記區域: Native Method Stack,負責登記native方法,
在執行引擎( Execution Engine )執行的時候通過本地方法介面(JNI)載入本地方法庫中的方法
PC程式計數器
程式計數器: Program Counter Register
每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指標,
指向方法區中的方法位元組碼(用來儲存指向像一條指令的地址, 也即將要執行的指令程式碼),
在執行引擎讀取下一條指令, 是一個非常小的記憶體空間,幾乎可以忽略不計
為什麼需要程式計數器?記錄要執行的程式碼位置,防止執行緒切換重新執行
位元組碼執行引擎修改程式計數器的值
方法區(Method Area)
方法區是被所有執行緒共享,所有欄位和方法位元組碼,以及一些特殊方法,如建構函式,介面程式碼也在此定義,
簡單說,所有定義的方法的資訊都儲存在該區域,此區域屬於共享區間。
靜態變數(static)、常量(final)、類資訊(構造方法、介面定義)(Class)、執行時的常量池存在方法區中,但是例項變數存在堆記憶體中,和方法區無關
棧
棧:先出後進,每個執行緒都有自己的棧,棧記憶體主管程式的執行,生命週期和執行緒同步,執行緒結束,棧記憶體也就是釋放。
對於棧來說,不存在垃圾回收問題,一旦執行緒結束,棧就結束。
棧記憶體中執行:8大基本型別 + 物件引用 + 例項的方法。
棧執行原理:棧楨
棧滿了:StackOverflowError
佇列:先進先出(FIFO:First Input First Output)
堆
一個JVM只有一個堆記憶體,堆記憶體的大小是可以調節的,
類載入器讀取類檔案後,一般會把類,方法,常量,變數,我們所有引用型別的真實物件,放入堆中。
堆記憶體細分為三個區域:
- 新生區(伊甸園區):Young/New
- 養老區old
- 永久區Perm
新生區:類的誕生,成長和死亡的地方
分為:
- 伊甸園區:所有物件都在伊甸園區new出來
- 倖存0區和倖存1區:輕GC之後存下來的
老年區(養老區):多次輕GC存活下來的物件放在老年區
真理:經過研究,99%的物件都是臨時物件
永久區
這個區域常駐記憶體的,用來存放IDK自身攜帶的Class物件,Interface後設資料,儲存的是Java執行時的一些環境或
類資訊,這個區域不存在垃圾回收。
關閉VM虛擬就會釋放這個區域的記憶體,一個啟動類,載入了大量的第三方jar包。
Tomcat部署了太多的應用,大量動態生成的反射類,不斷的被載入,直到記憶體滿,就會出現0OM;
- jdk1.6之前:永久代,常量池是在方法區。
- jdk1.7永久代,但是慢慢的退化了,去永久代, 常量池在堆中
- jdk1.8之後:無永久代,常量池在元空間
注意:
元空間:邏輯上存在,物理上不存在 ,因為:儲存在本地磁碟內,不佔用虛擬機器記憶體
預設情況下,JVM使用的最大記憶體為電腦總記憶體的四分之一,JVM使用的初始化記憶體為電腦總記憶體的六十四分之一.
總結:
- 棧:基本型別的變數,物件的引用變數,例項物件的方法
- 堆:存放由new建立的物件和陣列
- 方法區:Class物件,static變數,常量池(常量)
調優工具
public class Test2 {
static String a="111111111111";
public static void main(String[] args) {
while (true){
a=a+ new Random().nextInt(99999999)+new Random().nextInt(99999999);
}
}
}
下載地址:https://www.ej-technologies.com/download/jprofiler/version_92
安裝完成後,需要在IDEA中安裝外掛。
新增引數執行程式:
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError:當出現OOM錯誤,會生成一個dump檔案(程式的記憶體映象)
在專案目錄下找到dump檔案,雙擊開啟 , 可以看到什麼佔用了大量的記憶體
常見JVM調優引數
配置引數 | 功能 |
---|---|
-Xms | 初始堆大小。如:-Xms256m |
-Xmx | 最大堆大小。如:-Xmx512m |
-Xmn | 新生代大小。通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90% |
-XX:NewRatio | 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3 |
-XX:SurvivorRatio | 新生代中 Eden 與 Survivor 的比值。預設值為 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10 |
-XX:+PrintGCDetails | 列印 GC 資訊 |
XX:+HeapDumpOnOutOfMemoryError | 讓虛擬機器在發生記憶體溢位時 Dump 出當前的記憶體堆轉儲快照,以便分析用 |
常見垃圾回收演算法
引用計數演算法
原理是此物件有一個引用,即增加一個計數,刪除一個引用則減少一個計數。
垃圾回收時,只用收集計數為 0 的物件。此演算法最致命的是無法處理迴圈引用的問題。
複製演算法
此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。
垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。
此演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理。
優點:不會出現碎片化問題
缺點:需要兩倍記憶體空間,浪費
標記-清除演算法
此演算法執行分兩階段。
第一階段從引用根節點開始標記可回收物件
,
第二階段遍歷整個堆,把未標記的物件清除。
優點:不會浪費記憶體空間
缺點:此演算法需要暫停整個應用,同時,會產生記憶體碎片
標記-壓縮演算法
此演算法結合了 " 標記-清除 ” 和 “ 複製 ” 兩個演算法的優點。
也是分兩階段,
第一階段從根節點開始標記所有可回收物件
,
第二階段遍歷整個堆,清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。
此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。
分代回收策略
1.絕大多數剛剛被建立的物件會存放在Eden區
2.當Eden區第一次滿的時候,會觸發MinorGC(輕GC)。首先將Eden區的垃圾物件回收清除,並將存活的物件複製到S0,此時S1是空的。
3.下一次Eden區滿時,再執行一次垃圾回收,此次會將Eden和S0區中所有垃圾物件清除,並將存活物件複製到S1,此時S0變為空。
4.如此反覆在S0和S1之間切換幾次(預設15次)之後,還存活的物件將他們轉移到老年代中。
5.當老年代滿了時會觸發FullGC(全GC)
MinorGC
- 使用的演算法是複製演算法
- 年輕代堆空間緊張時會被觸發
- 相對於全收集而言,收集間隔較短
FullGC
- 使用的演算法一般是標記壓縮演算法
- 當老年代堆空間滿了,會觸發全收集操作
- 可以使用 System.gc()方法來顯式的啟動全收集
- 全收集非常耗時
垃圾收集器
垃圾回收器的常規匹配:
序列收集器(Serial)
Serial 收集器是 Hotspot 執行在 Client 模式下的預設新生代收集器, 它的特點是:單執行緒收集,但它卻簡單而高效
並行收集器(ParNew)
ParNew 收集器其實是前面 Serial 的多執行緒版本
Parallel Scavenge 收集器
與 ParNew 類似,Parallel Scavenge 也是使用複製演算法,也是並行多執行緒收集器。
但與其他收集器關注儘可能縮短垃圾收集時間不同,Parallel Scavenge 更關注系統吞吐量,
系統吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本, 同樣是單執行緒收集器,使用 “ 標記-整理 ” 演算法
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多執行緒和 “ 標記-整理 ” 算
法,吞吐量優先
CMS 收集器(Concurrent Mark Sweep)
CMS是一種以獲取最短回收停頓時間為目標的收集器(CMS又稱多併發低暫停的收集器),
基於 ” 標記-清除 ” 演算法實現, 整個 GC 過程分為以下 4 個步驟:
初始標記(CMS initial mark)
併發標記(CMS concurrent mark: GC Roots Tracing 過程)
重新標記(CMS remark)
併發清除(CMS concurrent sweep: 已死物件將會就地釋放, 注意:此處沒有壓縮)
G1 收集器
G1將堆記憶體 “ 化整為零 ” ,將堆記憶體劃分成多個大小相等獨立區域(Region),
每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。
收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新建立的物件還是已經存活了一段時間、熬過多次收集的舊物件都能獲取很好的收集效果。
為什麼要垃圾回收時要設計STW(stop the world)?
如果不設計STW,可能在垃圾回收時使用者執行緒就執行完了,堆中的物件都失去了引用,全部變成了垃圾,索性就
設計了STW,快速做完垃圾回收,再恢復使用者執行緒執行。