JVM實踐與應用
- 1.類加載器(加載、連接、初始化)
- 1.1 類加載要完成的功能
- 1.2 加載類的方式
- 1.3 類加載器
- 1.4 雙親委派模型
- 1.5自定義ClassLoader
- 1.6 破壞雙親委派模型
- 2.1 類連接主要驗證內容
- 2.2 類連接中的解析
- 2.3 類的初始化
- 3.1 類的初始化時機
- 3.2 類的初始化機制和順序
- 3.2 類的卸載
- 2. 內存分配
- 2.1 JVM簡化架構
- 2.2 棧、堆、方法區交互關系
- 3.1 Java堆內存模型和分配
- 3.2 對象的內存布局
- 3.3 對象的訪問定位
- 4.1 內存分配的參數
- 3. 字節碼執行引擎
- 3.1 棧幀
- 3.2 分派
- 4. 垃圾回收
- 4.1 什么是垃圾
- 4.2 ZGC收集器
- 4.3 GC性能指標
- 4.4 JVM內存配置原則
- 4.5 垃圾收集器
- 4.6 引用分類
- 4.7 跨代引用
- 4.8 GC類型
- 4.9 Stop-The-World
- 4.10 垃圾收集器類型
- 4.11 判斷類無用的條件
- 4.12 垃圾回收算法
- 4.13 垃圾收集器基礎和串行收集器
- 5.高效并發
- 5.1 Java內存模型、內存間的交互操作
- 5.2 多線程的可見性、有序性和指令重排、線程安全的處理方法
- 5.3 鎖優化:自旋鎖、鎖消除、鎖粗化、輕量級鎖、偏向鎖等
- 5.4 JVM中獲取鎖的步驟
- 5.5 同步代碼的基本規則
- 6.性能監控與故障處理工具
- 6.1 命令行工具
- 6.2 圖形化工具
- 6.3 兩種連接方式
- 6.4 JVM監控工具的作用
- 6.5 監控與故障處理實戰
- 7. JVM調優
- 7.1 調什么
- 7.2 如何調優
- 7.3 JVM調優的目標
- 7.4 常見調優策略
- 7.5 JVM調優冷思考
- 7.6 JVM調優經驗
- 7.7 內存泄漏
- 7.8 調優實戰
- 8. 加油站
- 8.1 字節碼部分
- 8.2 內存分配
- 8.3 監控工具和實戰
- 9.梳理jvm知識體系
- 9.1 地毯式總結jvm知識
- 9.2 后續學習方法
1.類加載器(加載、連接、初始化)
- 加載: 加載類的二進制數據
- 連接: 類的二進制數據合并到虛擬機運行時區。
- 驗證: 驗證類信息正確性,保證安全。
- 準備:為類的靜態變量分配內存,并初始化它們。
- 解析: 把常量池中的符號引用轉為直接引用。
- 初始化:為類的靜態變量賦初始值。
1.1 類加載要完成的功能
- 通過類的全限定名來獲取改類的二進制流
- 把二進制的字節流轉化為方法區的運行時數據結構
- 在堆上創建一個java.lang.Class對象,用來封裝類在方法區的數據結構,并向外提供訪問方法區的內數據結構的接口。
1.2 加載類的方式
- 最常見的方式:本地文件系統中加載、從jar等歸檔文件中加載
- 動態的方式:將java源文件動態編譯成class
- 其它方式:網絡下載、從專有數據庫中加載等等
1.3 類加載器
Java虛擬機自帶的加載器包括如下幾種:
啟動類加載器(BootstrapClassLoader))
平臺類加載器(PlatformClassLoader) | 擴展類加載器(ExtensionClassLoader)
應用程序類加載器(AppClassLoader)
用戶自定義的加載器,是java.lang.ClassLoader的子類
用戶可以定制類的加載方式;只不過自定義類加載器其加載
的順序是在所有系統類加載器的最后
- 啟動類加載器:
用于加載啟動的基礎模塊類,比如:java.base、java.management、.java.xml等等
Java8的啟動類加載器加載 <java_home>/lib,或者-XbootClasspath參數指定的,且是虛擬機能夠識別的類庫加載到內存中(按名字識別,比如rt.jar,對于不能識別的文件不予加載)
- 平臺類加載器:
用于加載一些平臺相關的模塊,比如:
java.scripting、java.compiler*、java.corba*等等
JDK8:擴展類加載器:負責加載<JRE_HOME>/lib/ext,
或者java.ext.dirs系統變量所指定路徑中的所有類庫
- 應用程序加載器
JDK8:應用程序類加載器:負責加載classpath.路徑中的所
有類庫
說明:
Java程序不能直接引用啟動類加載器,直接設置
classLoader為null,默認就使用啟動類加載器
類加載器并不需要等到某個類“首次主動使用”的時候才
加載它,規范允許類加載器在預料到某個類將要被使用
的時候就預先加載它
如果在加載的時候.class文件缺失,會在該類首次主動使用時
報告LinkageError錯誤,如果一直沒有被使用,就不會報錯
1.4 雙親委派模型
JVM中的ClassLoaderi通常采用雙親委派模型,要求除了啟
動類加載器外,其余的類加載器都應該有自己的父級加載器
這里的父子關系是組合而不是繼承,工作過程如下:
- jdk8先委派給父加載器,遞歸加載
- 如果沒有加載到,則使用自己的加載器加載。
1.5自定義ClassLoader
如果沒有指定父加載器,默認就是啟動加載器。
1.6 破壞雙親委派模型
雙親委派模型有個問題:父加載器無法向下識別子加載器加載的資源
為了解決這個問題,引入了線程上下文類加載器,可以通過Thread的setContextClassLoader()進行設置。
另外一種典型情況就是實現熱替換,比如OSGI的模塊化熱部署,它的類加載器就不再是嚴格遵守雙親委派模型,很多可能就在平級的類加載器中執行了。
2.1 類連接主要驗證內容
- 類文件結構檢查:按照jVM規范規定的類文件結構進行
- 元數據驗證:對字節碼描述的信息進行語義分析,保證其符合java語言規范要求
- 字節碼驗證:通過對數據流和控制流進行分析,確保程序語義是合法和符合邏輯的。這里主要是對方法體進行校驗。
- 符號引用驗證:對類自身以外的信息,也就是常量池中的各種符號引用,進行匹配校驗
2.2 類連接中的解析
所謂解析就是把常量池中的符號引用轉換成直接引用的過程,包括:符號引用:以一組無歧義的符號來描述所引用的目標,與虛擬機的實現無關。
直接引用:直接指向目標的指針、相對偏移量、或是能間接定位到目標的句柄,是和虛擬機實現相關的。
主要針對:類、接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符。
2.3 類的初始化
類的初始化就是為類的靜態變量賦初始值,或者說是執行類構造器方法的過程
- 如果類還沒有加載和連接,就先加載和連接
- 如果類存在父類,且父類沒有初始化,就先初始化父類
- 如果類中存在初始化語句,就依次執行這些初始化語句.
- 如果是接口的話:
a、初始化一個類的時候,并不會先初始化它實現的接口
b、初始化一個接口時,并不會初始化它的父接口
c、只有當程序首次使用接口里面的變量或者是調用接口方法的時候,才會導致接口初始化
3.1 類的初始化時機
Jva程序對類的使用方式分成:主動使用和被動使用,JVM必須在每個類或接口“首次主動使用”時才初始化它們;被動使用類不會導致類的初始化,主動使用的情況:
- 創建類實例
- 訪問某個類或接口的靜態變量
- 調用類的靜態方法
- 反射某個類
- 初始化某個類的子類,而類還沒有初始化
- JVM啟動的時候運行的主類
- 定義了default方法的接口,當接口實現類初始化時
3.2 類的初始化機制和順序
通過子類引用父類的靜態字段不會導致子類的初始化
通過數組定義引用類,不會觸發類的初始化
調用常量不會引發類的初始化
public class MyParent {public static String parentStr="now in MyParent!";static {System.out.println("my parent class init");}
}public class MyChild extends MyParent {static {System.out.println("MyChild class init");}static {System.out.println("my child static block 222");}public static int a =5;static {System.out.println("my child static block 333==" + a);}public static void t2() {System.out.println("now in mychild t2()");}
}public class Test{public static void main(String[]args){// 子類引用父類的字符串不會導致子類的初始化System.out.println("Mychild.parentStr = " + MyChild.parentStr);}
}
3.2 類的卸載
當代表一個類的Class對象不再被引用,那么Class對象的生命周期就結束了,對應的在方法區中的數據也會被卸載。
JVM自帶的類加載器裝載的類,是不會卸載的,由用戶自定義的類加載器加載的類是可以卸載的
2. 內存分配
2.1 JVM簡化架構
運行時數據區
包括:程序計數器、虛擬機棧、本地方法棧;Java堆、方法區
- 程序計數器
每個線程擁有一個寄存器,是線程私有的,用來存儲指向下一條指令的地址
在創建線程的時候,創建相應的程序計數器
執行本地方法時,程序計數器的值為undefined
是一塊較小的內存空間,是唯一個在jvm規范中沒有規定OutOfMemoryError的內存區域
- Java棧
1.棧是由一系列幀Frame組成(因此Java棧也叫幀棧),是線程私有的
2. 幀用來保存方法的局部變量表、操作數棧、動態連接、方法出口
3. 每一次方法調用創建一個幀,并壓棧,退出方法的時候,修改棧頂指針就可以把棧幀中的內容銷毀。
4. 局部變量表存放了編譯器可知的各種基本數據類型和引用類型,每個slot存放32位的數據,long、double占兩個槽位。
5. 棧的優點:
存取速度比堆快,僅次于寄存器
6. 棧的缺點:
存在棧中的數據大小、生存期是在編譯期決定的,缺乏靈活性
- Java堆
用來存放應用系統創建的對象和數組,所有線程共享Java堆
GC主要管理堆空間,對分代GC來說,堆也是分代的
堆的優點:運行期動態分配內存大小、自動進行垃圾回收;
堆的缺點:效率相對較慢
- 方法區
方法區是線程共享的,通常用來保存裝載的類的結構信息
通常和元空間關聯在一起,但具體的跟JVM實現和版本有關
JVM規范把方法區描述為堆的一個邏輯部分,但它有一個別名為Non-heap非堆,應是為了與Java堆區分開
- 運行時常量池
- 是Class文件中每個類或接口的常量池表,在運行期間的表示形式,通常包括:類的版本、字段、方法、接口等信息
- 方法區中分配
- 通常在加載類和接口到JVM后,就創建相應的運行時常量池
- 本地方法棧
在JVM中用來支持native方法執行的棧就是本地方法棧
2.2 棧、堆、方法區交互關系
3.1 Java堆內存模型和分配
Java堆用來存放應用系統創建的對象和數組,所有線程共享Java堆;
Java堆是在運行期動態分配內存大小,自動進行垃圾回收;
Java垃圾回收(GC)主要就是回收堆內存,對分代GC來說,堆也是分代的.
新生代用來放新分配的對象;新生代中經過垃圾回收,沒有回收掉的對象,被復制到老年代
老年代存儲對象比新生代存儲對象的年齡大得多;
老年代存儲一些大對象;
總結:
整個堆大小 = 新生代+老年代
新生代 = Eden+存活區從前的持久代,用來存放Class、Method等元信息的區域,從JDK8開始去掉了,取而代之的是元空間(MetaSpace), 元空間并不在虛擬機里面,而是直接使用本地內存
3.2 對象的內存布局
- 對象在內存中存儲的布局(這里以HotSpot虛擬機為例來說明),分為:對象頭、實例數據和對齊填充 。
- 對象頭,包含兩個部分:
- Mark Word: 存儲對象自身的運行狀態,如:hashcode,GC分代年齡,鎖狀態標志等
- 類型指針:對象指向它的類元數據的指針
- 實例數據
- 真正存放對象實例數據的地方
- 對其填充
- 這部分不一定存在,也沒有什么特殊含義,僅僅是占位符。因為HotSpot要求對象其實 地址都是8字節的整數倍,如果不是,就對齊。
3.3 對象的訪問定位
- 對象的訪問定位
在VM規范中只規定了reference類型是一個指向對象的引用,但沒有規定這個引用具體如何去定位、訪問堆中對象的具體位置 - 因此對象的訪問方式取決于JVM的實現,目前主流的有:使用句柄或使用指針兩種方式。
- 使用句柄:Java堆中會劃分出一塊內存來做為句柄池,referencer中存儲句柄的地址,句柄中存儲對象的實例數據和類元數據的地址,如下圖所示
- 使用指針:Java堆中會存放訪問類元數據的地址reference存儲的就直接是對象的地址,如下圖所示.
4.1 內存分配的參數
- Trace跟蹤參數
- -Xlog:gc 可以打印GC的簡要信息
- -Xlog:gc* 打印GC詳細信息
- -Xlog:gc:garbage-collection.log 以文件輸出
- -Xlog:gc+heap=debug 每一次GC后,都打印堆信息
- GC日志格式
- GC發生的時間,也就是JVM啟動以來經過的秒數
- 日志級別信息,和日志類型標記
- GC識別號
- GC類型和說明GC的原因
- 容量:GC前的容量->GC后的容量(該區域總容量)
- GC持續時間,單位秒
- Java堆的參數
- -Xms: 初始堆大小,默認物理內存的1/64,13要求是1024的倍數,1M1M的,大于1M
- -Xmx: 最大堆大小,默認物理內存的1/4
- -Xmn: 新生代大小,默認是整個堆的3/8 【推薦:新生代是整堆的25%-50%】
- -XX: NewSize 設置新生代 -XX:MaxNewSize 設置新生代最大size 或者使用-Xmn
- -XX:+HeapDumpOnOutOfMemoryError OOM時導出堆到文件
- -XX: HeapDumpPath 導出OOM的路徑
- -XX:NewRatio 老年代與新生代的比值,如果xms=xmx,且設置了xmn的情況下,該參數不用設置
- -XX: SurvivorRatio: Eden區和Survivor區的大小比值。設置為8,則兩個Survivor區與一個Eden區的比值為2:8,一個survivor占整個新生的1/10.
- -XX: OnOutOfMemoryError 在OOM時,執行一個腳本
- Java棧的參數
- -Xss 通常只有幾百k,決定 了函數調用的深度
- 元空間的參數
- -XX:MetaspaceSize 初始空間大小
- -XX:MaxMetaspaceSize 最大空間,默認是沒有限制的
- -XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空間容量的百分比
- -XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空間容量的百分比
-Xms1M -Xmx2M -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\GBase8s_WorkDir\GEM_mon\monitor-alarm\dd.hprof
3. 字節碼執行引擎
- JVM的字節碼執行引擎,功能基本就是輸入字節碼文件,然后對字節碼進行解析并處理,最后輸出執行的結果;
- 實現方式可能有通過解釋器直接解釋執行字節碼,或者是通過即時編譯器產生本地代碼,也就是編譯執行,當然也可能兩者皆有。
3.1 棧幀
- 棧幀是用于支持JVM進行方法調用和方法執行的數據結構
- 棧幀隨著方法調用而創建,隨著方法結束而銷毀
- 棧幀里面存儲了方法的局部變量、操作數棧、動態連接、方法返回地址等信息。
- 局部變量表
- 用來存放方法參數和方法內部定義的局部變量的存儲空間
- 以變量槽slot為單位,目前一個slot存放32位以內的數據類型
- 對于64位的數據占2個slot
- 對于實例方法,第0位slot存存放的是this,然后從1到n,依次分配給參數列表
- 然后根據方法體內部定義的變量順序和作用域來分配slot
- slot是復用的,以節省棧幀的空間,這種設計可能影響到系統的垃圾收集行為
public static void main(String[]args){{byte[] bs = new byte[1 * 1024 * 1024]; // 1Mbs = null; // 也可以導致堆內存回收。}// bs的插槽被a變量復用了,那么堆里的bs對象就會被回收。int a = 5;System.gc();System.out.println("totalMemory=" + Runtime.getRuntime().totalMemory() /1024.0/1024.0);System.out.println("freeMemory=" + Runtime.getRuntime().freeMemory() /1024.0/1024.0);System.out.println("totalMemory=" + Runtime.getRuntime().maxMemory() /1024.0/1024.0);
}
- 操作數棧
- 用來存放方法運行期間,各個指令操作的數據。
- 操作數棧中元素的數據類型必須和字節碼指令的順序嚴格匹配。
- 虛擬機在實現棧幀的時候可能會做一些優化,讓兩個棧幀出現部分重疊區域,以存放共用的數據。
- 動態鏈接
- 每個棧幀持有一個指向運行時常量池中該棧幀所屬方法的引用,以支持方法調用過程的動態連接。
- 靜態解析:類加載的時候,符號引用就轉化成直接引用
- 動態連接:運行期間轉換為直接引用。
- 方法返回地址
- 方法執行后返回的地址
- 方法調用
- 方法調用就是確定具體調用哪一個方法,并不涉及方法內部的執行過程。
- 部分方法是直接在類加載的解析階段,就確定了直接引用關系。
- 私有方法
- 靜態方法
- 實例構造器
- 父類方法
- 但是對于實例方法,也稱虛方法,因為重載和多態,需要運行期動態委派。
3.2 分派
- 分派:分成靜態分派和動態分派
- 靜態分派:所有依賴靜態類型來定位方法執行版本的分派方式,比如:重載方法
- 動態分派:根據運行期的實際類型來定位方法執行版本的分派方式,比如:覆蓋方法
單分派和多分派:就是按照分派思考的維度,多于一個的就是多分派,只有一個的稱為單分派。
如何執行方法中的字節碼指令:JVM通過基于棧的字節碼解釋執行引擎來執行指令,JVM的指令集也是基于棧的。
4. 垃圾回收
4.1 什么是垃圾
- 簡單說就是內存中已經不再被使用到的內存空間就是垃圾
- 引用計數法:給對象添加一個引用計數器,有訪問就加1,引用失效就減1
- 優點:實現簡單,效率高。
- 缺點:不能解決對象之間循環引用問題。
- 根搜索算法
- 從根(GC Roots)節點向下搜索對象節點,搜索走過的路稱為引用鏈,當一個對象到根之間沒有連通的話,則該對象不可用。
- 可作為GCRoots的對象包括:虛擬機棧(棧幀局部變量)中引用的對象、方法區類靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中JNI引用的對象。
- HotSpot使用了一組叫做OopMap的數據結構達到準確式GC的目的。
- 在OopMap的協助下,JVM可以很快的做完GC ROOTs枚舉。但是JVM并沒有為每一條指令生成一個OopMap
- 記錄OopMap的這些“特殊位置”被稱為安全點,即當前線程執行到安全點后才允許暫停進行GC。
- 如果一段代碼中,對象引用關系不會發生變化,這個區域中任何地方GC都是安全的,那么這個區域就稱為安全區域。
4.2 ZGC收集器
- JDK11加入的具有實驗性質的低延遲收集器
- ZGC的設計目標是: 支持TB級別內存容量,暫停時間低(< 10ms),對整個程序吞吐量的影響小于15%.
- ZGC里面的技術:著色指針和讀屏障
- 在指針里面存放一些信息的指針叫做著色指針
- 程序讀取內存的數據之前 會執行一些操作,然后將這個地址返回回去的技術就是讀屏障
4.3 GC性能指標
- 吞吐量 = 應用代碼執行的時間/運行的總時間(越大)
- GC負荷,與吞吐量相反,是GC時間/運行的總時間(越小)
- 暫停時間,就是發生Stop-the-world的總時間(越小)
- GC頻率,就是GC一個時間段內發生的次數(越小)
- 反應速度,就是從對象稱為垃圾到被回收的時間
- 交互式應用通常希望暫停時間越少越好
4.4 JVM內存配置原則
- 新生代盡可能設置大點,如果太小會導致
- YGC次數更加頻繁
- 可能會導致YGC后的對象進入老年代,如果此時老年代滿了,會觸發FGC(3/8 – 1/2)
- 對老年代,針對響應時間優先的應用:由于老年代通常采用并發收集器,因此其大小要綜合考慮并發量和并發持續時間等參數。
- 如果設置小了,可能會造成內存碎片,高回收頻率會導致應用暫停
- 如果設置大了,會需要較長的回收時間
- 對老年代,針對吞吐量優先的應用:通常設置較大的新生代和較小的老年代,這樣可以盡可能回收大部分短期對象,減少中期對象,而老年代盡量存放長期存活的對象。
- 依據對象的存貨周期進行分類,對象優先在新生代分配,長時間存活的對象進入老年代
- 根據不同代的特點,選取合適的收集算法:少量對象存活,適合復制算法,大量對象存活,適合標記清除或標記整理。
4.5 垃圾收集器
- 串行收集器
- 并行收集器
- 理解新生代Parallel Scavenge收集器、理解CMS、理解G1
- 理解GC性能指標和JVM內存配置原則
4.6 引用分類
- 強引用:類似Object a = new A()這樣的,不會被回收
- 軟引用:還有用但并不是必須的對象。用SoftReference來實現軟引用
- 弱引用: 只要發生gc,弱引用對象就會被回收。
- 虛引用:也稱幽靈引用或幻影引用,是最弱的引用。垃圾回收會回收掉。用PhantomReference來實現虛引用【結合引用隊列使用,可以替代finalizer,在DirectByteBuffer中用來回收堆外內存。】
4.7 跨代引用
-
就是一個代中的對象引用另外一個代中的對象
-
新生代-> 老年代
-
跨代引用假說:跨代引用對于同代引用來說只是極少數
-
隱含推論:存在互相引用關系的兩個對象,是應該傾向于同時生存或同時消亡的。
為了解決跨代掃描,引入了新的數據結構【記憶集RememberedSet】:一種用于記錄從非收集區域指向收集區域的指針集合的抽象數據結構。(空間換時間)- 字長精度:每個記錄精確到一個機器字長,該字包含跨代指針
- 對象精度:每個記錄精確到一個對象,該對象有字段含有跨代指針
- 卡精度:每個精度精確到一塊內存區域,該區域內有對象含有跨代指針
- 卡表(card table):是記憶集的一種具體實現,定義了記憶集的記錄精度和堆內存的映射關系等。
- 卡表的每個元素都對應著其標識的內存區域中的一塊特定大小的內存塊,這個內存塊稱為卡頁(Card Page)。
寫屏障
寫屏障可以看做是JVM對“引用類型字段賦值”這個動作的AOP
通過寫屏障來實現對象狀態改變后,維護卡表狀態。 -
判斷是否垃圾的步驟
- 根搜索算法判斷不可用
- 看是否有必要執行finalize方法
- 兩個步驟走完后對象仍然沒有人使用,那就屬于垃圾
4.8 GC類型
- MinorGC/YoungGC: 發生在新生代的收集動作
- MajorGC/OldGC: 發生在老年代的GC,目前只有CMS收集器會有單獨收集老年代的行為
- MixedGC:收集整個新生代以及部分老年代,目前只有G1收集器會有這種行為。
- FullGC: 收集整個Java堆和方法區的GC
4.9 Stop-The-World
STW是Java中一種全局暫停的現象,多半由于GC引起。所謂全局暫停,就是所有Java代碼停止運行,
native代碼可以執行,但不能和jvm交互。其危害是長時間服務停止,沒有響應;對于HA系統,可能引起主備切換,嚴重危害生產環境。
4.10 垃圾收集器類型
- 串行收集:GC單線程內存回收、會暫停所有的用戶線程,如:Serial
- 并行收集:多個 GC線程并發工作,此時用戶線程是暫停的,如Parallel
- 并發收集:用戶線程和GC線程同時執行(不一定是并行,可能交替執行),不需要停頓用戶線程,如:CMS
4.11 判斷類無用的條件
- JVM中該類的所有實例都已經被回收
- 加載該類的ClassLoader已經被回收
- 沒有任何地方引用該類的Class對象
- 無法再任何地方通過反射訪問這個類
4.12 垃圾回收算法
- 標記清除法
- 分為標記和清除兩個階段,先標記出要回收的對象,然后統一回收這些對象
- 優點:簡單
- 缺點:
- 效率不高、標記和清除的效率都不高
- 標記清除后會產生大量不連續的內存碎片,從而導致在分配大對象時觸發GC
- 分為標記和清除兩個階段,先標記出要回收的對象,然后統一回收這些對象
- 復制算法
- 把內存分成兩塊完全相同的區域,每次使用其中一塊當一塊使用完了,就把這塊上還存活的對象拷貝到另外一塊,然后把這塊清除掉。
- 優點:實現簡單,運行高效、不用考慮內存碎片問題
- 缺點:內存有些浪費
- JVM實際實現中,是將內存分為一塊較大的Eden區和兩塊較小的Survivor空間,每次使用Eden和一塊Survivor,回收時,把存活的對象復制到另一塊Survivor。
- HotSpot默認的 Eden和Survivor比是8:1,也就是每次能用90%的新生代空間
- 如果survivor空間不夠,就要依賴老年代進行分配擔保,把放不下的對象直接進入老年代
- 把內存分成兩塊完全相同的區域,每次使用其中一塊當一塊使用完了,就把這塊上還存活的對象拷貝到另外一塊,然后把這塊清除掉。
- 標記整理算法
- 由于復制算法在存活對象比較多的時候,效率較低,且有空間浪費,因此老年代一般不會選用復制算法,老年代多選用標記整理算法。
- 標記過程跟標記清除一樣,但后續不是直接清除可回收對象,而是讓所有存活對象都向一端移動,然后直接清除邊界以外的內存。
4.13 垃圾收集器基礎和串行收集器
年輕代和老年代垃圾回收期配合方式:
-
串行收集器:
- Serial串行收集器/Serial Old收集器,是一個單線程的收集器,在垃圾收集時,會stop-the-world
- 優點:是簡單,對于單cpu,由于沒有多線程的交互開銷,可能更高效,是默認的Client模式下的新生代收集器。
- 使用-XX: +UseSerialGC來開啟,會使用:Serial + SerialOld的收集器組合(新生代Serial+老年代SerialOld)
- 新生代使用復制算法,老年代使用標記-整理算法
- Serial串行收集器/Serial Old收集器,是一個單線程的收集器,在垃圾收集時,會stop-the-world
-
并行收集器
- ParNew(并行)收集器:使用多線程進行垃圾回收,在垃圾收集時,會Stop-The-World.
- 在并發能力好的CPU環境里,它停頓的時間要比串行收集器短;但對于單cpu或并發能力較弱的CPU,由于多線程的交互開銷,可能比串行回收器更差。
- 是Server模式下首選的新生代收集器,且能和CMS收集器配合使用。
- 不再使用-XX:+UseParNewGC來單獨開啟
- -XX: ParrallelGCThreads: 指定線程數,最好于CPU數量一致
- ParNew(并行)收集器:使用多線程進行垃圾回收,在垃圾收集時,會Stop-The-World.
-
新生代Parrallel Scavenge 收集器
- 新生代Parallel Scavengel收集器/Parallel Old收集器:是一個應用于新生代的、使用復制算法的、并行的收集器
- 跟ParNew很類似,但更關注吞吐量,能最高效率的利用CPU,適合運行后臺應用。
- 使用-XX: +UseParallelGC來開啟
- 使用-XX: UseParallelOldGC來開啟老年代使用ParrallelOld收集器,使用Parrallel Scavenge + Parrallel Old的收集器組合
- -XX: MaxGCPauseMills: 設置GC的最大停頓時間
- 新生代使用復制算法,老年代使用標記整理算法
-
CMS收集器(concurrent mark and sweep 并發標記清除)
- 分為:初始標記,只標記GC Roots能直接關聯到的對象;并發標記,進行GC Roots Tracing的過程。
- 重新標記:修改并發標記期間,因程序運行導致標記發生變化的那一部分現象。
- 并發清除:并發回收垃圾對象(gc線程和用戶線程同時跑)
- 初始標記和重新標記沒有用戶線程,所以還是會發生stop-the-world的。
- 使用標記清除算法,多線程并發收集的垃圾收集器。
- 最后的重置線程,指的是清空跟收集相關的數據并重置,為下一次收集做準備
- 優點:低停頓、并發執行
- 缺點:
- 并發執行,對CPU資源壓力大
- 無法處理 在處理過程中產生的垃圾,可能導致FullGC
- 采用的是標記清除算法會導致大量碎片,從而在分配大對象時可能觸發FullGC
- 開啟:-XX:UseConcMarkSweepGC 使用ParNew + CMS + Serial old的收集器組合,Serial Old將作為CMS出錯的后備收集器。
- -XX:CMSInitiatingOccupancyFraction 設置CMS收集器在老年代空間使用多少后觸發回收,默認80%
-
G1收集器
- G1 收集器:是一款面向服務端應用的收集器,與其他收集器相比,具有以下特點:
- Gl把內存劃分成多個獨立的區域(Region)
- G1仍采用分代思想,保留了新生代和老年代,但它們不再是物理隔離的,而是一部分Regionl的集合,且不需要Region是連續的。
- G1能充分利用多CPU、多核環境硬件優勢,盡量縮短STW。
- G1整體上采用標記-整理算法,局部是通過復制算法,不會產生內存碎片(新生代和老年代都使用復制)
- G1的停頓可預測,能明確指定在一個時間段內,消耗在垃圾收集上的時間不能超過多長時間
- Gl跟蹤各個Region里面垃圾堆的價值大小,在后臺維護一個優先列表,每次根據允許的時間來回收價值最大的區域,從而保證在有限時間內的高效收集。
- 跟CMS類似,也分為四個階段:
- 初始標記:只標記GCRootsi能直接關聯到的對象
- 并發標記:進行GC Roots Tracingl的過程
- 最終標記:修正并發標記期間,因程序運行導致標記發生變化的那一部分對象
- 篩選回收:根據時間來進行價值最大化的回收
- 使用與配置:
- -XX: UseG1GC 開啟G1,默認就是G1
- -XX:MaxGCPauseMillis=500, 最大GC停頓時間,這是個軟目標,JVM將盡可能(但不保證)停頓小于這個時間
- -XX:InitiatingHeapOccupancyPercent= n; 堆內存用了多少的時候就出發GC,默認為 45
- -XX: NewRatio=n,默認為2
- -XX: SurvivorRatio=n,默認為8
- -XX:MaxTenuringThreshold=n,新生代到老年代的歲數,默認是15
- -XX:ParrallelGCThreads=n,并行GC的線程數,默認值會根據平臺不同而不同
- -XX:ConcGCThreads=n:并發GC使用的線程數
- -XX:G1ReservePercent:=n:設置作為空閑空間的預留內存百分比,以降低目標空間溢出的風險,默認值是10%
- -XX:G1HeapRegionSize=n:設置的G1區域的大小。值是2的冪,范圍是1MB到32MB。目標是根據最小的)ava堆大小劃分出約2048個區域
- G1 收集器:是一款面向服務端應用的收集器,與其他收集器相比,具有以下特點:
5.高效并發
5.1 Java內存模型、內存間的交互操作
- JCP定義了一種ava內存模型,以前是在VM規范中,后來獨立出來成為SR-133(Java內存模型和線程規范修訂)
- 內存模型:在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象
- Java內存模型主要關注VM中把變量值存儲到內存和從內存中取出變量值這樣的底層細節
- 所有變量(共享的)都存儲在主內存中,每個線程都有自己的工作內存;工作內存中保存該線程使用到的變量的主內存副本拷貝。
- 線程對變量的所有操作(讀、寫)都應該在工作內存中完成。
- 不同線程不能相互訪問工作內存,交互數據要通過主內存。
- 內存間的交互操作:
- Java內存模型規定了一些操作來實現內存間交互,JVM會保證它們是原子的。
- lock:鎖定,把變量標識為線程獨占,作用于主內存變量。
- unlock:解鎖,把鎖定的變量釋放,別的線程才能使用作用于主內存變量。
- read:讀取,把變量值從主內存讀取到工作內存。
- load:載入,把read讀取到的值放入工作內存的變量副本中。
- use:使用,把工作內存中一個變量的值傳遞給執行引擎。
- assign:賦值,把從執行引擎接收到的值賦給工作內存里面的變量。
- store:存儲,把工作內存中一個變量的值傳遞到主內存中
- write:寫入,把store進來的數據存放如主內存的變量中
- 內存間交互操作的規則
- 不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但不保證連續執行,也就是說read與load之間、store-與write之間是可插入其他指令的。
- 不允許一個線程丟棄它的最近的assign操作,即變量在下作內存中改變了之后必須把該變化同步回主內存。
- 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中
- 一個新的變量只能從主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化的變量,也就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
- 一個變量在同一個時刻只允許一條線程對其執行引ock操作,但ock操作可以被同一個條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。
- 如果對一個變量執行引ock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assigna操作初始化變量的值。
- 如果一個變量沒有被ock操作鎖定,則不允許對它執行unlock操作,也不能unlock一個被其他線程鎖定的變量
- 對一個變量執行unlocki操作之前,必須先把此變量同步回主內存(執行store和write操作)。
5.2 多線程的可見性、有序性和指令重排、線程安全的處理方法
- 多線程的可見性
- 就是一個線程修改了變量,其他線程可以知道
- 保證可見性的常見方法:
- volatile
- Synchronized
- final(一旦初始化完成,其他線程就可見)
- volatile
- 基本上是jvm提供的最輕量級的同步機制,用volatile修飾的變量,對所有線程可見,即對volatile變量所做的寫操作能立即反應到其它線程中。
- 用volatile修飾的變量,在多線程環境下仍然是線程不安全的。
- volatile修飾的變量,是禁止指令重排優化的。
- 適合使用volatile的場景:
- 運算結果不依賴變量的當前值。
- 或者能保證只有一個線程修改變量的值
public class A {private volatile int a;public void aPlus() {a++;}public int getA() {return this.a;}
}public class MyRunnable implements Runnable{private A a = null;private String name ="";public MyRunnable(A a, String name) {this.a = a;this.name = name;}@Overridepublic void run(){for(int i = 0;i< 1000; i++){a.aPlus()}System.out.println("thread =" + name+ "is game over -->>");}
}public class Test1{public static void main(String[]args) {A a = new A();Thread t1 = new Thread(new MyRunnable(a,"t1"));Thread t2 = new Thread(new MyRunnable(a,"t2"));t1.join();t2.join();//不安全,只是保證了可見性System.out.println("finally , t1 and t2 threads A.a = " + a.getA()); // 1903 1508}
}
-
指令重排:指的是JVM為了優化,在條件允許的情況下,對指令進行一定的重新排列,直接運行當前能夠立即執行的后續指令,避開獲取下一條指令所需數據造成的等待。
- 線程內串行語義,不考慮多線程間的語義。
- 不是所有的指令都能重排,比如:寫后讀a=l,b=a;寫一個變量之后,再讀這個位置
- 寫后寫也是不能重排的,如:a = 1, a = 3;
- 讀后寫也不能重排,a = b, b = 2
-
指令重排的基本規則:
- 程序順序原則:一個線程內保證語義的串行性。
- volatile規則:volatile?變量的寫,先發生于讀。
- 鎖規則:解鎖(unlock)必然發生在隨后的加鎖(lock)前
- 傳遞性:A先于B,B先于C那么A必然先于C
- 線程的start方法先于它的每一個動作。
- 線程的所有操作先于線程的終結(Thread.join())
- 線程的中斷(interrupt())先于被中斷線程的代碼。
- 對象的構造函數執行結束先于finalize()方法。
-
多線程中的有序性
- 在本線程內,操作都是有序的
- 在線程外觀察,操作都是無序的,因為存在指令重排或主內存同步延時。
-
Java線程安全的處理方法
- 不可變是線程安全的
- 互斥同步(阻塞同步):synchronized、java.util.concurrent.ReentrantLock.目前這兩個方法特性已經差不多了,建議優先使用Synchronized,ReentrantLock增加了如下特性:
- 等待可中斷:當持有鎖的線程長時間不釋放鎖:正在等待的線程可以選擇放棄等待
- 公平鎖:多個線程等待同一個鎖時,須嚴格按照申請鎖的時間順序來獲得鎖
- 鎖綁定多個條件:一個ReentrantLocki對象可以綁定多個condition對象,而synchronized是針對一個條件的,如果要多個,就得有多個鎖。
- 非阻塞同步:是一種基于沖突檢查的樂觀鎖定策略,通常是先操作,如果沒有沖突,操作就成功了,有沖突再采取其它方式進行補償處理。
- 無同步方案:其實就是在多線程中,方法并不涉及共享數據自然也就無需同步了。
5.3 鎖優化:自旋鎖、鎖消除、鎖粗化、輕量級鎖、偏向鎖等
- 自旋鎖與自適應自旋
- 自旋:如果線程可以很快獲得鎖,那么可以不在OS層掛起線程,而是讓線程做幾個忙循環,這就是自旋。
- 自適應自旋:自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間和鎖的擁有者狀態來決定。
- 如果鎖被占用時間很短,自旋成功,那么能節省線程掛起以及切換時間,從而提升系統性能。
- 如果鎖被占用時間很長,自旋失敗,會白白耗費處理器資源,降低系統性能。
- 鎖消除
- 在編譯代碼的時候,檢測到根本不存在共享數據競爭,自然也就無需同步加鎖了;通過-XX:+EliminateLocks來開啟。
- 同時要使用-XX:+DoEscapeAnalysis:開啟逃逸分析,所謂逃逸分析:
- (1) 如果一個方法中定義的一個對象,可能被外部方法引用,稱為方法逃逸。
- (2) 如果對象可能被其它外部線程訪問,稱為線程逃逸,比如賦值給類變量或者可以在其它線程中訪問的實例變量。
- 鎖粗化
- 通常我們都要求同步塊要小,但一系列連續的操作導致對一個對象反復的加鎖和解鎖,這會導致不必要的性能損耗。這種情況建議把鎖同步的范圍加大到整個操作序列。
- 輕量級鎖
- 輕量級是相對于傳統鎖機制而言,本意是沒有多線程競爭的情況下,減少傳統鎖機制使用OS實現互斥所產生的性能損耗。
- 其實現原理很簡單,就是類似樂觀鎖的方式。
- 如果輕量級鎖失敗,表示存在競爭,升級為重量級鎖,導致性能下降。
- 偏向鎖
- 偏向鎖是在無競爭情況下,直接把整個同步消除了,連樂觀鎖都不用,從而提高性能;所謂的偏向,就是偏心,即鎖會偏向于當前已經占有鎖的線程。
- 只要沒有競爭,獲得偏向鎖的線程,在將來進入同步塊,也不需要做同步
- 當有其它線程請求相同的鎖時,偏向模式結束。
- 如果程序中大多數鎖總是被多個線程訪問的時候,也就是競爭比較激烈,偏向鎖反而會降低性能。
- 使用-X:-UseBiasedLocking:來禁用偏向鎖,默認開啟
5.4 JVM中獲取鎖的步驟
- 會嘗試偏向鎖;然后嘗試輕量級鎖
- 再然后嘗試自旋鎖
- 最后嘗試普通鎖,使用OS互斥量在操作系統層掛起
5.5 同步代碼的基本規則
- 盡量減少鎖持有的時間
- 盡量減少鎖的粒度
6.性能監控與故障處理工具
6.1 命令行工具
jps、jinfo、jstack、jmap、jstat、jstatd、jcmd
jps(VM Process Status Tool)):主要用來輸出VM中運行
的進程狀態信息,語法格式如下:jps[options][hostid]
hostid字符串的語法與URI的語法基本一致:
[protocol::][/hostname]port][/servername],如果不指定nostid,默認為當前主機或服務器。jinfo: 打印給定進程或核心文件或遠程調試服務器的配置信息。語
法格式:jinfo[option]pid #指定進程號(pid)的進程jstack:主要用來查看某個Java進程內的線程堆棧信息。語法 格式如下:jstack[option]pidjmap用來查看堆內存使用狀況,語法格式如下jmap [option]pidjstat
JVM統計監測工具,查看各個區內存和GC的情況
jstat -gc pidjcmd
JVM診斷命令工具,將診斷命令請求發送到正在運行的)ava
虛擬機,比如可以用來導出堆,查看java進程,導出線程信息,執行GC等
6.2 圖形化工具
jconsole、
jmc、visualvm
一個用于監視Java虛擬機的符合MX的圖形工具。它可以監視衣和遠程VM,還可監視和管理應用程序jmc(JDK Mission Control)Java任務控制(JMC)客戶端包括用于監視和管理Java應用程序的工具,
而不會引入通常與這些類型的工具相關聯的性能開銷。VisualVM
一個圖形工具,它提供有關在Java虛擬機中運行的基于Java技術的應用程序的詳細信息。
Java VisualVM提供內存和CPU分析,堆轉儲分析,內存泄漏檢測,訪問MBean和垃圾回收。
jmc下載地址:點一下我 ~
6.3 兩種連接方式
JMX、Jstatd
// 遠程連接Tomcat
CATALINA OPTS="-Xms800m -Xmx800m -Xmn350m-
XX:SurvivorRatio=8-XX:+HeapDumpOnOutOfMemoryError
-Dcom.sun.management.jmxremote=true-
Djava.rmi.server.hostname=192.168.1.105-
Dcom.sun.management.jmxremote.port=6666-
Dcom.sun.management.jmxremote.ssl=false-
Dcom.sun.managementote.ssl=false-
Dcom.sun.management.jmxremote.authenticate=false'配置jstatd:(1)自定義一個statd.policy文件,添加:
grant codebase "jrt:/jdk.jstatd"{
permission java.security.AllPermission;
grant codebase "jrt:/jdk.internal.jvmstat"{
permission java.security.AllPermission;
}:(2)然后在JDK HOME/bin下面運行jstatd,示例如:
./jstatd -J-Djava.rmi.server.hostname=192.168.1.102 -
J-Djava.security.policy=java.policy -p 1099 &
6.4 JVM監控工具的作用
- 對jvm運行期間的內部情況進行監控,比如:對jvm參數、CPU、內存、堆等信息的查看
- 輔助進行性能調優
- 輔助解決應用運行時的一些問題,比如:OutOfMemoryError、內存泄露、線程死鎖、鎖爭用、Java進程消耗CPU過高等等
6.5 監控與故障處理實戰
- 內存泄漏分析、線程查看、熱點方法查看、垃圾回收查看
- 線程死鎖
常見的JVM監控工具:重點掌握visualvm和jmc
理解并掌握兩種遠程連接方式:JMX、jstatd
7. JVM調優
JVM調優:調什么、如何調、調的目標是什么
JVM調優策略、調優冷思考、調優經驗
分析和處理內存溢出
7.1 調什么
- 內存方面
- JVM需要的內存總大小
- 各快內存分配,新生代,老年代,存活區
- 選擇合適的垃圾回收算法、控制GC停頓次數和時間
- 解決內存泄漏的問題,輔助代碼優化
- 內存熱點:檢查哪些對象在系統中數量最大,輔助代碼優化
- 線程方面
- 死鎖檢查,輔助代碼優化
- Dump線程詳細信息:查看線程內部運行情況,查找競爭線程,輔助代碼優化。
- CPU熱點:檢查系統哪些方法占用了大量CPU時間,輔助代碼優化。
7.2 如何調優
- 監控JVM的狀態,主要是內存、線程、代碼、IO幾部分
- 分析結果,判斷是否需要優化
- 調整:垃圾回收算法(選擇與參數配置)和內存分配(新生代、老年代);修改并優化代碼
- 不斷的重復監控、分析和調整,直至找到優化的平衡點
7.3 JVM調優的目標
- GC的時間足夠小
- GC的次數足夠的小
- 將轉移到老年代的對象數量降低到最小
- 減少FullGC的執行時間
- 發生FullGC的時間間隔足夠的長
7.4 常見調優策略
- 減少創建對象的數量
- 減少使用全局變量和大對象
- 調整新生代、老年代的大小到最合適
- 選擇合適的GC收集器、并設置合理的參數
7.5 JVM調優冷思考
- 多數的java應用不需要考慮在服務器上進行GC優化
- 多數導致GC問題的java應用,都不是因為參數設置錯誤,而是代碼問題
- 在應用上線之前,先考慮將機器的JVM參數設置到最優(最適合)
- JVM優化是到最后不得已才采用的手段。
- 在實際使用中,分析JVM情況優化代碼比優化JVM本身多得多
- 不需要優化的情況:
- MinorGC執行時間不到50ms
- MinorGC執行不頻繁,約10秒一次
- FullGC執行時間不到1s
- FullGC執行的頻率不算頻繁,不低于10分鐘1次
7.6 JVM調優經驗
- 要注意32位和64位的區別,通常32位的僅支持2-3g左右的內存
- 要注意client模式和Server模式的選擇
- 要想GC時間小必須要有一個更小的堆;而要保證GC次數足夠少,又必須保證有一個更大的堆,這兩個是沖突的,只能取其平衡。
- 針對JVM堆的設置,一般可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,通常把最大、最小設置為相同的值 。
- 新生代和老年代將根據默認的比例(1:2)分配堆內存,可以通過調整二者之間的比率NewRadio來調整,也可以通過-XX:newSize-XX:MaxNewSize來設置其絕對大小,同樣,為了防止新生的堆收縮,通常會把-X:newSize -XX:MaxNewSize設置為同樣大小
- 合理規劃新生代和老年代的大小
- 如果應用存在大量的臨時對象,應該選擇更大的新生代;如果存在相對較多的持久對象,老年代應該適當增大。在抉擇時應該本著FullGC盡量少的原則,讓老年代盡量緩存常用的對象,JVM的默認比例1:2也是這個道理。
- 通過觀察應用一段時間,看其在高峰時老年代會占多少內存,在不影響FullGC的前提下,根據實際情況加大新生代,但應該給老年代至少預留1/3的增長空間。
- 線程堆棧的設置:每個線程默認會開啟1M的堆棧,用于存放棧幀、 調用參數、局部變量等,對大多數應用而言這個默認值太大了,一般256K就足用。在內存不變的情況下,減少每個線程的堆棧,可以產生更多的線程。
7.7 內存泄漏
內存泄露導致系統崩潰前的一些現象,比如:
- 每次垃圾回收的時間越來越長,FuGC時間也延長到好幾秒
- FullGC的次數越來越多,最頻繁時隔不到1分鐘就進行一次FullGC
- 老年代的內存越來越大,并且每次FuGC后年老代沒有內存被釋放
- 老年代堆空間被占滿的情況
- 這種情況的解決方式:一般就是根據垃圾回收前后情況對比,同時根據對象引用情況分析,輔助去查找泄漏點
5.1 堆棧溢出的情況
通常拋出StackOverFlowError例外
一般就是遞歸調用沒退出,或者循環調用造成
dump thread and memory shot
7.8 調優實戰
重點是調優的過程、方法和思路
內存調整、數據庫連接調整、內存泄漏查找等
CATALINA OPTS="-Xms512m -Xmx512m -Xmn200m -XX SurvivorRatio=8 -XX +HeapDumpOnOutofMemoryError
-Dcom.sun.management.jmxremote=true
-Djava.rmi.server.hostname=192.168.1.113
-Dcom.sun.management.jmxremote.port=6666
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.managementote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false"
8. 加油站
8.1 字節碼部分
- 知道字節碼嗎?字節碼指令集都有哪些?Integer x = 5,int y = 5,比較x==y都經過哪些步驟?
Code:stack=2,locals=4,args_size=10:iconst_51:invokestatic #16 //method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;4:astore_15:iconst_26:istore_27:aload_18:invokevirtual #22 // method java/lang/Integer.intValue:()11:iload_212:if_icmpne15:iconst_116:goto19:iconst_020:istore_321:returnLineNumberTable:line5:0line6:5line8:7
- 簡述Java的類加載機制,并回答一個JVM中 可以是否存在兩個相同的類
* 通過類的全限定名來獲取改類的二進制流
* 把二進制的字節流轉化為方法區的運行時數據結構
* 在堆上創建一個java.lang.Class對象,用來封裝類在方法區的數據結構,并向外提供訪問方法區的內數據結構的接口。
# 主要說雙親委派流程
- 講講加載機制,都有哪些類加載器,這些類加載器都加載哪些文件?
- 啟動類加載器 java.base、java.management、java.xml等
- 平臺類加載器 java.scripting、java.compiler*、java.corba*
- 應用程序加載器:jdk.compile、jdk.jartool、jdk.jshell等 還加載classpath路徑中的所有類庫
加載-> 連接(驗證、準備、解析) -> 初始化 -> 使用 -> 卸載
8.2 內存分配
- 談談JVM內存模型(*內存分配 | 高效并發)
- 堆、元空間(線程共享)、本地方法棧、虛擬機棧、程序計數器(線程私有)
- JVM的數據區有哪些?作用是什么?
- Java堆內存一定是線程共享的嗎?
內存分配方法:為對象分配內存的基本方法:指針碰撞法、空閑列表法
內存分配并發問題的解決:CAS、TLAB(Hotspot會預先為每個線程在堆上劃分一塊內存,也就是線程
私有的(分配時是線程獨有的,其他回收使用都是共享的),TLAB這塊內存是比較小的,大概占Eden區
的1/100)。
- Java堆內存結構是咋樣的?哪些情況會觸發GC?會觸發哪些GC?
MinorGC/YoungGC: 發生在新生代的收集動作
MajorGC/OldGC: 發生在老年代的GC,目前只有CMS收集器會單獨收集老年代的行為
MixedGC:收集整個新生代以及部分老年代,目前只有G1收集器會有這種行為
- 說說JVM的垃圾回收
什么是垃圾、如何判定是垃圾、如何回收
根搜索算法、引用分類、GC類型、垃圾收集類型
/*
簡單說就是內存已經不再被使用到的內存空間就是垃圾
引用計數法:給對象添加一個引用計數器,有訪問就加1,引用失效就減1
優點:簡單、效率高、缺點:不能解決對象之間循環引用的問題
根搜索算法
清除算法:復制算法、標記清除、標記整理
垃圾收集器:串行收集器、并行收集器、新生代Parrallel、Scavenge收集器、CMS、G1
GC性能指標和JVM內存配置原則
*/
- JVM四種引用類型:
- 強應用:類似于Object a =new A(),不會被回收
- 軟引用:還有用但并不必須的對象。用SoftReference來實現
- 弱引用:非必須對象,比軟引用還要弱,垃圾回收會回收掉。用WeakReference來實現弱引用
- 虛引用:是最弱的引用,垃圾回收時會回收掉。用PhantomReference來實現虛引用。
- JVM回收算法和垃圾收集器。
8.3 監控工具和實戰
- 如何把java內存的數據全部dump出來
-XX:+HeapDumpOnOutOfMemoryError: OOM時導出堆文件
Jmap來查看堆內存使用情況,jmap pid
jmc、virualvm
- Jstack是干嘛的?
- 用來查看java進程中線程堆棧信息。jstack pid
- JVM統計檢測工具,查看各個區內存和GC情況
- jstat
- 如何定位問題?如何解決問題?說一下解決思路和處理方法。
jmc和virualVM結合使用
jfr定位問題,記錄過程,內存分配情況,線程、垃圾回收過程
結合源代碼定位問題。監控JVM的狀態,主要是內存、線程、代碼、I/O幾部分
分析結果,判斷是否需要優化
調整:垃圾回收算法和內存分配;修改并優化代碼
不斷的重復監控、分析和調整,直至找到優化的平衡點
- CPU 使用率過高咋么辦?
- 應用邏輯耗CPU:那么就是計算密集型[圖形圖像處理]
- 頻繁的io,大量線程死鎖,線程競爭資源
- CFR記錄一段時間,分析
- 線上引用頻繁full gc如何處理?
- 內存設置,整個堆太小了,新生代->進入老年代(老年代不夠了,頻繁觸發fullgc)
- 堆夠,比例不對
- 代碼頻繁分配大對象,能不能復用大對象或者盡快回收,設置為null
- 垃圾收集器是否合理,參數是否合理。
- 如果應用周期性地出現卡頓,你會咋么來排查這個問題?
- full-gc導致stw
- 規避full-gc,或者將full-gc的周期拉長
- 減少full-gc,時間變短
- 其他原因
- 你有沒有遇到過OutOfMemory問題?你是咋么來處理這個問題的?
- 老年代堆空間被占滿的情況
- 這種情況的解決辦法:一般就是根據垃圾回收前后情況對比,同時根據對象的引用情況分析,輔助去查找泄漏點
- StackOverFlow異常有沒有遇到過?這個異常會在什么情況下被觸發?如何指定線程堆棧的大小?
- 堆棧溢出的情況
- 通常拋出java.lang.StackOverFlowError例外
- 一般就是遞歸調用沒有退出,或者循環調用造成
- -Xss: 通常只有幾百k,決定了函數調用的深度。
寫在最后 :
監控工具實戰的方面、內存分配、垃圾回收
類的裝載、連接、初始化最重要
9.梳理jvm知識體系
9.1 地毯式總結jvm知識
/*
JVM 概述:認識jvm、java如何實現平臺無關的
JVM規范:理解JVM規范的作用、了解JVM規范里規定的主要內容
Class文件格式:CLass文件格式、閱讀class字節碼文件、閱讀虛擬機匯編語言表示的java類、asm開發實戰(認識asm、asm編程模型和核心api、asm開發)
類加載、連接和初始化:理解類從加載、連接、初始化到卸載的生命周期、理解類加載、類加載器、雙親委派模型、類連接、類初始化(類初始化、類初始化時機)、類卸載
JVM內存分配:JVM簡化架構、內存模型、理解棧、堆、方法區之間的交互關系
JVM堆內存:堆的結構、對象的內存布局、內存分配參數(Trace跟蹤參數、GC日志參數、堆參數、棧參數、元空間參數)
字節碼執行引擎:棧幀、局部變量表、操作數棧、動態連接、方法返回地址棧幀、運行期操作數棧和局部變量表之間的交互關系方法調用(靜態分派、動態分配)
垃圾回收基礎:什么是垃圾、如何判定是垃圾、如何回收、根搜索算法、引用分類、跨代引用、記憶集寫屏障、GC類型、Stop-the-World、垃圾收集類型
垃圾收集算法:標記清除法、復制算法、分配擔保、標記整理法
垃圾收集器:HotSpot中的收集器、串行收集器、并行收集器、新生代Parallel Scavenge收集器CMS收集器、G1收集器、了解ZGC收集器、GC性能指標、JVM內存配置原則
JVM對高效并發的支持:Java內存模型、內存間的交互操作、多線程的可見性、有序性、原子性指令重排、線程安全的處理方法、鎖優化(自旋鎖、鎖消除、鎖粗化、輕量級鎖、偏向鎖)
性能監控與故障處理工具:命令行工具:jps、jinfo、jstack、jmap、jstat、jstatd、jcmd圖形化工具:jconsole、jmc、visualvm兩種遠程連接方式:jmx、jstatd
jvm調優實戰:jvm調優(調什么、如何調、調優的目的是什么)JVM調優策略、jvm調優冷思考、jvm調優經驗分析和處理內存泄漏、調優實戰(多練習)
*/
9.2 后續學習方法
多應用,將所學的內容進行分析應用
掌握的越深刻 掌握的越牢固!