深刻理解Java虛擬機

一、泛型出現以前存在的問題:緩存

全部對象的類型都繼承自Object,虛擬機只有到運行時才能知道這個Object具體是什麼類型,在編譯期是沒法檢查這個Object是否強制轉型成功,會將ClassCaseException的風險轉移到程序運行期。多線程

二、泛型的做用:架構

經過泛型,編譯器能夠在編譯階段發現類型不一致的問題併發

三、泛型擦除:ide

將Java代碼編譯成Class文件,經過反編譯發現泛型都不見了,被替換爲原生類型,並插入強制轉型的代碼。學習

//泛型擦除前
List<String> list = new ArrayList<>();
list.add("hello");
System.out.println(list.get(0));
//泛型擦除後
List list1 = new ArrayList();
list1.add("hello");
System.out.println((String) list1.get(0));
複製代碼

二、運行期優化-代碼優化

一、公共子表達式消除 在程序基本塊中,若是一個表達式E已經被計算過了,下次再次使用的時候,若是表達式的變量值都沒發生改變,就能夠直接拿表達式的結果來代替E。優化

int x = 1;
int y = 2;
int z = x + y;
int w1 = x + y +2;
//編譯器對公共子表達式(x+y)進行消除
int w2 = z + 2;
複製代碼

2、方法調用

一、解析

類加載解析節點,將一部分符號引用轉化爲直接引用。前提是程序運行前有可肯定的調用版本,而且在運行期不可變。這些編譯期可知、運行期不可變的方法調用就是解析。線程

二、靜態分派和動態分派

一、靜態分派:3d

根據靜態類型來定位方法的分派叫作靜態分派,發生在編譯階段。對象

//父類
public class Parent {
 
}
//子類
public class Son extends Parent {

}
//調用
public class MyTest {

  public void say(Parent parent) {
    System.out.println("parent say");
  }

  public void say(Son son) {
    System.out.println("son say");
  }

  public static void main(String[] args) {
    MyTest myTest = new MyTest();
    //實際類型爲Parent
    Parent parent = new Parent();
    //實際類型爲Son
    Parent son = new Son();
    myTest.say(parent);
    myTest.say(son);
  }
}
複製代碼

返回結果:

Parent爲變量的靜態類型,Son爲實際類型。其中靜態類型是在編譯期可知的,而實際類型是在運行期肯定下來的,編譯器在編譯階段不知道某個對象的實際類型是什麼,因此是用靜態類型做爲斷定依據來選擇使用哪一個重載版本的,因此選擇了say(Parent)做爲調用目標。

二、動態分派

public class Parent {

  public void say() {
    System.out.println("parent....");
  }
}

public class Son extends Parent {

  public void say() {
    System.out.println("son....");
  }
}
//調用
Parent parent = new Parent();
Parent son = new Son();
parent.say();
son.say();
複製代碼

結果:

虛擬機根據實際類型的不一樣來分派方法

基本步驟:

  • 找到棧頂第一個元素所指向的對象實際類型
  • 若是找到對應方法,進行訪問權限驗證,經過則直接引用,不經過則拋出異常。
  • 不然,按照繼承關係從下向上對其各個父類進行方法的搜索和驗證過程。
  • 若是沒方法,則拋AbstractMethodError異常。

3、併發

一、處理器、緩存、內存的關係

二、主內存、工做內存的關係

  • 線程的工做內存中保存了被該線程使用的變量的主內存的拷貝副本
  • 線程對變量的讀取、賦值等操做是在工做內存中進行
  • 不一樣線程之間沒法直接訪問對方工做內存的變量,線程間變量值傳遞經過主內存來完成

三、內存間的交互操做

將變量從主內存拷貝到工做內存中,將工做內存同步到主內存中。定義了8中操做,每步操做都是原子的、不可再分。

  • lock(鎖定):做用於主內存變量,將一個變量標識爲一條線程獨佔的狀態
  • unlock(解鎖):做用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,纔可被其餘線程鎖定。
  • read(讀取):做用於主內存變量,把一個變量的值從主內存傳輸到線程的工做內存中。
  • load(載入):做用於工做內存的變量,把read操做從主內存獲得的變量放到工做內存的變量副本中。
  • 在此我向你們推薦一個架構學習交流圈:830478757  幫助突破瓶頸 提高思惟能力
  • use(使用):做用於工做內存變量,當遇到須要使用變量的值得字節碼指令時,會將工做內存的變量傳給執行引擎。
  • assign(賦值):做用於工做內存的變量,當遇到給變量賦值的字節碼指令時,會把一個從執行引擎接收到的值賦給工做內存的變量。
  • store(存儲):做用於工做內存的變量,把工做內存的變量值傳遞給主內存中。
  • write(寫入):做用於主內存的變量,把從工做內存中獲得的變量值放入主內存的變量中。

注 :

  • read與load之間、store和write以前能夠插入其餘指令,會致使多線程操做的同步問題。
  • 一個變量在同時刻只容許一個線程對其進行lock操做。

四、volatile關鍵字解析

一、可見性:

  • 可見性:一條線程修改變量的值,新值對於其餘線程是馬上得知的。
  • synchronized和final也能實現可見性。
  • 普通變量:若是線程A修改了普通變量的值,須要向主內存進行回寫。另外一條線程B在A回寫完成後再從主內存進行讀取操做,新變量值才能對線程B可見。
  • 注意:不是全部對volatile變量的寫操做都會當即反應到其餘線程中。
private volatile static int x;
public static void main(String[] args) {
  for (int i = 0; i < 20; i++) {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        for (int i1 = 0; i1 < 1000; i1++) {
          x++;
        }
      }
    });
    thread.start();
  }
  System.out.println("x="+x);
}
複製代碼

最終的結果不是20000,說明volatile修飾的變量也沒實現正確併發的目的。

緣由:

x++ 是由多條字節碼指令構成的,包括取值,+1,賦值操做,volatile只能保證最後變量取到操做棧頂時該變量的同步性,可是在這以前其餘線程是能夠修改該變量的值。

二、volatile適用的場景:

  • 運算結果不依賴變量的當前值(例如 x = x+1 不可用)
  • 變量不須要與其餘狀態變量共同參與不變約束 (x = 1+y 不可用)

三、禁止指令重排序優化 普通變量只能保證執行過程全部依賴賦值結果的地方都能獲得正確的結果,不能保證變量賦值的順序與代碼中執行順序一致,

實現方式:在多線程訪問同一內存時,至關於經過一個內存屏障,保證不能把後面的指令重排序到內存屏障以前的位置。

相關文章
相關標籤/搜索