1. 程式人生 > 實用技巧 >全網最全!這份深入講解jdk和jvm原理的筆記,重新整理了我對JVM的認知

全網最全!這份深入講解jdk和jvm原理的筆記,重新整理了我對JVM的認知

前言

前兩天和朋友探討技術的時候有聊到JVM和JDK這一塊,聊到這裡兩個人就像高山流水遇知音那是根本停不下來,事後我想著趁現在印象還比較深刻就把這些東西整理起來分享給大家來幫助更多的人吧。話不多說,滿滿的乾貨都整理在下面了!

JVM探究

jvm的位置

jvm的體系結構

堆裡面有垃圾,需要被GC回收

棧裡面是沒有垃圾的,用完就彈出去了,棧裡面有垃圾,程式就崩了,執行不完main方法。

Java棧,本地方法棧,程式計數器裡面是不可能存在垃圾的。也就不會有垃圾回收。

所謂的jvm調優就是在堆裡面調優了,jvm調優99%都是在方法區和堆裡面進行調優的。

類載入器

public class Car {
    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();
        System.out.println(car1.hashCode());
        System.out.println(car2.hashCode());
        System.out.println(car3.hashCode());
        Class<? extends Car> aClass1 = car1.getClass();
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();
        System.out.println(aClass1.hashCode());
        System.out.println(aClass2.hashCode());
        System.out.println(aClass3.hashCode());

    }
}

  

作用:載入class檔案 - 類似new Student();

類是一個模板,是抽象的,而new出來的物件是具體的,是對這個抽象的類的例項化

1.虛擬機器自帶的載入器

2.啟動類(根)載入器

3.擴充套件載入器

4.應用程式(系統類)載入器

ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);//AppClassLoader 應用程式載入器

System.out.println(classLoader.getParent());//ExtClassLoader 擴充套件類載入器

System.out.println(classLoader.getParent().getParent());//null 1.不存在  2.Java程式獲取不到

  

1.類載入器收到類載入的請求

2.將這個請求向上委託給父類載入器去完成,一直向上委託,直到根載入器

3.啟動類載入器會檢查是否能夠載入當前這個類,能載入就結束,使用當前載入器,否則,丟擲異常,通知子類載入器進行載入。

4.重複步驟3

若都找不到就會報 Class Not Found

null:Java呼叫不到,可能程式語言是C寫的,所以調不到

Java =C+±- 去掉C裡面比較繁瑣的東西 指標,記憶體管理(JVM幫你做了)

雙親委派機制

雙親委派機制:安全

APP–>EXC–BOOTStrap(根目錄,最終執行)

當某個類載入器需要載入某個.class檔案時,它首先把這個任務委託給他的上級類載入器,遞迴這個操作,如果上級的類載入器沒有載入,自己才會去載入這個類。

在src下建立Java.lang包,建立一個String類

package java.lang;

public class String {
    public String toString(){
        return "hello";
    }

    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.getClass().getClassLoader());
        s.toString();
    }
}

  

執行結果

它會去最終的BOOTStrap裡面的String類裡面去執行,找到執行類的位置,發現裡面沒有要執行的mian方法,所以會報這個錯。

在src下建立類Student

public class Student {
    public String toString(){
        return "HELLO";
    }

    public static void main(String[] args) {
        Student student = new Student();
        System.out.println(student.getClass().getClassLoader());
        System.out.println(student.toString());
    }
    
}

  

執行結果

如上圖可見最終是在APP裡面執行的,成功輸出HELLO語句

雙親委派機制的作用

1、防止重複載入同一個.class。通過委託去向上面問一問,載入過了,就不用再載入一遍。保證資料安全。
2、保證核心.class不能被篡改。通過委託方式,不會去篡改核心.class,即使篡改也不會去載入,即使載入也不會是同一個.class物件了。不同的載入器載入同一個.class也不是同一個Class物件。這樣保證了Class執行安全。

沙箱安全機制

​ Java安全模型的核心就是Java沙箱(sandbox),什麼是沙箱?沙箱是一個限制程式執行的環境,沙箱機制就是將Java程式碼限定在虛擬機器(JVM)特定的執行範圍中,並且嚴格限制程式碼對本地系統資源訪問,通過這樣的措施來保證對程式碼的有效隔離,防止對本地系統造成破壞,沙箱主要限制系統資源訪問,那系統資源包括什麼?CPU,記憶體,檔案系統,網格,不同級別的沙箱對這些資源訪問的限制也是可以不一樣。

​ 所以的Java程式執行都可以指定沙箱,可以定製安全策略。

​ 在Java中將執行程式分為原生代碼呵遠端程式碼兩種,原生代碼預設視為可信任的,而遠端程式碼則被看作是不受信任的,對於授權的原生代碼,可以訪問一切本地資源,而對於非授信的遠端程式碼在早期的Java實現中,安全依賴於沙箱機制,如下圖jdk1.0安全模型

但如此嚴格的安全機制也給程式的功能擴充套件帶來障礙,比如當用戶希望遠端程式碼訪問本地系統的檔案時候,就無法實現,因此在後續的Java1.1版本中,針對安全機制做了改進,增加了安全策略,允許使用者指定程式碼本地資源的訪問許可權,如下圖所示JDK1.1安全模型

​ 在Java1.2版本中,再次改進了安全機制,增加了程式碼簽名,不論原生代碼或是遠端程式碼,都會按照使用者的安全策略設定,由類載入器載入到虛擬機器中許可權不同的執行空間,來實現差異化的程式碼執行許可權控制,如下圖所JDK1.2安全模型

當前最新的安全機制實現,則引入域(Domain)的概念,虛擬機器會把所有程式碼載入到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行互動,而各個應用域部分則通過系統域的部分代理來各種需求的資源進行訪問,虛擬機器中不同的受保護域,對應不一樣的許可權,存在不同域中的類檔案就具有了當前域的全部許可權,如下圖所示,最新的安全模型

組成沙箱的基本元件:

位元組碼校驗器(bytecode verifier):確保Java類檔案遵循Java語言規範,這樣可以幫助Java程式實現記憶體保護,但並不是所有的類檔案都會經過位元組碼校驗,比如核心類。

類載入器(class loader):其中類載入器在3個方面對Java沙箱起作用

​ 它防止惡意程式碼去幹涉善意的程式碼;//雙親委派機制

​ 它守護了被信任的類庫邊界;

​ 它將程式碼歸入保護域,確定了程式碼可以進行哪些操作;

​ 虛擬機器為不同的類載入器載入的類提供不同的名稱空間,名稱空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個名稱空間由Java虛擬機器為每一個類載入器維護的,它們互相甚至不可見。

​ 類載入器採用的機制是雙親委派模式。

​ 虛擬機器為不同的類載入開始載入,外層惡意同名類得不到載入從而無法使用;

​ 由於嚴格通過包來區分了訪問域:外層惡意的類通過內建程式碼也無法獲得許可權訪問到內建類,破壞程式碼就自然無法生效。

​存取控制器(access controller):存取控制器可以控制核心API對作業系統的存取許可權,而這個控制的策略設定,可以由使用者指定。

​ 安全管理器 (security manager):是核心API和作業系統之間的主要介面,實現許可權控制,比存取控制器優先順序高。

​ 安全軟體包(security package):java.security下的類和擴充套件包下的類,允許使用者為自己的應用增加新的安全特性,包括:

​ 安全提供者

​ 資訊摘要

​ 數字簽名 kettools https(需要證書)

​ 加密

​ 鑑別

Native

凡是帶了native關鍵字,說明Java的的作用範圍達不到了,會去呼叫底層C語言的庫!

會進入本地方法棧

呼叫本地方法本地介面 JNI

JNI的作用:擴充套件Java的使用,融合不同的程式語言為Java所用,最初:C,C++

Java誕生的時候C ,C++比較火,Java想要立足,必須要有呼叫C,C++的程式。

他在記憶體區域專門開闢了一塊標記區域 :Native Method Stack,登記native方法

在最終執行的時候,載入本地方法庫中的方法通過JNI

Java程式驅動印表機,管理系統,掌握即可,在企業級應用中較為少見。

​ 目前該方法使用使用的越來越少了,除非 是與硬體有關的應用,比如通過Java程式驅動印表機或者Java系統管理生產裝置,在企業級應用中已經比較少見,因為現在的異構領域間通訊很發達,比如可以使用Socjet通訊,也可以使Web Service等等 ,不多做介紹!

PC暫存器

​ 程式計數器:Program Counter Register

​ 每個執行緒都有一個程式計數器,要執行緒私有的,就是一個指標,指向方法區中的方法位元組碼(用來儲存指向像一條指令的地址,也即將要執行的指令程式碼),在執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不計。

方法區

Method Area 方法區

​ 方法區是被所有執行緒共享,所有欄位和方法位元組碼,以及一些特殊方法,如建構函式,介面程式碼也在此定義,簡單說,所有定義的方法的資訊都儲存在該區域,在此區域屬於共享區間

​ 靜態變數,常量,類資訊(構造方法,介面定義),執行時的常量池存在方法區中,但是例項變數存在堆記憶體中,與方法區無關。

static,final,Class,常量池

棧是一種資料結構

​ 程式 = 資料結構 + 演算法 :持續學習~

​ 程式 = 框架 + 業務邏輯 :吃飯~

棧:先進後出,後進先出

佇列:先進先出(FIFO:First input First Output)

方法執行完成以後,就會被棧彈出去

兩個方法互相調,就會導致棧溢位

public class Inn {
    public static void main(String[] args) {
        new Inn().test();
    }
    public void test(){
        a();
    }
    public void a(){
        test();
    }
    //a調test,test調a
}

  

執行結果

棧:棧記憶體,主管程式的執行,生命週期和執行緒同步,也就是執行緒如果都結束了,棧也就變成空的了;

執行緒結束,佔記憶體也就釋放了,對於棧來說,不存在垃圾回收問題,一旦執行緒結束,棧就沒了。

棧:8大基本型別+物件引用+例項的方法

棧執行原理:棧幀

函式呼叫過程中,肯定需要空間的開闢,而呼叫這個函式時為該函式開闢的空間就叫做該函式的棧幀

程式正在執行的方法一定在棧的頂部,執行完就會彈出去

棧1在執行完成之後就會彈出去,然後棧2在去執行,棧2執行完,程式也就結束了

棧滿了:StackOverflowError

棧+堆+方法區 :互動

如下圖:物件在記憶體中例項化的過程

三種jvm

Sun 公司 HotSpot Java HotSpot™ 64-Bit Server VM (build 25.77-b03, mixed mode)
BEA :JRockit
IBM :J9 VM
我們用的是hotspot


Heap(堆):一個jvm只有一個,堆記憶體的大小是可以調節的。

類載入器讀取了類檔案後,一般會把什麼東西放到堆中呢?類,方法,常量,儲存我們所有引用型別的真實物件。

堆記憶體中還要細分為三個區域:

新生區(伊甸園區)

養老區

永久區

GC垃圾回收,主要是在伊甸園區和養老區,

假設記憶體滿了,會報OOM,堆記憶體不夠,堆溢位

在jdk8以後,永久儲存區改了個名字叫(元空間)

新生區

它是一個類:誕生和成長的地方,甚至死亡;

新生區分為 伊甸園區和倖存者區

伊甸園區;所有的物件都是在伊甸園區裡new出來的

倖存區:(0,1)

當伊甸園區滿了以後,會觸發輕GC,對伊甸園區進行垃圾回收,當某個物件通過GC倖存下來以後,就會進入到倖存者區,依次不斷的迴圈,當倖存0區和1區也滿了的時候,在經歷過多次GC以後,活下來的物件,也就是被重新引用的物件就會進入到老年區。而當老年區也滿了的時候,就會觸發重GC,重CG除了會去清理老年區的,還會伊甸園區和倖存0區1區的所有垃圾全清理掉。而在重GC清除下,活下來的就會進入到養老區。當重GC清理完畢以後,新生區和養老區還是都滿了,這個時候就會報堆溢位的報錯。

真理:經過研究,99%物件都是臨時物件。

老年區

新生區裡面GC不掉的物件就會去到老年區

永久區

這個區域常駐記憶體的,用來存放JDK自身攜帶的Class物件,Interface元資料,儲存的是Java執行時的一些環境或類資訊,這個區域不存在垃圾回收!關閉VM虛擬機器就會釋放這個區域的記憶體。

一個啟動類載入了大量的第三方jar包,Tomcat部署了太多的應用,大量動態生成的反射類,不斷的被載入,直到記憶體滿,就會出現OOM;

jdk1.6之前:永久代,常量池是在方法區之中

jdk1.7:永久代,但是慢慢的退化,去永久代,常量池在堆中

jdk1.8之後:無永久代,常量池在元空間。

邏輯上存在,物理上不存在

堆記憶體調優

堆記憶體滿了,該如何處理?

public static void main(String[] args) {
    long max = Runtime.getRuntime().maxMemory();
    long total = Runtime.getRuntime().totalMemory();
    System.out.println("max="+max+"位元組\t"+(max/(double)1024/1024)+"MB");
    System.out.println("total="+max+"位元組\t"+(total/(double)1024/1024)+"MB");
}

  

​ 先嚐試把堆記憶體空間擴大,假如還是用原來的程式碼跑,繼續包堆溢位的錯,我們就該去考慮考慮自己程式碼那塊有問題,可能是有垃圾程式碼或者是死迴圈程式碼,它在不斷的佔用記憶體空間

​ 分析記憶體,看一下那個地方出現了問題(專業工具)

​ 調優程式碼-Xms1024m -Xmx1024m -XX:+PrintGCDetails

Xms後面填計算機給jvm分配的記憶體,Xmx後面填Jvm初始的記憶體值

在一個專案中,突然出現了OOM故障,那該如何排除研究為什麼出錯~

能夠看到程式碼第幾行出錯;
Dubug:一行行分析程式碼!
MAT,Jprofiler作用:

分析Dump記憶體檔案,快速定位記憶體洩漏;
獲得堆中的資料
獲得大的物件~
。。。。

GC

jvm在進行垃圾回收時,並不是對這三個區域統一回收,大部分時候,回收但是新生代

新生代

倖存區(from,to)

老年區

GC兩種類:輕GC(普通的GC),重GC(全域性GC)

GC常用演算法

標記清除法

​ 最基礎的收集演算法是“標記-清除”(Mark-Sweep)演算法,如它的名字一樣,演算法分為“標記”和“清除”兩個階段:

​ 首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件,之所以說它是最基礎的收集演算法,是因為後續的收集演算法都是基於這種思路並對其缺點進行改進而得到的。

​ 它的主要缺點有兩個:一個是效率問題,標記和清除過程的效率都不高;另外一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致,當程式在以後的執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。標記-清除演算法的執行過程(需要較大記憶體時卻不夠了就要回收一次)

複製演算法
為了解決效率問題,一種稱為“複製”(Copying)的收集演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對其中的一塊進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為原來的一半,未免太高了一點。

標記-整理演算法

複製收集演算法在物件存活率較高時就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。

分代收集演算法(並不是一種新的思想,只是將java堆分成新生代和老年代,根據各自特點採用不同演算法)
當前商業虛擬機器的垃圾收集都採用“分代收集”(Generational Collection)演算法,這種演算法並沒有什麼新的思想,只是根據物件的存活週期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”演算法來進行回收。
新生代–複製演算法。老年代–標記-整理演算法。

位元組碼引擎

1.概述
​ Java虛擬機器位元組碼執行引擎是jvm最核心的組成部分之一,所有的 Java 虛擬機器的執行引擎都是一致的:輸入的是位元組碼檔案,處理過程是位元組碼解析的等效過程,輸出的是執行結果,下面將主要從概念模型的角度來講解虛擬機器的方法呼叫和位元組碼執行。

2.執行引擎的解釋和作用

​ 類載入器將位元組碼載入記憶體之後,執行引擎以Java 位元組碼指令為單元,讀取Java位元組碼。問題是,現在的java位元組碼機器是讀不懂的,因此還必須想辦法將位元組碼轉化成平臺相關的機器碼(也就是系統能識別的0和1)。這個過程可以由直譯器來執行,也可以有即時編譯器(JIT Compiler)來完成

​ 具體步驟如下圖

執行引擎內部包括如下

語法糖

1.概述
​ 語法糖是一種用來方便程式設計師程式碼開發的手段,簡化程式開發,但是不會提供實質性的功能改造,但可以提高開發效率或者語法的嚴謹性或者減少編碼出錯的機會。
​ 總而言之,語法糖可以看作是編譯器實現的一種小把戲。

2.泛型和型別擦除

​ 泛型的本質是引數化型別,也就是操作的資料型別本身也是一個引數。這種引數型別可以用在類,介面,方法中,分別叫泛型類,泛型介面,泛型方法。

​ 但是java的泛型是一個語法糖,並非是真實泛型,只在原始碼中存在,List和List 在編譯之後,就是List 並在相應的地方加上了型別轉換程式碼。這種實現方式叫型別擦除,也叫偽泛型。

但是,擦除法所謂的擦除,僅僅是對方法的code屬性中的位元組碼進行擦除,實際上元資料中還是保留了泛型資訊,這也是我們能通過反射手段獲取引數化型別的根本依據。

泛型:

public class b {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("hello","a");
        System.out.println(map.get("hello"));
    }
}

  

實際上:

public class b {
    public b(){
        
    }
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("hello","a");
        System.out.println(map.get("hello"));
    }
}

  

3…自動裝箱和遍歷迴圈

自動裝箱和遍歷迴圈

public class b {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4);
        for (Integer integer:
             list) {
            System.out.println(integer);
        }
    }
}

  

實際上

public class b {
    public static void main(String[] args) {
        List<Integer> list =                   Arrays.asList(Integer.valueOf(1),Integer.valueOf(2),Integer.valueOf(3),Integer.valueOf(4));
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = (Integer) iterator.next();
            System.out.println(next);
        }

    }

  

自動裝箱用了Integer.valueOf
for迴圈用了迭代器

4.條件編譯

​ —般情況下,程式中的每一行程式碼都要參加編譯。但有時候出於對程式程式碼優化的考慮,希望只對其中一部分內容進行編譯,此時就需要在程式中加上條件,讓編譯器只對滿足條件的程式碼進行編譯,將不滿足條件的程式碼捨棄,這就是條件編譯。

反編譯之前

public static void main(String[] args) {
    if(true){
        System.out.println("hello");
    }else{
        System.out.println("beybey");
    }
}

  

反編譯之後

public static void main(String[] args) {
        System.out.println("hello");
}

  

首先,我們發現,在反編譯後的程式碼中沒有System.out.println(“beybey”);,這其實就是條件編譯。

當if(tue)為true的時候,編譯器就沒有對else的程式碼進行編譯。

所以,Java語法的條件編譯,是通過判斷條件為常量的if語句實現的。根據if判斷條件的真假,編譯器直接把分支為false的程式碼塊消除。通過該方式實現的條件編譯,必須在方法體內實現,而無法在正整個Java類的結構或者類的屬性上進行條件編譯。

執行期優化

​ Java 語言的 “編譯期” 其實是一段 “不確定” 的操作過程,因為它可能是指一個前端編譯器把 *.java 檔案轉變成 *.class 檔案的過程;也可能是指虛擬機器的後端執行期編譯器(JIT 編譯器,Just In Time Compiler)把位元組碼轉變成機器碼的過程;還可能是指使用靜態提前編譯器(AOT 編譯器,Ahead Of Time Compiler)直接把 *.java 檔案編譯成本地機器程式碼的過程

1.直譯器與編譯器

什麼是直譯器?

​ 大概意思:

​ 在電腦科學中,直譯器是一種計算機程式,它直接執行由程式語言或指令碼語言編寫的程式碼,並不會把原始碼預編譯成機器碼。一個直譯器,通常會用以下的姿勢來執行程式程式碼:

​ 分析原始碼,並且直接執行。
​ 把原始碼翻譯成相對更加高效率的中間碼,然後立即執行它。
​ 執行由直譯器內部的編譯器預編譯後儲存的程式碼
​ 可以把直譯器看成一個黑盒子,我們輸入原始碼,它就會實時返回結果。
​ 不同型別的直譯器,黑盒子裡面的構造不一樣,有些還會整合編譯器,快取編譯結果,用來提高執行效率(例如 Chrome V8 也是這麼做的)。
​ 直譯器通常是工作在「執行時」,並且對於我們輸入的原始碼,是一行一行的解釋然後執行,然後返回結果。

​ 什麼是編譯器?

​ 原始檔經過編譯器編譯後才可生成二進位制檔案,編譯過程包括預處理、編譯、彙編和連結,日常交流中常用“編譯”稱呼此四個過程

2.編譯物件與觸發條件

"熱點程式碼"分兩類,

​ 第一類是被多次呼叫的方法-這是由方法呼叫觸發的編譯。

​ 第二類是被多次執行的迴圈體 – 儘管編譯動作是由迴圈體所觸發的,但編譯器依然會以整個方法(而不是單獨的迴圈體)作為編譯物件。

判斷一段程式碼是不是熱點程式碼,是不是需要觸發即時編譯,這樣的行為稱為熱點探測(Hot Spot Detection);熱點探測判定方式有兩種:

​ 第一種是基於取樣的熱點探測

​ 第二種是基於計數器的熱點探測

HotSpot虛擬機器中使用的是基於計數器的熱點探測方法,因此它為每個方法準備了兩類計數器:方法呼叫計數器(Invocation Counter)和回邊計數器(Back EdgeCounter)。確定虛擬機器執行引數的前提下,這兩個計數器都有一個確定的閾值,當計數器超過閾值溢位了,就會觸發JIT編譯。

3.編譯優化技術

經典優化技術

語言無關的經典優化技術之一:公共子表示式消除。
語言相關的經典優化技術之一:陣列範圍檢查消除。
最重要的優化技術之一:方法內聯。
最前沿的優化技術之一:逃逸分析。
公共子表示式消除

    int d= (c * b)*12+a+ (a + b * c)
 
//編譯器檢測到“c * b”與“b* c”是一樣的表示式,而且在計算期間b與c的值是不變的。
        int d=E*12+a+(a+E);

  

陣列邊界檢查消除

if (foo != null) {
        return foo.value;
        } else {
        throw new NullPointerException();
        }

        # 虛擬機器隱式優化;
        try {
        return foo.value;
        } catch (Segment_Fault e) {
        uncommon_trap(e);

  

4 Java與C/C++的編譯器對比

第一,因為即時編譯器執行佔用的是使用者程式的執行時間,具有很大的時間壓力,它能提供的優化手段也嚴重受制於編譯成本

第二,Java語言是動態的型別安全語言,這就意味著需要由虛擬機器來確保程式不會違反語言語義或訪問非結構化記憶體

第三,Java語言中雖然沒有virtual關鍵字,但是使用虛方法的頻率卻遠遠大於C/C++語言

Java記憶體模型與執行緒

1. 硬體效率與一致性

除了增加快取記憶體之外,為了使得處理器內部的運算單元儘可能被充分利用,處理器可能會對輸入程式碼進行亂序執行優化,處理器會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程式中各個語句計算的先後順序與程式碼中的順序一致。

2. Java記憶體模型

​ 主記憶體與工作記憶體

​ Java記憶體模型的主要目標:定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。

​ 為了獲取較好的執行效能,Java記憶體模型並沒有限制執行引擎使用處理器的特定暫存器或快取來和主記憶體進行互動,也沒有限制即時編譯器進行調整程式碼執行順序這類優化操作。

​ 記憶體間的互動操作

​ lock(鎖定):作用於主記憶體的變數,它把一個變數標誌為一條執行緒獨佔的狀態。
​ unlock(解鎖):作用於主記憶體中的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖 定。
​ read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
​ load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
​ use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎。
​ assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接受到的值賦給工作記憶體的變數。
​ store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
​ write(寫入):作用於主記憶體中的變數,它把store操作從主記憶體中得到的變數值放入主記憶體的變數中。

​ 原子性、可見性與有序性

​ 原子性(Atomicity):由Java記憶體模型來直接保證的原子性變數操作包括read、load、assign、use、和write。

​ 可見性(Visibility):可見性是指當一個執行緒修改了共享變數的值,其它執行緒能夠立即得知這個修改。J
​ 有序性(Ordering):如果在本執行緒內觀察,所有的操作都是有序的;

3.Java與執行緒

​ 執行緒的實現

​ 使用核心執行緒實現

​ 核心執行緒(Kernel-Level Thread,KLT)就是直接由作業系統核心支援的執行緒,這種執行緒由核心來完成執行緒切換,核心通過作業系統排程器對執行緒進行排程,並負責將執行緒的任務對映到各個處理器上。每個核心執行緒可以視為核心的一個分身,這樣作業系統就有能力同時處理多件事情,支援多執行緒的核心就叫多執行緒核心。

使用使用者執行緒實現

​ 從廣義上來講,一個執行緒只要不是核心執行緒,就可以認為是使用者執行緒,因此,從這個定義上來講,輕量級程序也屬於使用者執行緒,但輕量級程序的實現始終是建立在核心之上的,許多操作都進行系統呼叫,效率會受到限制。
而狹義上的使用者執行緒指的是完全建立在使用者空間的執行緒庫上,系統核心不能感知執行緒存在的實現。
​ 使用使用者執行緒的優勢在於不需要系統核心的支援。劣勢也在於沒有系統核心的支援,所有的執行緒操作都需要使用者程式自己處理。

使用使用者執行緒加輕量級程序混合實現

​ 在這種混合模式下,即存在使用者執行緒,也存在輕量級程序。使用者執行緒還是完全建立在使用者空間中,因此使用者執行緒的建立、切換、析構等操作依然廉價,並且可以支援大規模的使用者執行緒併發

4.Java執行緒的狀態轉化

新建(New):建立後尚未啟動的執行緒處於這種狀態。
執行(Runable):Runable包括了作業系統執行緒狀態中的Running和Ready,也就是說處於此種狀態的執行緒可能正在執行,也可能正在等待CPU為它分配執行時間。
無限期等待(Waiting):處於這種狀態下的執行緒不會被分配CPU執行時間,他們要等待被其他執行緒顯示喚醒。
限期等待(Timed Waiting):處於這種狀態下的執行緒也不會被分配CPU執行時間,不過無須等待被其他執行緒顯示喚醒,在一定時間之後它們由系統自動喚醒。
阻塞(Blocked):執行緒被阻塞了,“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待著獲取一個排他鎖,這個事件將在另外一個執行緒放棄這個鎖的時候發生。
結束(Terminate):已經終止的執行緒的執行緒狀態,執行緒已經結束執行。

執行緒安全與鎖優化

1.執行緒安全

​ 當多個執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲得正確的結果,那這個物件是執行緒安全的

2.Java 語言中的執行緒安全

​ 2.1不可變

​ 在 Java 語言中,不可變執行緒一定是安全的,無論是物件的方法實現還是方法的呼叫者,都不需要採取任何的執行緒安全保障措施

​ 其中最簡單的就是把物件中帶有狀態的變數都宣告為 final,這樣在建構函式結束之後,它就是不可變的

​ 2.2絕對執行緒安全

private static Vector<Integer> vector = new Vector<Integer>();  
1
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}

    Thread removeThread = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (int i = 0; i < vector.size(); i++) {  
                vector.remove(i);  
            }  
        }  
    });  
      
    Thread printThread = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (int i = 0; i < vector.size(); i++) {  
                System.out.println(vector.get(i));  
            }  
        }  
    });  
      
    removeThread.start();  
    printThread.start();  
      
    // 不要同時產生過多的執行緒,否則會導致作業系統假死  
    while (Thread.activeCount() > 20);  
}  
}

  

3.相對執行緒安全

相對的執行緒安全就是我們通常意義上所講的執行緒安全,它需要保證對這個物件單獨的操作是執行緒安全,我們在呼叫的時候不需要做額外的保障措施,但是對於一些特定順序的連續呼叫,就可能需要在呼叫端使用額外的同步手段來保證呼叫的正確性。

4.執行緒相容

執行緒相容是指物件本身並不是執行緒安全的,但是可以通過在呼叫端正確地使用同步手段來保證物件在併發環境中可以安全地使用,我們平常說一個類不是執行緒安全的,絕大多數時候指的是這一種情況。

5.執行緒對立

執行緒對立是指無論呼叫端是否採取了同步措施,都無法在多執行緒環境中併發使用的程式碼。由於 Java 語言天生就具備多執行緒特性,執行緒對立這種排斥多執行緒的程式碼是很少出現的,而且通常都是有害的,應當儘量避免。

執行緒安全的實現方法

1.互斥同步

同步是指在多個執行緒併發訪問共享資料時,保證共享資料在同一個時刻只被一個(或者是一些,使用訊號量的時候)執行緒使用,而互斥是實現同步的一種手段,互斥是方法,同步是目的

2.非阻塞同步

測試並設定,獲取並增加,交換,比較並交換,載入連線 / 條件儲存

3.無同步方案

​ 要保證執行緒安全,不一定非要保證執行緒同步,還可以有其他的方案

​ 1.可重入程式碼
​ 2.執行緒本地儲存

鎖優化

自旋鎖和自適應自旋鎖

如果鎖在很短的時間內釋放了,那麼自旋的效果就很好

偏向鎖

​ 偏向鎖的意思是這個鎖會偏向第一個獲取到他的鎖,如果在接下來執行的過程中,該鎖一直沒有被其他的鎖獲取的話,則持有偏向鎖的執行緒永遠不需要再進行同步.一旦有新的執行緒試圖獲取該鎖,偏向模式會被撤銷.撤銷後進入無鎖狀態.這裡會改變物件頭的關於偏性模式的標誌位和關於鎖的標誌位

輕量級鎖
當使用輕量級鎖(鎖標識位為00)時,執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中

鎖粗化

​ 這個原則大部分時間是對的但是如果一個系列的連續操作都是對同一個物件反覆的加鎖和解鎖,甚至加鎖操作出現在迴圈體之中,即使沒有執行緒競爭,頻繁的進行互斥同步的操作也會導致不必要的效能損耗.

鎖消除

public String concatString(String s1, String s2){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

  

​ 我們發現sb的引用被限制在了concatStirng方法裡面他永遠不可能被其他執行緒訪問到,因此雖然這裡有鎖但是可以被安全的消除掉.在解釋執行時這裡仍然會枷鎖,但是經過服務端編譯器即時編譯後,這段程式碼會自動忽略所有的同步措施直接執行.

最後

大家看完有什麼不懂的可以在下方留言討論,也可以關注我私信問我,我看到後都會回答的。也歡迎大家關注我的公眾號:前程有光,金三銀四跳槽面試季,整理了1000多道將近500多頁pdf文件的Java面試題資料,文章都會在裡面更新,整理的資料也會放在裡面。謝謝你的觀看,覺得文章對你有幫助的話記得關注我點個贊支援一下!