JVM內核學習筆記

一、補充知識

  • 補碼: 正數的補碼是其本身,負數的補碼是反碼加1

    例如:-6

    原碼:10000110
    反碼:11111001
    補碼:11111010
    
  • 爲什麼要有補碼的存在?

    1. 無歧義的表示零:0; 0既不屬於正數,也不屬於負數

      用正數表示0:
      
          源碼: 00000000
          反碼: 01111111
          補碼: 00000000
      
      用負數表示0:
      
          源碼: 10000000
          反碼: 11111111
          補碼: 00000000
      
    2. 方便計算機計算,補碼相加符號位可以直接參與運算

二、JVM運行機制

1. JVM啓動流程

這裏寫圖片描述

說明:

  • 裝在配置:根據當前路徑和系統版本尋找jvm.cfg
  • JVM.dll爲JVM主要實現
  • JNIEnv爲JVM接口,findClass等操作通過它實現

2. JVM基本結構

  • 運行時數據區:

    1. 方法區;

      • 保存裝載的類信息
      • -類型的常量池
      • 字段,方法信息
      • 方法字節碼
      • 通常和永久區(Perm)關聯在一起
      • PS: JDK6時,String等常量信息置於方法區,JDK7時,已經移動到了堆,JDK8移除永久代,增加了元空間MetaSpace

        1. 本地方法棧;

        2. 堆是線程共享的,所有通過new關鍵字生成的對象,都在堆中。java GC主要針對的區域就是堆,GC算法跟堆算法密切相關,例如分代收集算法要求堆,必須是分代存放數據的。
        3. 線程私有

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

          1. 小對象(一般幾十個bytes),在沒有逃逸的情況下,可直接分配在棧上
          2. 直接分配棧上,可以自動回收,減輕GC壓力(一個函數調用結束,棧幀自動回收)
          3. 大對象或者逃逸對象無法棧上分配
        5. PC寄存器

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

3. 內存模型

  1. 每一個線程有一個工作內存和主存獨立
  2. 工作內存存放主存中變量的值的拷貝
  3. 當數據從主內存複製到工作存儲時,必須出現兩個動作

    1. 第一:由主內存執行的讀(read)操作;
    2. 第二,由工作內存執行的相應的load操作;當數據從工作內存拷貝到主內存時,也出現兩個操作:

      1. 第一個,由工作內存執行的存儲(store)操作;
      2. 第二個,由主內存執行的相應的寫(write)操作。每一個操作都是原子的,即執行期間不會被中斷,對於普通變量,一個線程中更新的值,不能馬上反應在其他變量中,如果需要在其他線程中立即可見,需要使用 volatile 關鍵字。
    3. 如下圖所示:
      這裏寫圖片描述

  4. 可見性

    1. 一個線程修改了變量,其他線程可以立即知道
    2. 保證可見性的方法
    3. volatile
    4. synchronized (unlock之前,寫變量值回主存)
    5. final(一旦初始化完成,其他線程就可見)
  5. 有序性

    1. 在本線程內,操作都是有序的
    2. 在線程外觀察,操作都是無序的。(指令重排 或 主內存同步延時)
  6. 指令重排

    1. 線程內串行語義
      • 寫後讀 a = 1;b = a; 寫一個變量之後,再讀這個位置。
      • 寫後寫 a = 1;a = 2; 寫一個變量之後,再寫這個變量。
      • 讀後寫 a = b;b = 1; 讀一個變量之後,再寫這個變量。
      • 以上語句不可重排
      • 編譯器不考慮多線程間的語義
      • 可重排: a=1;b=2;
    2. 反例:
      這裏寫圖片描述
      這裏寫圖片描述
    3. 指令重排的基本原則
      1. 程序順序原則:一個線程內保證語義的串行性
      2. volatile規則:volatile變量的寫,先發生於讀
      3. 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
      4. 傳遞性:A先於B,B先於C 那麼A必然先於C
      5. 線程的start方法先於它的每一個動作
      6. 線程的所有操作先於線程的終結(Thread.join())
      7. 線程的中斷(interrupt())先於被中斷線程的代碼
      8. 對象的構造函數執行結束先於finalize()方法

4.字節碼運行的兩種方式 - 編譯和解釋運行

  1. 解釋運行
    1. 解釋執行以解釋方式運行字節碼
    2. 解釋執行的意思是:讀一句執行一句
  2. 編譯運行(JIT– just in time)
    1. 將字節碼編譯成機器碼
    2. 直接執行機器碼
    3. 運行時編譯
    4. 編譯後性能有數量級的提升
  3. java代碼->字節碼->機器碼
  4. 4.

常用JVM配置參數

Trace跟蹤參數

  1. 打印GC的簡要信息 -verbose:gc-XX:+printGC
    輸出如下:

    [GC 4790K->374K(15872K), 0.0001606 secs]
    [GC 4790K->374K(15872K), 0.0001474 secs]
    [GC 4790K->374K(15872K), 0.0001563 secs]
    [GC 4790K->374K(15872K), 0.0001682 secs]

  2. 打印GC詳細信息 : -XX:+PrintGCDetails
    打印CG發生的時間戳: -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] 

    PrintGCDetails的輸出

    def new generation   total 13824K, used 11223K [0x27e80000, 0x28d80000, 0x28d80000)
    eden space 12288K,  91% used [0x27e80000, 0x28975f20, 0x28a80000)
    from space 1536K,   0% used [0x28a80000, 0x28a80000, 0x28c00000)
    to   space 1536K,   0% used [0x28c00000, 0x28c00000, 0x28d80000)
    tenured generation   total 5120K, used 0K [0x28d80000, 0x29280000, 0x34680000)
    the space 5120K,   0% used [0x28d80000, 0x28d80000, 0x28d80200, 0x29280000)
    compacting perm gen  total 12288K, used 142K [0x34680000, 0x35280000, 0x38680000)
    the space 12288K,   1% used [0x34680000, 0x346a3a90, 0x346a3c00, 0x35280000)
    ro space 10240K,  44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)
    rw space 12288K,  52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000)
    
  3. -Xloggc:log/gc.log

    • 指定GC log的位置,以文件輸出
    • 幫助開發人員分析問題
  4. -XX:+PrintHeapAtGC

    • 每次一次GC後,都打印堆信息
  5. -XX:+TraceClassLoading
    監控類的加載

    [Loaded java.lang.Object from shared objects file]
    [Loaded java.io.Serializable from shared objects file]
    [Loaded java.lang.Comparable from shared objects file]
    [Loaded java.lang.CharSequence from shared objects file]
    [Loaded java.lang.String from shared objects file]
    [Loaded java.lang.reflect.GenericDeclaration from shared objects file]
    [Loaded java.lang.reflect.Type from shared objects file]
  6. -XX:+PrintClassHistogram

    • 按下Ctrl+Break後,打印類的信息:
    • 分別顯示:序號、實例數量、總大小、類型
num instances bytes class name
1: 890617 470266000 [B
2: 890643 21375432 java.util.HashMap$Node
3: 890608 14249728 java.lang.Long
4: 13 8389712 [Ljava.util.HashMap$Node;
5: 2062 371680 [C
6: 463 41904 java.lang.Class

堆的分配參數

  1. -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會儘可能維持在最小堆,所以free mem和total mem的值會隨着內存使用的變化而變化

  2. -Xmn :設置新生代大小

  3. -XX:NewRatio
    • 新生代(eden+2*s)和老年代(不包含永久區)的比值
    • 4 表示 新生代:老年代=1:4,即年輕代佔堆的1/5
  4. -XX:SurvivorRatio
    • 設置兩個Survivor區和eden的比
    • 8表示 兩個Survivor :eden=2:8,即一個Survivor佔年輕代的1/10
  5. -XX:+HeapDumpOnOutOfMemoryError
    • OOM時導出堆到文件
  6. -XX:+HeapDumpPath
    • 導出OOM的路徑
  7. -XX:OnOutOfMemoryError

    • 在OOM時,執行一個腳本”-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p”
    • 當程序OOM時,在D:/a.txt中將會生成線程的dump
    • 可以在OOM時,發送郵件,甚至是重啓程序
  8. -XX:PermSize -XX:MaxPermSize

    • 設置永久區的初始空間和最大空間
    • 他們表示,一個系統可以容納多少個類型
    • 使用CGLIB等庫的時候,可能會產生大量的類,這些類,有可能撐爆永久區導致OOM
根據實際事情調整新生代和倖存代的大小
官方推薦新生代佔堆的3/8
倖存代佔新生代的1/10
在OOM時,記得Dump出堆,確保可以排查現場問題

棧的分配參數

  1. -Xss
    • 通常只有幾百K
    • 決定了函數調用的深度
    • 每個線程都有獨立的棧空間
    • 局部變量、參數 分配在棧上

GC 算法與種類

GC的對象是堆空間和永久區

GC的算法

  1. 引用計數法

    • 老牌垃圾回收算法
    • 通過引用計算來回收垃圾
    • 引用計數器的實現很簡單,對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1,當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,則對象A就不可能再被使用。
    • 引用計數法的問題:
      • 引用和去引用伴隨加法和減法,影響性能
      • 很難處理循環引用
    • 使用者:
      • COM
      • ActionScript3
      • Python
  2. 標記清除
    標記-清除算法是現代垃圾回收算法的思想基礎。標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先通過根節點,標記所有從根節點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象。然後,在清除階段,清除所有未被標記的對象
    這裏寫圖片描述

  3. 標記壓縮
    標記-壓縮算法適合用於存活對象較多的場合,如老年代。它在標記-清除算法的基礎上做了一些優化。和標記-清除算法一樣,標記-壓縮算法也首先需要從根節點開始,對所有可達對象做一次標記。但之後,它並不簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之後,清理邊界外所有的空間。
    這裏寫圖片描述
  4. 複製算法
    • 與標記-清除算法相比,複製算法是一種相對高效的回收方法
    • 不適用於存活對象較多的場合 如老年代
    • 將原有的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未使用的內存塊中,之後,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收
      這裏寫圖片描述
      複製算法的最大問題是:空間浪費 整合標記清理思想
  5. 複製算法優化【分代收集思想】
    這裏寫圖片描述

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

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

可觸及性

  1. 可觸及的
    • 從根節點可以觸及到這個對象
  2. 可復活的
    • 一旦所有引用被釋放,就是可復活狀態
    • 因爲在finalize()中可能復活該對象
  3. 不可觸及的

    • 在finalize()後,可能會進入不可觸及狀態
    • 不可觸及的對象不可能復活
    • 可以回收
  4. 經驗

    • 避免使用finalize(),操作不慎可能導致錯誤。
    • 優先級低,何時被調用, 不確定
    • 何時發生GC不確定
    • 可以使用try-catch-finally來替代它

    • 棧中引用的對象
    • 方法區中靜態成員或者常量引用的對象(全局對象)
    • JNI方法棧中引用對象

Stop-The-World

  • Java中一種全局暫停的現象
  • 全局停頓,所有Java代碼停止,native代碼可以執行,但不能和JVM交互
  • 多半由於GC引起
    1. Dump線程
    2. 死鎖檢查
    3. 堆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
這兩個參數是矛盾的。因爲停頓時間和吞吐量不可能同時調優

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的線程數量

GC參數整理

-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:設置eden區大小和survivior區大小的比例
-XX:NewRatio:新生代和老年代的比
-XX:+UseParNewGC:在新生代使用並行收集器
-XX:+UseParallelGC :新生代使用並行回收收集器
-XX:+UseParallelOldGC:老年代使用並行回收收集器
-XX:ParallelGCThreads:設置用於垃圾回收的線程數
-XX:+UseConcMarkSweepGC:新生代使用並行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:設定CMS的線程數量
-XX:CMSInitiatingOccupancyFraction:設置CMS收集器在老年代空間被使用多少後觸發
-XX:+UseCMSCompactAtFullCollection:設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片的整理
-XX:CMSFullGCsBeforeCompaction:設定進行多少次CMS垃圾回收後,進行一次內存壓縮
-XX:+CMSClassUnloadingEnabled:允許對類元數據進行回收
-XX:CMSInitiatingPermOccupancyFraction:當永久區佔用率達到這一百分比時,啓動CMS回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到達閥值的時候,才進行CMS回收

類加載器

class裝載驗證流程

  • 加載

    • 裝載類的第一個階段
    • 取得類的二進制流
    • 轉爲方法區數據結構
    • 在Java堆中生成對應的java.lang.Class對象
  • 鏈接

    • 驗證

      • 目的:保證Class流的格式是正確的
        • 文件格式的驗證
          • 是否以0xCAFEBABE開頭
          • 版本號是否合理
        • 元數據驗證
          • 是否有父類
          • 繼承了final類?
          • 非抽象類實現了所有的抽象方法
        • 字節碼驗證 (很複雜)
          • 運行檢查
          • 棧數據類型和操作碼數據參數吻合
          • 跳轉指令指定到合理的位置
        • 符號引用驗證
          • 常量池中描述類是否存在
          • 訪問的方法或字段是否存在且有足夠的權限
    • 準備

    • 分配內存,併爲類設置初始值 (方法區中)

      • public static int v=1;
      • 在準備階段中,v會被設置爲0
      • 在初始化的中才會被設置爲1
      • 對於static final類型,在準備階段就會被賦上正確的值
      • public static final int v=1;
    • 解析

      • 符號引用替換爲直接引用
  • 初始化
    • 執行類構造器
      • static變量 賦值語句
      • static{}語句
      • 子類的調用前保證父類的被調用
      • 是線程安全的

什麼是類裝載器ClassLoader

  • 概念
    • ClassLoader是一個抽象類
    • ClassLoader的實例將讀入Java字節碼將類裝載到JVM中
    • ClassLoader可以定製,滿足不同的字節碼流獲取方式
    • ClassLoader負責類裝載過程中的加載階段

JDK中ClassLoader默認設計模式

  • ClassLoader的重要方法
/** 載入並返回一個Class*/
public Class<?> loadClass(String name) throws ClassNotFoundException;
/** 定義一個類,不公開調用*/
protected final Class<?> defineClass(byte[] b, int off, int len);
/**回調該方法,自定義ClassLoader的推薦做法*/
protected Class<?> findClass(String name) throws ClassNotFoundException loadClass;
/** 尋找已經加載的類*/
protected final Class<?> findLoadedClass(String name);
  • 分類
    • BootStrap ClassLoader (啓動ClassLoader)[rt.jar /-Xbootclasspath]
    • Extension ClassLoader (擴展ClassLoader)[%JAVA_HOME%/lib/ext/*.jar]
    • App ClassLoader (應用ClassLoader/系統ClassLoader)[Classpath下]
    • Custom ClassLoader(自定義ClassLoader) [完全自定義路徑]
    • 每個ClassLoader都有一個Parent作爲父親
  • 問題
    • 頂層ClassLoader,無法加載底層ClassLoader的類
    • Java框架(rt.jar)如何加載應用的類?
    • javax.xml.parsers包中定義了xml解析的類接口,Service Provider Interface SPI位於rt.jar ,即接口在啓動ClassLoader中。而SPI的實現類,在AppLoader。
  • 解決
    • Thread. setContextClassLoader()
      • 上下文加載器
      • 是一個角色
      • 用以解決頂層ClassLoader無法訪問底層ClassLoader的類的問題
      • 基本思想是,在頂層ClassLoader中,傳入底層ClassLoader的實例

打破常規模式

  • 雙親模式的破壞
    • 雙親模式是默認的模式,但不是必須這麼做
    • Tomcat的WebappClassLoader 就會先加載自己的Class,找不到再委託parent
    • OSGi的ClassLoader形成網狀結構,根據需要自由加載Class

熱替換

  • 類被加載後,無需重啓服務器,新加載的類即可直接使用

性能監控工具

  • 系統性能監控

    • 確定系統運行的整體狀態,基本定位問題所在
      • uptime
        • 系統時間
        • 運行時間
        • 連接數
        • 1,5,15分鐘內的系統平均負載
      • top
      • vmstat
      • pidstat
  • Java自帶的工具

    • jps
      • 列出java進程,類似於ps命令
      • 參數-q可以指定jps只輸出進程ID ,不輸出類的短名稱
      • 參數-m可以用於輸出傳遞給Java進程(主函數)的參數
      • 參數-l可以用於輸出主函數的完整路徑
      • 參數-v可以顯示傳遞給JVM的參數
    • jinfo
      • 可以用來查看正在運行的Java應用程序的擴展參數,甚至支持在運行時,修改部分參數
      • -flag :打印指定JVM的參數值
      • -flag [+|-]:設置指定JVM參數的布爾值
      • -flag =:設置指定JVM參數的值
    • jmap
      • 生成Java應用程序的堆快照和對象的統計信息
      • jmap -histo 2972 >c:\s.txt
      • Dump堆
        • jmap -dump:format=b,file=c:\heap.hprof 2972
    • jstack

      • 打印線程dump
      • -l 打印鎖信息
      • -m 打印java和native的幀信息
      • -F 強制dump,當jstack沒有響應時使用
    • JConsole

      • 圖形化監控工具
      • 可以查看Java應用程序的運行概況,監控堆信息、永久區使用情況、類加載情況等
    • Visual VM
      • Visual VM是一個功能強大的多合一故障診斷和性能監控的可視化工具

Java堆分析

內存溢出(OOM)的原因

  • 可能發生內存溢出的內存空間
    • 永久區
      • 用cglib生成大量的類,類的元信息是保存在永久區的
    • 線程棧
      • 這裏的棧溢出指,在創建線程的時候,需要爲線程分配棧空間,這個棧空間是向操作系統請求的,如果操作系統無法給出足夠的空間,就會拋出OOM
    • 直接內存
      • ByteBuffer.allocateDirect()無法從操作系統獲得足夠的空間

線程安全

  • 多線程網站統計訪問人數
  • 使用鎖,維護計數器的串行訪問與安全性
  • 多線程訪問ArrayList[兩個線程同時往ArrayList中添加百萬元素,可能會發生越界異常]

對象頭Mark

  • Mark Word,對象頭的標記,32位
  • 描述對象的hash、鎖信息,垃圾回收標記,年齡
    • 指向鎖記錄的指針
    • 指向monitor的指針
    • GC標記
    • 偏向鎖線程ID

偏向鎖

  • 大部分情況是沒有競爭的,所以可以通過偏向來提高性能
  • 所謂的偏向,就是偏心,即鎖會偏向於當前已經佔有鎖的線程
  • 將對象頭Mark的標記設置爲偏向,並將線程ID寫入對象頭Mark
  • 只要沒有競爭,獲得偏向鎖的線程,在將來進入同步塊,不需要做同步
  • 當其他線程請求相同的鎖時,偏向模式結束
  • -XX:+UseBiasedLocking
    • 默認啓用
  • 在競爭激烈的場合,偏向鎖會增加系統負擔
  • -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0-XX:-UseBiasedLocking

輕量級鎖

  • BasicObjectLock
    • 嵌入在線程棧中的對象
  • 普通的鎖處理性能不夠理想,輕量級鎖是一種快速的鎖定方法。
  • 如果對象沒有被鎖定
    • 將對象頭的Mark指針保存到鎖對象中
    • 將對象頭設置爲指向鎖的指針(在線程棧空間中)
  • 如果輕量級鎖失敗,表示存在競爭,升級爲重量級鎖(常規鎖)
  • 在沒有鎖競爭的前提下,減少傳統鎖使用OS互斥量產生的性能損耗
  • 在競爭激烈時,輕量級鎖會多做很多額外操作,導致性能下降

自旋鎖

  • 當競爭存在時,如果線程可以很快獲得鎖,那麼可以不在OS層掛起線程,讓線程做幾個空操作(自旋)
  • JDK1.6中-XX:+UseSpinning開啓
  • JDK1.7中,去掉此參數,改爲內置實現
  • 如果同步塊很長,自旋失敗,會降低系統性能
  • 如果同步塊很短,自旋成功,節省線程掛起切換時間,提升系統性能

幾種鎖的總結

  • 不是Java語言層面的鎖優化方法
  • 內置於JVM中的獲取鎖的優化方法和獲取鎖的步驟
  • 偏向鎖可用會先嚐試偏向鎖
  • 輕量級鎖可用會先嚐試輕量級鎖
  • 以上都失敗,嘗試自旋鎖
  • 再失敗,嘗試普通鎖,使用OS互斥量在操作系統層掛起

減少鎖持有時間

public synchronized void syncMethod(){
    othercode1();
    mutextMethod();
    othercode2();
}

改爲:

public void syncMethod2(){
    othercode1();
    synchronized(this){
        mutextMethod();
    }
    othercode2();
}

減小鎖粒度

  • 將大對象,拆成小對象,大大增加並行度,降低鎖競爭
  • 偏向鎖,輕量級鎖成功率提高
  • ConcurrentHashMap
  • HashMap的同步實現
    • Collections.synchronizedMap(Map

鎖分離

  • 根據功能進行鎖分離
  • ReadWriteLock
  • 讀多寫少的情況,可以提高性能
類型 讀鎖 寫鎖
讀鎖 可訪問 不可訪問
寫鎖 不可訪問 不可訪問
  • 讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離
  • LinkedBlockingQueue
    • 隊列
    • 鏈表

鎖粗化

  • 通常情況下,爲了保證多線程間的有效併發,會要求每個線程持有鎖的時間儘量短,即在使用完公共資源後,應該立即釋放鎖。只有這樣,等待在這個鎖上的其他線程才能儘早的獲得資源執行任務。但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化
for(int i=0;i<CIRCLE;i++){
    synchronized(lock){

    }
}

改爲:

synchronized(lock){
for(int i=0;i<CIRCLE;i++){

    }
}

鎖消除

  • 在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作
  • 例如StringBuffer爲局部變量時,其內部的鎖實現其實是不需要的
  • 參數
    • -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks 鎖消除
    • -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks 不使用鎖消除

無鎖

  • 鎖是悲觀的操作
  • 鎖是樂觀的操作
  • 無鎖的一種實現方式
    • CAS(Compare And Swap)
    • 非阻塞的同步
    • CAS(V,E,N)
  • 在應用層面判斷多線程的干擾,如果有干擾,則通知線程重試
  • java.util.concurrent.atomic.AtomicInteger
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章