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

前言

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

JVM探究

jvm的位置

jvm的體系結構java

堆裏面有垃圾,須要被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語句

雙親委派機制的做用

一、防止重複加載同一個.class。經過委託去向上面問一問,加載過了,就不用再加載一遍。保證數據安全。
二、保證核心.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面試題資料,文章都會在裏面更新,整理的資料也會放在裏面。謝謝你的觀看,以爲文章對你有幫助的話記得關注我點個贊支持一下!

相關文章
相關標籤/搜索