JVM內存結構和Java內存模型別再傻傻分不清了

JVM內存結構和Java內存模型都是面試的熱點問題,名字看感覺都差不多,網上有些博客也都把這兩個概念混着用,實際上他們之間差別還是挺大的。
通俗點說,JVM內存結構是與JVM的內部存儲結構相關,而Java內存模型是與多線程編程相關,本文針對這兩個總是被混用的概念展開講解。

JVM內存結構

JVM構成

說到JVM內存結構,就不會只是說內存結構的5個分區,而是會延展到整個JVM相關的問題,所以先了解下JVM的構成。

在這裏插入圖片描述

  • Java源代碼編譯成Java Class文件後通過類加載器ClassLoader加載到JVM中
    • 類存放在方法區
    • 類創建的對象存放在
    • 堆中對象的調用方法時會使用到虛擬機棧,本地方法棧,程序計數器
    • 方法執行時每行代碼由解釋器逐行執行
    • 熱點代碼由JIT編譯器即時編譯
    • 垃圾回收機制回收堆中資源
    • 和操作系統打交道需要調用本地方法接口

JVM內存結構

程序計數器

在這裏插入圖片描述
(通過移位寄存器實現)

  • 程序計數器是線程私有的,每個線程單獨持有一個程序計數器
  • 程序計數器不會內存溢出

虛擬機棧

  • 棧:線程運行需要的內存空間

  • 棧幀:每一個方法運行需要的內存(包括參數,局部變量,返回地址等信息)

  • 每個線程只有一 個活動棧幀(棧頂的棧幀),對應着正在執行的代碼
    在這裏插入圖片描述

  • 常見問題解析

    • 垃圾回收是否涉及棧內存:不涉及,垃圾回收只涉及堆內存

    • 棧內存分配越大越好嗎:內存一定時,棧內存越大,線程數就越少,所以不應該過大

    • 方法內的局部變量是否是線程安全的:

      • 普通局部變量是安全的
      • 靜態的局部變量是不安全的
      • 對象類型的局部變量被返回了是不安全的
      • 基本數據類型局部變量被返回時安全的
      • 參數傳入對象類型變量是不安全的
      • 參數傳入基本數據類型變量時安全的
    • 棧內存溢出(StackOverflowError)

      • 棧幀過多

        • 如遞歸調用沒有正確設置結束條件
      • 棧幀過大

        • json數據轉換 對象嵌套對象 (用戶類有部門類屬性,部門類由用戶類屬性)
      • 線程運行診斷

        • CPU佔用過高(定位問題)

          • ‘top’命令獲取進程編號,查找佔用高的進程
          • ‘ps H -eo pid,tid,%cpu | grep 進程號’ 命令獲取線程的進程id,線程id,cpu佔用
          • 將查看到的佔用高的線程的線程號轉化成16進制的數 :如6626->19E2
          • ‘ jstack 進程id ’獲取進程棧信息, 查找‘nid=0X19E2’的線程
          • 問題線程的最開始‘#數字’表示出現問題的行數,回到代碼查看
        • 程序運行很長時間沒有結果(死鎖問題)

          • ‘ jstack 進程id ’獲取進程棧信息
          • 查看最後20行左右有無‘Fount one Java-level deadlock’
          • 查看下面的死鎖的詳細信息描述和問題定位
          • 回到代碼中定位代碼進行解決

本地方法棧

  • 本地方法棧爲虛擬機使用到的 Native 方法服務
  • Native 方法是 Java 通過 JNI 直接調用本地 C/C++ 庫,可以認爲是 Native 方法相當於 C/C++ 暴露給 Java 的一個接口
  • 如notify,hashcode,wait等都是native方法

  • 通過new關鍵字創建的對象都會使用堆內存

  • 堆是線程共享的

  • 堆中有垃圾回收機制

  • 堆內存溢出(OutOfMemoryError)

    • 死循環創建對象
  • 堆內存診斷

    • 命令行方式

      • ‘jps’獲取運行進程號
      • ‘jmap -heap 進程號’查看當前時刻的堆內存信息
    • jconsole

      • 命令行輸入jconsole打開可視化的界面連接上進程
      • 可視化的檢測連續的堆內存信息
    • jvisualvm

      • 命令行輸入jvisualvm打開可視化界面選擇進程
      • 可視化的查看堆內存信息

方法區

  • 方法區只是一種概念上的規範,具體的實現各種虛擬機和不同版本不相同
    • HotSpot1.6 使用永久代作爲方法區的實現
    • HotSpot1.8使用本地內存的元空間作爲方法區的實現(但StringTable還是放在堆中)
      在這裏插入圖片描述
  • 常見問題
    • StringTable特性

      • 常量池中的字符串僅是字符,第一次使用時才變爲對象

      • 利用串池機制,避免重複創建字符串

      • 字符串常量拼接原理是StringBuilder(1.8)

      • 字符串常量拼接原理是編譯器優化

      • StringTable在1.6中存放在永久代,在1.8中存放在堆空間

      • intern方法主動將串池中沒有的字符串對象放入串池

        • 1.8中:嘗試放入串池,如果有就不放入,只返回一個引用;如果沒有就放入串池,同時返回常量池中對象引用

        • 1.6中:嘗試放入串池,如果有就不放入,只返回一個引用;如果沒有就複製一個放進去(本身不放入),同時返回常量池中的對象引用

        • 字符串常量池分析(1.8環境)

          String s1 = "a";
          String s2 = "b";
          String s3 = "a"+"b";
          String s4 = s1+s2;
          String s5 = "ab";
          String s6 = s4.intern();
          
          
          System.out.println(s3==s4);// s3在常量池中,s4在堆上(intern嘗試s4放入常量池,因爲ab存在了就拒絕放入返回ab引用給s6,s4還是堆上的)
          System.out.println(s3==s5);// s3在常量池中,s4也在常量池中(字符串編譯期優化)
          System.out.println(s3==s6);// s3在常量池中,s6是s4的intern返回常量池中ab的引用,所以也在常量池中
          
          
          String x2 = new String("c")+new String("d");
          String x1 = "cd";
          x2.intern();
          
          System.out.println(x1==x2);//x2調用intern嘗試放入常量池,但常量池中已經有cd了,所以只是返回一個cd的引用,而x2還是堆上的引用
          
    • JVM調優三大參數(如: java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar)

      • -Xss:規定了每個線程虛擬機棧的大小(影響併發線程數大小)
      • -Xms:堆大小的初始值(超過初始值會擴容到最大值)
      • -Xmx:堆大小的最大值(通常初始值和最大值一樣,因爲擴容會導致內存抖動,影響程序運行穩定性)
    • JVM內存結構中堆和棧的區別

      • 管理方式:棧自動釋放,堆需要GC
      • 空間大小:棧比堆小
      • 碎片:棧產生的碎片遠少於堆
      • 分配方式:棧支持靜態分配和動態分配,堆只支持動態分配
      • 效率:棧的效率比堆高

GC垃圾回收機制

1. 垃圾判別方法

引用計數算法
  • 判斷對象的引用數量來決定對象是否可以被回收

  • 每個對象實例都有一個引用計數器,被引用則+1,完成引用則-1

  • 優點:執行效率高,程序執行受影響小

  • 缺點:無法檢測出循環引用的情況,導致內存泄露

可達性分析算法
  • Java虛擬機中的垃圾回收器採用可達性分析來探索所有存活對象

  • 掃描堆中的對象,看是否能沿着GC Root對象爲起點的引用鏈找到該對象,找不到則可以回收

  • 哪些對象可以作爲GC Root

  • 通過System Class Loader或者Boot Class Loader加載的class對象,通過自定義類加載器加載的class不一定是GC Root

    • 虛擬機棧中的引用的對象

    • 本地方法棧中JNI(natice方法)的引用的對象

    • 方法區中的常量引用的對象

    • 方法區中的類靜態屬性引用的對象

    • 處於激活狀態的線程

    • 正在被用於同步的各種鎖對象

    • GC保留的對象,比如系統類加載器等。

2. 垃圾回收算法

標記清除法
  • 標記沒有被GC Root引用的對象
  • 清除被標記位置的內存
  • 優點:處理速度快
  • 缺點:造成空間不連續,產生內存碎片
    在這裏插入圖片描述
標記整理法
  • 標記沒有被GC Root引用的對象
  • 整理被引用的對象
  • 優點:空間連續,沒有內存碎片
  • 缺點:整理導致效率較低

在這裏插入圖片描述

複製算法
  • 分配同等大小的內存空間
  • 標記被GC Root引用的對象
  • 將引用的對象連續的複製到新的內存空間
  • 清除原來的內存空間
  • 交換FROM空間和TO空間
  • 優點:空間連續,沒有內存碎片
  • 缺點:佔用雙倍的內存空間
    在這裏插入圖片描述
    在這裏插入圖片描述
    在這裏插入圖片描述

3. 分代垃圾回收機制

  • 分代垃圾回收流程
    在這裏插入圖片描述

    • 對象首先分配在伊甸園區域
    • 新生代空間不足時,觸發Minor GC,伊甸園和from存活的對象使用【複製算法】複製到to中,存活的對象年齡加一,並且交換from區和to區
    • Minor GC會引發Stop the world(STW)現象,暫停其他用戶的線程。垃圾回收結束後,用戶線程才恢復運行
    • 當對象壽命超過閾值時,會晉升至老年代,最大壽命是15(4位二進制)
    • 當老年代空間不足,會先嚐試觸發Minor GC,如果之後空間仍不足,會觸發Full GC(STW時間更長,老年代可能使用標籤清除或標記整理算法)
    • 當存放大對象新生代放不下而老年代可以放下,大文件會直接晉升到老年代
    • 當存放大對象新生代和老年代都放不下時,拋出OOM異常
  • 默認堆內存分配
    在這裏插入圖片描述

    • 新生代佔1/3,老年代佔2/3
    • -XX:NewRatio:老年代和年輕代內存大小的比例
    • 新生代中按8 1 1進行分配,兩個倖存區大小需要保持一致
    • -XX:SurvivorRatio: Eden和Survivor的比值,默認是8(8:1)
  • GC相關VM參數
    在這裏插入圖片描述

4. 垃圾回收器

  • 安全點(SafePoint)

    • 分析過程中對象引用關係不會發生改變的點

    • 產生安全點的地方:

      • 方法調用
      • 循環跳轉
      • 異常跳轉
    • 安全點的數量應該設置適中

  • 串行(SerialGC)

    • 單線程的垃圾回收器
    • 堆內存較小,CPU核數少,適合個人電腦
    • SerialGC收集器 (-XX:+UseSerialGC 複製算法) Client模式下默認的年輕代收集器
    • SerialGC Old收集器 (-XX:+UseSerialOldGC 標記-整理算法)Client模式下默認的老年代收集器
      在這裏插入圖片描述
  • 吞吐量優先(ParallelGC)

    • 多線程的垃圾回收器
    • 堆內存較大,多核CPU,適合服務器
    • 儘可能讓單位時間內STW暫停時間最短(吞吐量=運行代碼時間/(運行代碼時間+垃圾回收時間))
    • 並行的執行
    • ParallelGC收集器(-XX:+UseParallelGC 複製算法) Server模式下默認的年輕代垃圾回收器
    • ParallelGC Old收集器(-XX:+UseParallelOldGC 複製算法)

    在這裏插入圖片描述

  • 響應時間優先(CMS -XX:+UseConcMarkSweepGC 標記清除算法)

    • 多線程的垃圾回收器

    • 堆內存較大,多核CPU,Server模式下默認的老年代垃圾回收器

    • 儘可能讓單次STW暫停時間最短

    • 部分時期內可以併發執行

    • 執行流程

      • 初始標記:stop-the-world
      • 併發標記:併發追溯標記,程序不會停頓
      • 併發預清理:查找執行併發標記階段從年輕代晉升到老年代的對象
      • 重新標記:暫停虛擬機,掃描CMS堆中的剩餘對象
      • 併發清理:清理垃圾對象,程序不會停頓
      • 併發重置:重置CMS收集器的數據結構

在這裏插入圖片描述

  • G1(-XX:+UseG1GC 複製+標記清除算法)

    • G1l垃圾回收器簡介
    • 定義:Garbage First (2017 jdk9 默認)
    • 特點
      • 併發和並行
      • 分代收集
      • 空間整合
      • 可預測的停頓
    • 使用場景
      • 同時注重吞吐量和低延遲,默認暫停目標是200ms
      • 超大堆內存,會將整個堆劃分爲多個大小相等的Region(新生代和老年代不再物理隔離了)
      • 整體上是標記整理算法,兩個區域之間是複製算法
  • 垃圾回收階段

    • 新生代垃圾收集

      • 會發生STW
    • 新生代垃圾收集+併發標記

      • 在Young GC時會進行GC Root的初始標記
      • 老年代佔用堆內存空間比例達到閾值時,進行併發標記(不會STW)
    • 混合收集,對新生代,倖存區和老年代都進行收集

      • 最終標記,會STW
      • 拷貝存活,會STW
      • 三種階段循環交替
        在這裏插入圖片描述
  • Full GC

    • SerialGC

      • 新生代內存不足發生的垃圾收集:minor GC
      • 老年代內存不足發生的垃圾收集:full GC
    • ParallelGC

      • 新生代內存不足發生的垃圾收集:minor GC
      • 老年代內存不足發生的垃圾收集:full GC
    • CMS

      • 新生代內存不足發生的垃圾收集:minor GC

      • 老年代內存不足

        • 併發收集成功:併發的垃圾收集
        • 併發收集失敗:串行的full GC
    • G1

      • 新生代內存不足發生的垃圾收集:minor GC

      • 老年代內存不足,達到閾值時進入併發標記和混合收集階段

        • 如果回收速度>新產生垃圾的速度 :併發垃圾收集
        • 如果回收速度<新產生垃圾的速度:串行的full GC

5. 四種引用

在這裏插入圖片描述

  • 強引用

    • 最常見的對象:通過new關鍵字創建,通過GC Root能找到的對象。
    • 當所有的GC Root都不通過【強引用】引用該對象時,對象才能被垃圾回收
  • 軟引用

    • 僅有【軟引用】引用該對象時,在垃圾回收後,內存仍不足時會再次發起垃圾回收,回收軟引用對象

    • 可以配合引用隊列來釋放軟引用自身

    • 創建一個軟引用:SoftReference ref = new SoftReference<>(new Object());

    • 軟引用被回收後,仍然還保留一個null,如將軟引用加入集合,回收後遍歷集合仍然還存在一個null

      • 解決:使用引用隊列,軟引用關聯的對象被回收時,軟引用自身會被加入到引用隊列中,通過queue.poll()取得對象進行刪除
      • 創建一個而引用隊列:ReferenceQueue queue = new ReferenceQueue<>();
      • 創建加入了引用隊列的軟引用:SoftReference ref = new SoftReference<>(new Object(),queue);
  • 弱引用

    • 僅有【弱引用】引用該對象時,在垃圾回收時,無論內存是否充足,都會回收弱引用對象
    • 可以配合引用隊列來釋放弱引用自身
    • 創建一個弱引用:WeakReference ref = new WeakReference<>(new Object());
    • 引用隊列使用同軟引用
  • 虛引用

    • 必須配合引用隊列使用,主要配合ByteBuffer使用,被引用對象回收時,會將【虛引用】入隊,由Reference Hanler線程調用虛引用相關方法釋放【直接內存】(unsafe類中方法)
  • 終結器引用

    • 無需手動編碼,但其內部配合引用隊列使用,在垃圾回收時,終結器引用隊列入隊(引用對象暫未回收),再由Finalizer線程通過終結器引用找到被引用對象並調用他的finalize方法,第二次gc時回收被引用對象

類加載

類加載器的分類

在這裏插入圖片描述

類加載過程

在這裏插入圖片描述

  • 加載

    • 通過ClassLoader加載Class文件字節碼,生成Class對象
  • 鏈接

    • 校驗:檢查加載的的Class的正確性和安全性

    • 準備:爲類變量分配存儲空間並設置類變量初始值

    • 解析:JVM將常量池內的符號引用轉換爲直接引用

  • 初始化

    • 執行類變量賦值和靜態代碼塊

LoadClass和forName的區別

  • Class.ForName得到的class是已經初始化完成的
  • ClassLoader.loadClass得到的class是還沒有鏈接的

雙親委派機制

在這裏插入圖片描述

  • 什麼是雙親委派機制
    • 當某個類加載器需要加載某個.class文件時,它首先把這個任務委託給他的上級類加載器,遞歸這個操作,如果上級的類加載器沒有加載,自己纔會去加載這個類。
  • 爲什麼要使用雙親委派機制
    • 防止重複加載同一個.class文件,通過委託去向上級問,加載過了就不用加載了。
    • 保證核心.class文件不會被串改,即使篡改也不會加載,即使加載也不會是同一個對象,因爲不同加載器加載同一個.class文件也不是同一個class對象,從而保證了class執行安全

自定義類加載器

  • 需求場景

    • 想要加載非classpath的隨意路徑的類文件
    • 通過接口來使用實現,希望解耦合
  • 步驟

    • 繼承Classloader父類
    • 遵循雙親委派機制,重寫findClass方法(不能重寫loadClass,重寫了就不符合雙親委派了)
    • 讀取類的字節碼
    • 調用父類的defineClass方法加載類
    • 使用者調用類加載的loadClass方法
  • 案例演示

創建自定義類加載器

public class MyClassLoader extends ClassLoader {
    private String path;
    private String classLoaderName;

    public MyClassLoader(String path, String classLoaderName) {
        this.path = path;
        this.classLoaderName = classLoaderName;
    }

    //用於尋找類文件
    @Override
    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }


    //用於加載類文件
    private byte[] loadClassData(String name) {
        name = path + name + ".class";

        try (InputStream in = new FileInputStream(new File(name));
             ByteArrayOutputStream out = new ByteArrayOutputStream();) {
            int i = 0;
            while ((i = in.read()) != -1) {
                out.write(i);
            }
            return out.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

調用自定義類加載器加載類

public class MyClassLoaderChecker {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyClassLoader m = new MyClassLoader("C:\\Users\\73787\\Desktop\\","myClassLoader");
        Class<?> c = m.loadClass("Robot");
        System.out.println(c.getClassLoader());
        c.newInstance();
    }
}

反射機制

反射的定義

JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意方法和屬性;這種動態獲取信息以及動態調用對象方法的功能稱爲java語言的反射機制。

反射的常用場景

第三方應用開發過程中,會需要某個類的某個成員變量、方法或是屬性是私有的或者只對系統應用開放,就可以通過Java的反射機制來獲取所需的私有成員或者方法

反射相關的類

在這裏插入圖片描述

Class類:

代表類的實體,在運行的Java應用程序中表示類和接口

  • 獲得類的方法

在這裏插入圖片描述

  • 獲得類中屬性的方法

在這裏插入圖片描述

  • 獲得類中方法的方法
    在這裏插入圖片描述
  • 獲取類中構造器的方法
    在這裏插入圖片描述
Filed類

Filed代表類的成員變量(屬性)

在這裏插入圖片描述

Method類

在這裏插入圖片描述

Constructor類

在這裏插入圖片描述

案例

定義一個Robot類

public class Robot {
    //私有屬性
    private String name;
    //公有方法
    public void sayHi(String hello){
        System.out.println(hello+" "+name);
    }
    //私有方法
    private String thorwHello(String tag){
        return "hello "+tag;
    }
}

編寫一個反射應用類,針對私有的屬性和方法必須設置setAccessible(true)才能進行訪問

public class ReflectSample {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //加載類
        Class<?> rc = Class.forName("leetcode.Robot");
        //獲取類實例
        Robot r = (Robot)rc.newInstance();
        //打印類名
        System.out.println(rc.getName());
        
        //加載一個私有方法
        Method getHello = rc.getDeclaredMethod("thorwHello",String.class);
        getHello.setAccessible(true);
        Object bob = getHello.invoke(r, "bob");
        System.out.println(bob);
       
         //加載一個公有方法
        Method sayHi = rc.getMethod("sayHi",String.class);
        Object welcome = sayHi.invoke(r,"welcome");
       
         //加載一個私有屬性
        Field name = rc.getDeclaredField("name");
        name.setAccessible(true);
        name.set(r,"tom");
        sayHi.invoke(r,"welcome");
    }
}

Java內存模型

什麼是Java內存模型(JMM)

  • 通俗來說,JMM是一套多線程讀寫共享數據時,對數據的可見性,有序性和原子性的規則

爲什麼會有Java內存模型

JVM實現不同會造成“翻譯”的效果不同,不同CPU平臺的機器指令有千差萬別,無法保證同一份代碼併發下的效果一致。所以需要一套統一的規範來約束JVM的翻譯過程,保證併發效果一致性

原子性

  • 什麼是原子性
    • 原子性指一系列的操作,要麼全部執行成功,要麼全部不執行,不會出現執行一半的情況,是不可分的。
  • 原子性怎麼實現
    • 使用synchronized或Lock加鎖實現,保證任一時刻只有一個線程訪問該代碼塊
    • 使用原子操作
  • Java中的原子操作有哪些
    • 除long和double之外的基本類型的賦值操作(64位值,當成兩次32位的進行操作)
    • 所有引用reference的賦值操作
    • java.concurrent.Atomic.*包中所有類的原子操作
  • 創建對象的過程是否是原子操作(常應用於雙重檢查+volatile創建單例場景)
    • 創建對象實際上有3個步驟,並不是原子性的
      • 創建一個空對象
      • 調用構造方法
      • 創建好的實例賦值給引用

可見性

  • 什麼是可見性問題
    • 可見性指的是當一個線程修改了某個共享變量的值,其他線程是否能夠馬上得知這個修改的值。
  • 爲什麼會有可見性問題、
    • 對於單線程程序來說,可見性是不存在的,因爲我們在任何一個操作中修改了某個變量的值,後續的操作中都能讀取這個變量值,並且是修改過的新值。
    • 對於多線程程序而言。由於線程對共享變量的操作都是線程拷貝到各自的工作內存進行操作後才寫回到主內存中的,這就可能存在一個線程A修改了共享變量x的值,還未寫回主內存時,另外一個線程B又對主內存中同一個共享變量x進行操作,但此時A線程工作內存中共享變量x對線程B來說並不可見,這種工作內存與主內存同步延遲現象就造成了可見性問題
      在這裏插入圖片描述
  • 如何解決可見性問題
    • 解決方法1:加volatile關鍵字保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值立即被其他的線程看到,即修改的值立即更新到主存中,當其他線程需要讀取時,它會去內存中讀取新值
    • 解決方法2:使用synchronized和Lock保證可見性。因爲它們可以保證任一時刻只有一個線程能訪問共享資源,並在其釋放鎖之前將修改的變量刷新到內存中
  • 案例
/**
* 〈可見性問題分析〉
*
* @author Chkl
* @create 2020/3/4
* @since 1.0.0
*/
public class FieldVisibility {
    int a = 1;
    int b = 2;

    private void change() {
        a = 3;
        b = a;
    }
    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }
    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();




            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }
}

循環創建兩類線程,一個線程用於做值的交換,一個線程用於打印值

比較直觀的三種結果

  • 打印線程先執行:b = 2, a = 1
  • 交換線程先執行:b = 3, a = 3
  • 交換線程執行到一半就切出去打印了,只執行了a=3賦值操作:b = 2 , a =3

實際上除了很容易想到的三種情況外還有一種特殊情況:b = 3 , a = 1

  • 這種情況就是可見性問題
  • a的值在線程1(執行交換線程)的本地緩存中進行了更新,但是並沒有同步到共享緩存,而b的值成功的更新到了共享緩存,導致線程2(執行打印線程)從共享緩存中獲取到的數據並不是實時的最新數據
    -在這裏插入圖片描述

有序性(重排序)

  • 什麼是重排序
    • 在線程內部的兩行代碼的實際執行順序和代碼在Java文件中的邏輯順序不一致,代碼指令並不是嚴格按照代碼語句順序執行的,他們的順序被改變了,這就是重排序。
  • 重排序的意義
    • JVM能根據處理器特性(CPU多級緩存系統、多核處理器等)適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的發揮機器性能。
    • 案例
      計算:
      a = 3;
      b = 2;
      a = a + 1;
      重排序優化前的instructions
      
      load a
      set to 3
      store 3
      
      load b
      set to 2
      store b
      
      load a
      set to 4
      store a
      
      經過重排序處理後
      
      load a
      set to 3
      set to 4
      store a
      
      
      load b
      set to 2
      store b
      
      上述少了兩個指令,優化了性能
      
  • 重排序的3種情況
    • 編譯器優化( JVM,JIT編輯器等): 編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序
    • 指令級並行的重排序:現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
    • 內存系統的重排序: 由於處理器使用緩存和讀寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。
      在這裏插入圖片描述

volatile

  • 什麼是volatile

    • volatile是一種同步機制,比synchronized或者Lock相關類更輕量級,因爲使用volacile並不會發生上下文切換等開銷很大的行爲
    • volatile是無鎖的,並且只能修飾單個屬性
  • 什麼時候適合用vilatile

    • 一個共享變量始終只被各個線程賦值,沒有其他操作
    • 作爲刷新的觸發器,引用刷新之後使修改內容對其他線程可見(如CopyOnRightArrayList底層動態數組通過volatile修飾,保證修改完成後通過引用變化觸發volatile刷新,使其他線程可見)
  • volatile的作用

    • 可見性保障:修改一個volatile修飾變量之後,會立即將修改同步到主內存,使用一個volatile修飾的變量之前,會立即從主內存中刷新數據。保證讀取的數據都是最新的,之前的修改都是可見的。
    • 有序性保障(禁止指令重排序優化):有volatile修飾的變量,賦值後多了一個“內存屏障“( 指令重排序時不能把後面的指令重排序到內存屏障之前的位置)
  • volatile的性能

    • volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因爲它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。

happens-before規則

什麼是happens-before規則:前一個操作的結果可以被後續的操作獲取。

  • 程序的順序性規則:在一個線程內一段代碼的執行結果是有序的。雖然還會指令重排,但是隨便它怎麼排,結果是按照我們代碼的順序生成的不會變!
  • volatile規則: 就是如果一個線程先去寫一個volatile變量,然後一個線程去讀這個變量,那麼這個寫操作的結果一定對讀的這個線程可見。
  • 傳遞性規則:happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那麼A happens-before C。
  • 管程鎖定規則:無論是在單線程環境還是多線程環境,對於同一個鎖來說,一個線程對這個鎖解鎖之後,另一個線程獲取了這個鎖都能看到前一個線程的操作結果!(管程是一種通用的同步原語,synchronized就是管程的實現)
  • 線程啓動規則:在主線程A執行過程中,啓動子線程B,那麼線程A在啓動子線程B之前對共享變量的修改結果對線程B可見。
  • 線程終止規則: 在主線程A執行過程中,子線程B終止,那麼線程B在終止之前對共享變量的修改結果在線程A中可見。
  • 線程中斷規則: 對線程interrupt()方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,可以通過Thread.interrupted()檢測到是否發生中斷。
  • 對象終結規則:一個對象的初始化的完成,也就是構造函數執行的結束一定 happens-before它的finalize()方法。

如果有用,點個贊再走吧

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