JVM啓動流程
JVM基本結構
PC寄存器
- 每個線程擁有一個PC寄存器
- 在線程創建時創建
- 指向下一條指令的地址
- 執行本地方法時,PC的值爲undefined
方法區
保存裝載的類信息
- 類型的常量池(JDK6時,String等常量池置於方法,JDK7時,已經移動到了堆)
- 字段、方法信息
- 方法字節碼
通常和永久區(Perm)關聯在一起
Java堆
- 和程序開發密切相關
- 應用系統對象都保存在Java堆中
- 所有線程共享Java堆
- 對分代GC來說,堆也是分代的
GC的主要工作區間
Java棧
- 線程私有
- 棧由一系列幀組成(因此Java棧也叫做幀棧)
- 幀保存一個方法的局部變量、操作數棧、常量池指針
- 每一次方法調用創建一個幀,並壓棧
Java棧——局部變量表 包含參數和局部變量
public static int runStatic(int i, long l, float f, Object o, byte b) {
return 0;
}
靜態方法的局部變量表如下圖所示
public int runInstance(char c, short s, boolean b) {
return 0;
}
實例方法的局部變量表如下圖所示
Java棧——函數調用組成幀棧
public static int runStatic(int i, long l, float f, Object o, byte b) {
return runStatic(i, l, f, o, b);
}
Java棧——操作數棧
java沒有寄存器,所有參數傳遞使用操作數棧
public static int add(int a, int b) { int c = 0; c = a + b; return c; }
對應的操作爲:
0: iconst_0 // 0 壓棧 1: istore_2 // 彈出int,存放於局部變量2 2: iload_0 // 把局部變量0壓棧 3: iload_1 // 局部變量1壓棧 4: iadd // 彈出2個變量,求和,結果壓棧 5: istore_2 // 彈出結果,放於局部變量2 6: iload_2 // 局部變量2壓棧 7: ireturn // 返回
Java棧——棧上分配
- 小對象(一般幾十個bytes),在沒有逃逸的情況下,可以直接分配在棧上
- 直接分配在棧上,函數調用完成自動清理空間,減輕GC壓力
- 大對象或者逃逸對象無法棧上分配
棧、堆、方法區交互
// 運行時,JVM把AppMain的信息都放入方法區
public class AppMain {
// main方法本身放入方法區
public static void main(String[] args){
// test1 是引用,所以放到棧區裏,Sample是自定義對象應該放到堆裏面
Sample test1 = new Sample("測試1");
Sample test2 = new Sample("測試2");
test1.printName();
test2.printName();
}
}
// 運行時,JVM把Sample的信息都放入方法區
public class Sample {
private String name;
// new Sample實例後,name引用放入棧區裏,name對象放入堆裏
public Sample(String name){
this.name = name;
}
// print方法本身放入方法區裏
public void printName(){
System.out.println(name);
}
}
內存模型
- 每一個線程有一個工作內存和主內存
- 工作內存存放主內存中變量的值的拷貝
當數據從主內存複製到工作存儲時,必須出現兩個動作:第一,由主內存執行的讀(read)操作;第二,由工作內存執行的相應的load操作;當數據從工作內存拷貝到主內存時,也出現兩個操作:第一個,由工作內存執行的存儲(store)操作;第二,由主內存執行的相應的寫(write)操作
每一個操作都是原子的,即執行期間不會被中斷
對於普通變量,一個線程中更新的值,不能馬上反應在其他線程中
如果需要在其他線程中立即可見,需要使用volatile關鍵字
volatile
public class VolatileStopThread extends Thread {
private volatile boolean stop = false;
public void stopMe(){
stop = true;
}
@Override
public void run(){
int i = 0;
while(!stop){
i++;
}
System.out.println("Stop thread");
}
public static void main(String args[]) throws InterruptedException {
VolatileStopThread t = new VolatileStopThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
}
如果沒有volatile關鍵字,server運行就無法停止
volatile不能代替鎖,一般認爲volatile比鎖性能好(不絕對)
選擇使用volatile的條件是:語義是否滿足應用
可見性
一個線程修改了變量,其他線程可以立即知道的方法:
- volatile
- synchronized(unlock之前,寫變量值回主內存)
- final(一旦初始化完成,其他線程就可見)
有序性
- 在本線程內,操作都是有序的
- 在線程外觀察,操作都是無序的(指令重排或主內存同步延時)
指令重排
線程內串行語義
- 寫後讀 a = 1; b = a; 寫一個變量之後,再讀這個位置
- 寫後寫 a = 1; a = 2; 寫一個變量之後,再寫這個變量
- 讀後寫 a = b; b = 1; 讀一個變量之後,再寫這個變量
- 以上語句不可重排
- 編譯器不考慮多線程間的語義
- 可重排: a = 1; b = 2;
指令重排——破壞線程間的有序性
class OrderExample { int a = 0; boolean flag = false; public void writer(){ a = 1; flag = true; } public void reader(){ if(flag){ int i = a + 1; ... } } }
線程A首先執行writer()方法
線程B接着執行reader()方法
線程B在int i = a + 1是不一定能看到a已經被賦值爲1,因爲在writer中,兩句話順序可能打亂
指令重排——保證有序性的方法
class OrderExample { int a = 0; boolean flag = false; public synchronized void writer(){ a = 1; flag = true; } public synchronized void reader(){ if(flag){ int i = a + 1; ... } } }
同步後,即使做了writer重排,因爲互斥的緣故,reader線程看writer線程也是順序執行的
指令重排的基本原則
- 程序順序原則:一個線程內保證語義的串行性
- volatile規則:volatile變量的寫,先發生於讀
- 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
- 傳遞性:A先於B,B先於C,那麼A必然先於C
- 線程的start方法先於它的每一個動作
- 線程的所有操作先於線程的終結(Thread.join())
- 線程的中斷(interrupt())先於被中斷線程的代碼
- 對象的構造函數執行結束先於finalize()方法
解釋運行
- 解釋執行以解釋方式運行字節碼
- 解釋執行的意思是:讀一句執行一句
編譯運行(JIT)
- 將字節碼編譯成機器碼
- 直接執行機器碼
- 運行時編譯
- 編譯後性能有數量級的提升
常用JVM配置參數
Trace跟蹤參數
-verbose:gc
輸出虛擬機中GC的詳細情況
使用後輸出如下:
[Full GC 168K->97K(1984K), 0.0253873 secs]
168K和97K分別表示垃圾收集GC前後所有存活對象使用的內存容量,數據1984K爲堆內存的總容量,收集所需要的時間是0.0253873秒
-XX:+PrintGC
同-verbose:gc
-XX:+PrintGCDetails
打印GC詳細信息
-XX:+PrintGCTimeStamps
[GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
-Xloggc:log/gc.log
- 指定GC log的位置,以文件輸出
- 幫助開發人員分析問題
-XX:+PrintHeapAtGC
- 每次一次GC後,都打印堆信息
-XX:+TraceClassLoading
- 監控類的加載
-XX:+PrintClassHistogram
- 按下Ctrl+Break後,打印類的信息
- 分別顯示:序號、實例數量、總大小、類型
堆分配參數
-Xmx -Xms
- 指定最大堆和最小堆
-Xmx20m -Xms5m
System.out.print("Xmx = "); System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); System.out.print("free mem = "); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("total mem = "); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); // 結果爲 Xmx = 19.375M free mem = 4.342750549316406M total mem = 4.875M
- Java會儘可能維持在最小堆
-Xmn
- 設置新生代大小(官方推薦新生代佔堆的3/8)
-XX:NewRatio
- 新生代(eden + 2 * s)和老年代(不包含永久區)的比值
- 4表示新生代:老年代=1:4,即新生代佔堆的1/5
-XX:SurvivorRatio
- 設置兩個Survivor區和eden的比
- 8表示兩個Survivor:eden=2:8,即一個Survivor佔新生代的1/10(官方推薦)
如下例
public static void main(String[] args){ byte[] b = null; for(int i = 0; i < 10; i++){ b = new byte[1 * 1024 * 1024]; } }
如果設置
-Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails
則沒有觸發GC,數據全部分配在老年代
如果設置
-Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails
則沒有觸發GC,數據全部分配在eden,老年代沒有使用
如果設置
-Xmx20m -Xms20m -Xmn7m -XX:+PrintGCDetails
則進行了2次新生代GC,s0 s1太小需要老年代擔保
如果設置
-Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
則進行了3次新生代GC,s0 s1增大
如果設置
-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+PrintGCDetails
則進行了2次新生代GC,新生代空間增大
如果設置
-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3 -XX:+PrintGCDetails
則進行了1次新生代GC,新生代空間增大,s0 s1增大
-XX:+HeapDumpOnOutOfMemoryError
- OOM時導出堆到文件
-XX:+HeapDumpPath
- 導出OOM的路徑
示例:
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath Vector v = new Vector(); for(int i = 0; i < 25; i++){ v.add(new byte[1 * 1024 * 1024]); }
-XX:OnOutOfMemoryError
- 在OOM時,執行一個腳本
“-XX:OnOutOfMemoryError=D:/tools/printstack.bat %p”,printstack.bat的內容爲
D:/tools/jdk1.7_40/bin/jstack -F %1 > D:/a.txt
- 當程序OOM時,在D:/a.txt中會生成線程的dump
- 可以在OOM時,發送郵件,甚至是重啓程序
永久區分配參數
-XX:PermSize -XX:MaxPermSize
- 設置永久區的初始空間和最大空間
- 他們表示,一個系統可以容納多少個類型
使用CGLIB等庫的時候,可能會產生大量的類,這些類,有可能撐爆永久區導致OOM
for(int i = 0; i < 100000; i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean" + i, new HashMap()); // 不斷地產生新的類
}
棧大小分配
-Xss
- 通常只有幾百K
- 決定了函數調用的深度
- 每個線程都有獨立的棧空間
- 局部變量、參數分配在棧上
如下例
public class TestStackDeep {
private static int count = 0;
public static void recursion(long a, long b, long c) {
long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
count++;
recursion(a, b, c);
}
public static void main(String args[]) {
try {
recursion(0L, 0L, 0L);
} catch (Throwable e) {
System.out.println("deep of calling = " + count);
e.printStackTrace();
}
}
}
設置-Xss128K,拋出java.lang.StackOverflowError時,deep of calling = 292
設置-Xss256K,拋出java.lang.StackOverflowError時,deep of calling = 1080
JIT及其相關參數
- 字節碼執行性能較差,所以可以對於熱點代碼編譯成機器碼再執行,在運行時的編譯,叫做JIT Just-In-Time
- JIT的基本思路是,將熱點代碼,就是執行比較頻繁的代碼,編譯成機器碼
相關參數
Xint
- 解釋執行
Xcomp
- 全部編譯執行
Xmixed
- 默認,混合
GC算法與種類
引用計數法
- 老牌垃圾回收算法
- 通過引用計算來回收垃圾
使用者
- COM
- ActionScript
- Python
引用計數器的實現很簡單,對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1,當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,則對象A就不可能被再被使用
引用計數法的問題
- 引用和去引用伴隨着加法和減法,影響性能
- 很難處理循環引用
標記-清除
標記-清除算法是現代垃圾回收算法的思想基礎。標記-清除算法是將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先通過根結點,標記所有從根節點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象,然後,在清除階段,清除所有未被標記的對象
標記-壓縮
標記-壓縮算法適合用於存活對象較多的場合,如老年代,它在標記-清除算法的基礎上做了一些優化。和標記-清除算法一樣,標記-壓縮算法也首先需要從根節點開始,對所有可達對象做一次標記。但之後,它並不簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存的一端,之後,清理邊界外所有的空間
複製算法
- 與標記-清除算法相比,複製算法是一種相對高效的回收方法
- 不適用於存活對象較多的場合,如老年代
- 將原有的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未使用的內存塊中,之後,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收
分代思想
- 依據對象的存活週期進行分類,短命對象歸爲新生代,長命對象歸爲老年代
根據不同代的特點,選取合適的收集算法
- 少量對象存活,適合複製算法
- 大量對象存活,適合標記清理或者標記壓縮
GC算法總結
引用計數
- 沒有被Java採用
標記-清除
- 標記-壓縮
複製算法
- 新生代
所有的算法,需要能夠識別一個垃圾對象,因此需要給出一個可觸及性的定義
可觸及性
可觸及的
從根節點可以觸及到這個對象
根節點包括
- 棧中引用的對象
- 方法區中靜態成員或者常量引用的對象(全局對象)
- JNI方法棧中引用對象
可復活的
- 一旦所有引用被釋放,就是可復活狀態
- 因爲在finalize()中可能復活該對象
不可觸及的
- 在finalize()後,可能會進入不可觸及狀態
- 不可觸及的對象不可能復活
可以回收
public class CanReliveObj { public static CanReliveObj obj; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("CanReliveObj finalize called"); obj = this; } @Override public String toString() { return "I am CanReliveObj"; } public static void main(String[] args) throws InterruptedException { obj = new CanReliveObj(); obj = null; //可復活 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } System.out.println("第二次gc"); obj = null; //不可復活 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println("obj 是 null"); } else { System.out.println("obj 可用"); } } } // 輸出結果爲: CanReliveObj finalize called obj 可用 第二次gc obj 是 null
應該儘量避免使用finalize(),操作不慎可能導致錯誤,因爲它的優先級低,何時被調用不確定,何時發生GC也不確定,可以使用try-catch-finally來替代它
Stop-The-World
Stop-The-World
- java中一種全局暫停的現象
- 全局停頓,所有Java代碼停止,native代碼可以執行,但不能和JVM交互
多半由於GC引起
- Dump線程
- 死鎖檢查
- 堆Dump
GC時爲什麼會有全局停頓
- 類比在聚會時打掃房間,聚會時很亂,又有新的垃圾產生,房間永遠打掃不乾淨,只有讓大家停止活動了,才能將房間打掃乾淨
危害
- 長時間服務停止,沒有響應
- 遇到HA系統,可能引起主備切換,嚴重危害生產環境
GC參數
串行收集器
- 最古老,最穩定
- 效率高
- 可能會產生較長的停頓
-XX:+UseSerialGC
- 新生代、老年代使用串行回收
- 新生代複製算法
- 老年代標記-壓縮
並行收集器
ParNew
-XX:+UseParNewGC
- 新生代並行
- 老年代串行
- Serial收集器新生代的並行版本
- 複製算法
- 多線程,需要多核支持
- -XX:ParallelGCThreads 限制線程數量
Parallel收集器
- 類似ParNew
- 新生代複製算法
- 老年代標記-壓縮
- 更加關注吞吐量
-XX:+UseParallelGC
- 使用Parallel收集器+老年代串行
-XX:+UseParallelOldGC
- 使用Parallel收集器+老年代並行
-XX:MaxGCPauseMills
- 最大停頓時間,單位毫秒
- GC盡力保證回收時間不超過設定值
-XX:GCTimeRatio
- 0-100的取值範圍
- 垃圾收集時間佔總時間的比
- 默認99,即最大允許1%時間做GC
XX:MaxGCPauseMills和XX:GCTimeRatio,這兩個參數是矛盾的,因爲停頓時間和吞吐量不可能同時調優
CMS收集器
CMS收集器
- Concurrent Mark Sweep 併發(與用戶線程一起執行)標記清除
- 標記-清除算法
- 併發階段會降低吞吐量
- 老年代收集器(新生代使用ParNew)
- -XX:+UseConcMarkSweepGC
CMS運行過程比較複雜,着重實現了標記過程,可分爲
初始標記
- 根可以直接關聯到的對象
- 速度快
併發標記(和用戶線程一起)
- 主要標記過程,標記全部對象
重新標記
- 由於併發標記時,用戶線程依然運行,因此在正式清理前,再做修正
併發清除(和用戶線程一起)
- 基於標記結果,直接清理對象
特點
- 儘可能降低停頓
會影響系統整體吞吐量和性能
- 比如,在用戶線程運行過程中,分一半CPU去做GC,系統性能在GC階段,反應速度就下降一半
清理不徹底
- 因爲在清理階段,用戶線程還在運行,會產生新的垃圾,無法清理
因爲和用戶線程一起運行,不能在空間快滿時再清理
- -XX:CMSInitiatingOccupancyFraction設置觸發GC的閥值
- 如果不幸內存預留空間不夠,就會引起concurrent mode failure
-XX:+UseCMSCompactAtFullCollection Full GC後,進行一次整理
- 整理過程是獨佔的,會引起停頓時間變長
-XX:+CMSFullGCsBeforeCompaction
- 設置進行幾次Full GC後,進行一次碎片整理
-XX:ParallelCMSThreads
- 設定CMS的線程數量
-XX:CMSInitiatingPermOccupancyFraction
- 當永久區佔用率達到這一百分比時,啓動CMS回收
XX:UseCMSInitiatingOccupancyOnly
- 表示只在到達閥值的時候,才進行CMS回收
類裝載器
class裝載驗證流程
加載
- 取得類的二進制流
- 轉爲方法區數據結構
- 在Java堆中生成對應的java.lang.Class對象
鏈接
驗證
目的:保證Class流的格式是正確的
文件格式的驗證
- 是否以0xCAFEBABE開頭
- 版本號是否合理
元數據驗證
- 是否有父類
- 是否繼承了final類
- 非抽象類是否實現了所有的抽象方法
字節碼驗證(很複雜)
- 運行檢查
- 棧數據類型和操作碼數據參數是否吻合
- 跳轉指令是否指定到合理的位置
準備
分配內存,併爲類設置初始值(方法區中)
- public static int v = 1;
- 在準備階段中,v會被設置爲0
- 在初始化的<clinit>中才會被設置爲1
- 對於static final類型,在準備階段就會被賦上正確的值
解析
- 符合引用(字符串)替換爲直接引用(指針或者地址偏移量)
初始化
執行類構造器<clinit>
- static變量賦值語句
- static{}語句
子類的<clinit>調用前保證父類的<clinit>被調用
- <clinit>是線程安全的
什麼是類裝載器ClassLoader
- ClassLoader是一個抽象類
- ClassLoader的實例將讀入Java字節碼將類裝載到JVM中
- ClassLoader可以定製,滿足不同的字節碼流獲取方式
- ClassLoader負責類裝載過程中的加載階段
ClassLoader的重要方法
public Class <?> loadClass(String name) throws ClassNotFoundException
- 載入並返回一個Class
protected final Class<?> defineClass(byte[] b, int off, int len)
- 定義一個類,不公開調用
protected Class<?> findClass(String name) throws ClassNotFoundException
- loadClass回調該方法,自定義ClassLoader的推薦做法
protected final Class<?> findLoadedClass(String name)
- 尋找已經加載的類
ClassLoader的分類
- BootStrap ClassLoader(啓動ClassLoader)
- Extension ClassLoader(擴展ClassLoader)
- App ClassLoader(應用ClassLoader/系統ClassLoader)
- Custom ClassLoader(自定義ClassLoader)
ClassLoader的協同工作
鎖
對象頭Mark
- Mark Word,對象頭的標記,32位
描述對象的hash、鎖信息,垃圾回收標記,年齡
- 指向鎖記錄的指針
- 指向monitor的指針
- GC標記
- 偏向鎖線程ID
偏向鎖
- 大部分情況是沒有競爭的,所以可以通過偏向來提高性能
- 所謂的偏向,就是偏心,即鎖會偏向於當前已經佔有鎖的線程
- 將對象頭Mark的標記設置爲偏向,並將線程ID寫入對象頭Mark
- 只要沒有競爭,獲得偏向鎖的線程,在將來進入同步塊,不需要做同步
- 當其他線程請求相同的鎖時,偏向模式結束
-XX:+UseBiasedLocking
- 默認啓用
在競爭激烈的場合,偏向鎖會增加系統負擔
輕量級鎖
- 普通的鎖處理性能不夠理想,輕量級鎖是一種快速的鎖定方法
如果對象沒有被鎖定
- 將對象頭的Mark指針保存到鎖對象中
- 將對象頭設置爲指向鎖的指針(在線程棧空間中)
- 如果輕量級鎖失敗,表示存在競爭,升級爲重量級鎖(常規鎖)
- 在沒有鎖競爭的前提下,減少傳統鎖使用OS互斥量產生的性能損耗
- 在競爭激烈時,輕量級鎖會多做很多額外操作,導致性能下降
自旋鎖
- 當競爭存在時,如果線程可以很快獲得鎖,那麼可以不在OS層掛起線程,讓線程做幾個空操作(自旋)
- JDK1.6中-XX:+UseSpinning開啓
- JDK1.7中,去掉此參數,改爲內置實現
- 如果同步塊很長,自旋失敗,會降低系統性能
- 如果同步塊很短,自旋成功,節省線程掛起切換時間,提升系統性能
JVM中獲取鎖的步驟
- 偏向鎖可用會嘗試偏向鎖
- 輕量級鎖可用會先嚐試輕量級鎖
- 以上都失敗,嘗試自旋鎖
- 再失敗,嘗試普通鎖,使用OS互斥量在操作系統層掛起
鎖優化方法
減少鎖持有時間
public synchronized void syncMethod(){ othercode1(); mutextMethod(); othercode2(); } => public void syncMethod2(){ othercode1() synchronized(this){ mutextMethod(); } othercode2(); }
減小鎖粒度
- 將大對象,拆成小對象,大大增加並行度,降低鎖競爭
- 偏向鎖,輕量級鎖成功率提高
ConcurrentHashMap
- 若干個Segment:Segment<K,V>[] segments
- Segment中維護HashEntry<K,V>
- put操作時,先定位到Segment,鎖定一個Segment,執行input
- 在減小鎖粒度後,ConcurrentHashMap允許若干個線程同時進入
鎖分離
- 根據功能進行鎖分離
- ReadWriteLock
- 讀多寫少的情況,可以提高性能
鎖粗化
如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化
public void demoMethod(){ synchronized(lock){ // do sth } // 做其他不需要同步的工作,但能很快執行完畢 synchronized(lock){ // do sth } } => public void demoMethod(){ // 整合成一次鎖請求 synchronized(lock){ // do sth // 做其他不需要同步的工作,但能很快執行完畢 // do sth } }
鎖消除
- 在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作
無鎖
- 鎖是悲觀的操作
- 無鎖是樂觀的操作
無鎖是一種實現方式
- CAS(Compare And Swap)
- 非阻塞的同步
- CAS(V,E,N)
在應用層面判斷多線程的干擾,如果有干擾,則通知線程重試