深入理解Java虛擬機器(二)

猥瑣發育_別浪發表於2018-12-06

一、編譯和程式碼優化

1、編譯器優化-泛型:

1、泛型出現之前存在的問題:
所有物件的型別都繼承自Object,虛擬機器只有到執行時才能知道這個Object具體是什麼型別,在編譯期是無法檢查這個Object是否強制轉型成功,會將ClassCaseException的風險轉移到程式執行期。

2、泛型的作用:
通過泛型,編譯器可以在編譯階段發現型別不一致的問題

3、泛型擦除:
將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));
複製程式碼

2、執行期優化-程式碼優化

1、公共子表示式消除 在程式基本塊中,如果一個表示式E已經被計算過了,下次再次使用的時候,如果表示式的變數值都沒發生改變,就可以直接拿表示式的結果來代替E。

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

二、方法呼叫

1、解析

類載入解析節點,將一部分符號引用轉化為直接引用。前提是程式執行前有可確定的呼叫版本,並且在執行期不可變。這些編譯期可知、執行期不可變的方法呼叫就是解析。

2、靜態分派和動態分派

1、靜態分派:

根據靜態型別來定位方法的分派叫做靜態分派,發生在編譯階段。

//父類
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);
  }
}
複製程式碼

返回結果:

深入理解Java虛擬機器(二)
Parent為變數的靜態型別,Son為實際型別。其中靜態型別是在編譯期可知的,而實際型別是在執行期確定下來的,編譯器在編譯階段不知道某個物件的實際型別是什麼,所以是用靜態型別作為判定依據來選擇使用哪個過載版本的,所以選擇了say(Parent)作為呼叫目標。

2、動態分派

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();
複製程式碼

結果:

深入理解Java虛擬機器(二)
虛擬機器根據實際型別的不同來分派方法

基本步驟:

  • 找到棧頂第一個元素所指向的物件實際型別
  • 如果找到對應方法,進行訪問許可權驗證,通過則直接引用,不通過則丟擲異常。
  • 否則,按照繼承關係從下向上對其各個父類進行方法的搜尋和驗證過程。
  • 如果沒方法,則拋AbstractMethodError異常。

三、併發

1、處理器、快取、記憶體的關係

深入理解Java虛擬機器(二)

2、主記憶體、工作記憶體的關係

深入理解Java虛擬機器(二)

  • 執行緒的工作記憶體中儲存了被該執行緒使用的變數的主記憶體的拷貝副本
  • 執行緒對變數的讀取、賦值等操作是在工作記憶體中進行
  • 不同執行緒之間無法直接訪問對方工作記憶體的變數,執行緒間變數值傳遞通過主記憶體來完成

3、記憶體間的互動操作

將變數從主記憶體拷貝到工作記憶體中,將工作記憶體同步到主記憶體中。定義了8中操作,每步操作都是原子的、不可再分。

  • lock(鎖定):作用於主記憶體變數,將一個變數標識為一條執行緒獨佔的狀態
  • unlock(解鎖):作用於主記憶體的變數,把一個處於鎖定狀態的變數釋放出來,才可被其他執行緒鎖定。
  • read(讀取):作用於主記憶體變數,把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中。
  • load(載入):作用於工作記憶體的變數,把read操作從主記憶體得到的變數放到工作記憶體的變數副本中。
  • use(使用):作用於工作記憶體變數,當遇到需要使用變數的值得位元組碼指令時,會將工作記憶體的變數傳給執行引擎。
  • assign(賦值):作用於工作記憶體的變數,當遇到給變數賦值的位元組碼指令時,會把一個從執行引擎接收到的值賦給工作記憶體的變數。
  • store(儲存):作用於工作記憶體的變數,把工作記憶體的變數值傳遞給主記憶體中。
  • write(寫入):作用於主記憶體的變數,把從工作記憶體中得到的變數值放入主記憶體的變數中。

注 :

  • read與load之間、store和write之前可以插入其他指令,會導致多執行緒操作的同步問題。
  • 一個變數在同時刻只允許一個執行緒對其進行lock操作。

4、volatile關鍵字解析

1、可見性:

  • 可見性:一條執行緒修改變數的值,新值對於其他執行緒是立刻得知的。
  • 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只能保證最後變數取到操作棧頂時該變數的同步性,但是在這之前其他執行緒是可以修改該變數的值。

2、volatile適用的場景:

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

3、禁止指令重排序優化 普通變數只能保證執行過程所有依賴賦值結果的地方都能得到正確的結果,不能保證變數賦值的順序與程式碼中執行順序一致,

實現方式:在多執行緒訪問同一記憶體時,相當於通過一個記憶體屏障,保證不能把後面的指令重排序到記憶體屏障之前的位置。

5、synchronized基本原理:

synchronized關鍵字經過編譯後,會在同步塊前後行程monitorenter和monitorexit兩個位元組碼指令。在執行monitorenter指令時,如果物件沒被鎖定,或者當前執行緒擁有這個物件鎖,把鎖的計算器加1,執行monitorexit時,鎖的計數器減1,當計數器為0時,鎖會被釋放。如果獲取物件鎖失敗,當前執行緒會阻塞等待,直到物件鎖被釋放。

6、同步

同步(一)

同步(一)

相關文章