JVM相關知識總結整理

JVM啓動流程

jvm_001

JVM基本結構

jvm_002

PC寄存器

  • 每個線程擁有一個PC寄存器
  • 在線程創建時創建
  • 指向下一條指令的地址
  • 執行本地方法時,PC的值爲undefined

方法區

  • 保存裝載的類信息

    • 類型的常量池(JDK6時,String等常量池置於方法,JDK7時,已經移動到了堆)
    • 字段、方法信息
    • 方法字節碼
  • 通常和永久區(Perm)關聯在一起

Java堆

  • 和程序開發密切相關
  • 應用系統對象都保存在Java堆中
  • 所有線程共享Java堆
  • 對分代GC來說,堆也是分代的
  • GC的主要工作區間

    jvm_003

Java棧

  • 線程私有
  • 棧由一系列幀組成(因此Java棧也叫做幀棧)
  • 幀保存一個方法的局部變量、操作數棧、常量池指針
  • 每一次方法調用創建一個幀,並壓棧

Java棧——局部變量表 包含參數和局部變量

public static int runStatic(int i, long l, float f, Object o, byte b) {
    return 0;
}

靜態方法的局部變量表如下圖所示

jvm_004

public int runInstance(char c, short s, boolean b) {
    return 0;
}

實例方法的局部變量表如下圖所示

jvm_005

Java棧——函數調用組成幀棧

public static int runStatic(int i, long l, float f, Object o, byte b) {
    return runStatic(i, l, f, o, b);
}

jvm_006

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  // 返回
    

    jvm_007

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);
    }
}

jvm_008

內存模型

  • 每一個線程有一個工作內存和主內存
  • 工作內存存放主內存中變量的值的拷貝

當數據從主內存複製到工作存儲時,必須出現兩個動作:第一,由主內存執行的讀(read)操作;第二,由工作內存執行的相應的load操作;當數據從工作內存拷貝到主內存時,也出現兩個操作:第一個,由工作內存執行的存儲(store)操作;第二,由主內存執行的相應的寫(write)操作

jvm_009

每一個操作都是原子的,即執行期間不會被中斷

對於普通變量,一個線程中更新的值,不能馬上反應在其他線程中

jvm_010

如果需要在其他線程中立即可見,需要使用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中,兩句話順序可能打亂

    jvm_011

  • 指令重排——保證有序性的方法

    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線程也是順序執行的

    jvm_012

  • 指令重排的基本原則

    • 程序順序原則:一個線程內保證語義的串行性
    • 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詳細信息

    jvm_013

  • -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後,都打印堆信息

    jvm_014

  • -XX:+TraceClassLoading

    • 監控類的加載

    jvm_015

  • -XX:+PrintClassHistogram

    • 按下Ctrl+Break後,打印類的信息

    jvm_017

    • 分別顯示:序號、實例數量、總大小、類型

堆分配參數

  • -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
    

    jvm_018

    則沒有觸發GC,數據全部分配在老年代

    如果設置

    -Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails
    

    jvm_019

    則沒有觸發GC,數據全部分配在eden,老年代沒有使用

    如果設置

    -Xmx20m -Xms20m -Xmn7m -XX:+PrintGCDetails
    

    jvm_020

    則進行了2次新生代GC,s0 s1太小需要老年代擔保

    如果設置

    -Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
    

    jvm_021

    則進行了3次新生代GC,s0 s1增大

    如果設置

    -Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+PrintGCDetails
    

    jvm_022

    則進行了2次新生代GC,新生代空間增大

    如果設置

    -Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3 -XX:+PrintGCDetails
    

    jvm_023

    則進行了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]);
    }
    

    jvm_024

  • -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()); // 不斷地產生新的類
}

jvm_025

棧大小分配

  • -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的基本思路是,將熱點代碼,就是執行比較頻繁的代碼,編譯成機器碼

jvm_038

  • 相關參數

    • Xint

      • 解釋執行
    • Xcomp

      • 全部編譯執行
    • Xmixed

      • 默認,混合

GC算法與種類

引用計數法

  • 老牌垃圾回收算法
  • 通過引用計算來回收垃圾
  • 使用者

    • COM
    • ActionScript
    • Python

引用計數器的實現很簡單,對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1,當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,則對象A就不可能被再被使用

jvm_026

引用計數法的問題

  • 引用和去引用伴隨着加法和減法,影響性能
  • 很難處理循環引用

jvm_027

標記-清除

標記-清除算法是現代垃圾回收算法的思想基礎。標記-清除算法是將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先通過根結點,標記所有從根節點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象,然後,在清除階段,清除所有未被標記的對象

jvm_028

標記-壓縮

標記-壓縮算法適合用於存活對象較多的場合,如老年代,它在標記-清除算法的基礎上做了一些優化。和標記-清除算法一樣,標記-壓縮算法也首先需要從根節點開始,對所有可達對象做一次標記。但之後,它並不簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存的一端,之後,清理邊界外所有的空間

jvm_029

複製算法

  • 與標記-清除算法相比,複製算法是一種相對高效的回收方法
  • 不適用於存活對象較多的場合,如老年代
  • 將原有的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未使用的內存塊中,之後,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收

jvm_030

分代思想

  • 依據對象的存活週期進行分類,短命對象歸爲新生代,長命對象歸爲老年代
  • 根據不同代的特點,選取合適的收集算法

    • 少量對象存活,適合複製算法
    • 大量對象存活,適合標記清理或者標記壓縮

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

    • 新生代、老年代使用串行回收
    • 新生代複製算法
    • 老年代標記-壓縮

jvm_031

並行收集器

  • ParNew

    • -XX:+UseParNewGC

      • 新生代並行
      • 老年代串行
    • Serial收集器新生代的並行版本
    • 複製算法
    • 多線程,需要多核支持
    • -XX:ParallelGCThreads 限制線程數量

jvm_032

  • Parallel收集器

    • 類似ParNew
    • 新生代複製算法
    • 老年代標記-壓縮
    • 更加關注吞吐量
    • -XX:+UseParallelGC

      • 使用Parallel收集器+老年代串行
    • -XX:+UseParallelOldGC

      • 使用Parallel收集器+老年代並行

jvm_033

  • -XX:MaxGCPauseMills

    • 最大停頓時間,單位毫秒
    • GC盡力保證回收時間不超過設定值
  • -XX:GCTimeRatio

    • 0-100的取值範圍
    • 垃圾收集時間佔總時間的比
    • 默認99,即最大允許1%時間做GC
  • XX:MaxGCPauseMills和XX:GCTimeRatio,這兩個參數是矛盾的,因爲停頓時間和吞吐量不可能同時調優

CMS收集器

  • CMS收集器

    • Concurrent Mark Sweep 併發(與用戶線程一起執行)標記清除
    • 標記-清除算法
    • 併發階段會降低吞吐量
    • 老年代收集器(新生代使用ParNew)
    • -XX:+UseConcMarkSweepGC

CMS運行過程比較複雜,着重實現了標記過程,可分爲

  • 初始標記

    • 根可以直接關聯到的對象
    • 速度快
  • 併發標記(和用戶線程一起)

    • 主要標記過程,標記全部對象
  • 重新標記

    • 由於併發標記時,用戶線程依然運行,因此在正式清理前,再做修正
  • 併發清除(和用戶線程一起)

    • 基於標記結果,直接清理對象

jvm_034

  • 特點

    • 儘可能降低停頓
    • 會影響系統整體吞吐量和性能

      • 比如,在用戶線程運行過程中,分一半CPU去做GC,系統性能在GC階段,反應速度就下降一半
    • 清理不徹底

      • 因爲在清理階段,用戶線程還在運行,會產生新的垃圾,無法清理
    • 因爲和用戶線程一起運行,不能在空間快滿時再清理

      • -XX:CMSInitiatingOccupancyFraction設置觸發GC的閥值
      • 如果不幸內存預留空間不夠,就會引起concurrent mode failure

jvm_035

  • -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的協同工作

jvm_036

對象頭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
  • 讀多寫少的情況,可以提高性能

jvm_037

鎖粗化

  • 如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化

    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)
  • 在應用層面判斷多線程的干擾,如果有干擾,則通知線程重試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章