那麼運行時數據區包括哪幾部分呢?java
用來指示程序執行哪一條指令,這跟彙編語言的程序計數器的功能在邏輯上是同樣的。JVM規範中規定,若是線程執行的是非native方法,則程序計數器中保存的是當前須要執行的指令地址,若是線程執行的是native方法,則程序計數器中的值undefined。每一個線程都有本身獨立的程序計數器。爲何呢?由於多線程下,一個CPU內核只會執行一條線程中的指令,所以爲了使每一個線程在線程切換以後可以恢復到切換以前的程序執行的位置,因此每一個線程都有本身獨立的程序計數器。程序員
Java虛擬機棧中存放的是一個個棧幀,當程序執行一個方法時,就會建立一個棧幀並壓入棧中,當方法執行完畢以後,便會將棧幀移除棧。咱們所說的「棧」是指Java虛擬機棧,一個棧幀中包括:局部變量表、操做數棧、動態鏈接、方法返回地址、附加信息算法
局部變量表編程
主要是存儲方法中的局部變量,包括方法中局部變量的信息和方法的參數。如:各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址),其中64位長度的long和double類型的數據會佔用2個局部變量空間(Slot),其他的數據類型只佔用1個。局部變量表的大小在編譯器就能夠肯定其大小了,所以在程序執行期間局部變量表的大小是不會改變的。在Java虛擬機規範中,對這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。數組
操做數棧緩存
虛擬機把操做數棧做爲它的工做區,程序中的全部計算過程都是在藉助於操做數棧來完成的,大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧。安全
動態鏈接數據結構
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用(指向運行時常量池:在方法執行的過程當中有可能須要用到類中的常量),持有這個引用是爲了支持方法調用過程當中的動態鏈接多線程
方法返回地址架構
當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址。
附加信息
虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀中,例如與高度相關的信息,這部分信息徹底取決於具體的虛擬機實現。在實際開發中,通常會把動態鏈接,方法返回地址與其它附加信息所有歸爲一類,稱爲棧幀信息。
本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
在C語言中,程序員能夠經過malloc函數和free函數在堆上申請和釋放空間。那麼在Java中是怎麼樣的呢?Java中的堆是用來存儲對象自己的以及數組(固然,數組引用是存放在Java棧中的),幾乎全部的對象實例都在這裏分配內存。在Java中,程序員基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。另外,堆是被全部線程共享的,在JVM中只有一個堆。
方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、以及編譯器編譯後的代碼等。運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。在JVM規範中,沒有強制要求方法區必須實現垃圾回收。不少人習慣將方法區稱爲「永久代」,是由於HotSpot虛擬機以永久代來實現方法區,從而JVM的垃圾收集器能夠像管理堆區同樣管理這部分區域,從而不須要專門爲這部分設計垃圾回收機制。不過自從JDK7以後,Hotspot虛擬機便將運行時常量池從永久代移除了。
句柄訪問,對象移動時,只會改變句柄中實例的數據指針。
Sum Hotspot採用的直接指針訪問對象, 減小第二次尋址。
-Xms JVM初始分配的堆內存,
-Xmx JVM最大容許分配的堆內存,
-XX:apDumpOnOutOfMemoryError
MAT內存分析工具
-Xss設置棧大小, -Xoss設置本地方法棧大小(實際無效)。
線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverFlowError。
虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出OutOfMemoryError。
-XX:PermSize JVM初始分配的非堆內存
-XX:MaxPermSize JVM最大容許分配的非堆內存
-XX:MaxDirectMemorySize指定DirectByteBuffer能分配的空間的限額,若是沒有顯示指定這個參數啓動jvm,默認值是xmx對應的值。
程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出執行者出棧和入棧操做。這幾個區域內存分配和回收都具有肯定性,方法結束或者線程結束時, 內存天然跟着回收。
Java堆和方法區, 內存分配不固定,內存的分配和回收都是動態的,垃圾收集器所關注的也是這裏。
主流Java虛擬機沒有采用,主要緣由是很難解決對象之間相互循環引用的問題。
主流商用程序語言(Java、C#)經過可達性分析來斷定對象是否存活。
利用引用鏈判斷對象是否可回收。
GC Roots對象包括如下幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象;
方法區中的靜態屬性引用的變量;
方法區中常量引用的對象;
本地方法棧中JNI(Native方法)引用的對象;
強引用 只要存在,垃圾收集器永遠不會回收掉被引用的對象;
軟引用:有用但非必須的引用,當內存不足時, 此類引用的對象會被回收;
弱引用:非必須的引用,進行垃圾回收時,此類對象會被回收;
虛引用:虛引用不對對象生存時間構成影響,沒法經過此類引用獲取對象實例,使用目的是對象被回收時收到一個系統通知。
一個對象的回收須要經歷兩次標記過程,若是可達性分析後沒有與GC Roots相鏈接的引用鏈,會被第一次標記並進行一次篩選,篩選有沒有必要執行finalize()方法(當對象沒有覆蓋finalize方法,或者finalize被執行過, 則視爲沒有必要執行finalize)。若是必要執行finalize方法,對象被放置在F-Queue隊列中,由低優先級的Finalize去執行, 並不保證對象的finalize方法必定執行完(執行時間過長會影響隊列其餘對象),這是對象最後的自我拯救(成功則第二次標記時移除即將回收的集合),對象的finalize()方法只會執行一次,以後在通過一次標記肯定最終是否被回收。
方法區的垃圾回收性價比不高,通常狀況下可回收的空間不多,主要回收廢棄的常量和無用的類。
廢棄常量回收的條件是,該常量不被引用。
無用的類回收的條件是,該類全部的實例都已經被回收,加載該類的ClassLoader已經被回收,該類對應的java.lang.Class對象沒有任何地方被引用,沒法在任何地方經過反射訪問該類的方法,但不必定會被回收。
首先表記須要被清除的對象,標記完成後統一回收被標註的對象。缺點:效率不高,會產生大量不連續的內存碎片。
可用內存分爲大小相等的兩塊,當其中一塊內存用完,將還存活的對象複製到另外一塊上,清理掉以前使用的一塊內存。缺點:可用內存縮小爲了原來的一半。
標記過程與「標記-清除」算法同樣,而後讓全部存得到對象都向一段移動,而後清理掉邊界之外的內存。
根據對象存活週期的不一樣將內存劃分爲幾塊,堆通常分爲新生代和老年代,新生代選用複製算法,老年代用標記-清理或標記-整理算法來進行回收。
枚舉根節點
安全點
安全區域
Serial收集器
ParNew收集器
Paraller scavenge收集器
CMS收集器
G1收集器
對象優先在Eden區進行分配, 當Eden區沒有足夠的空間的時候,虛擬機發起一次Minor GC(發生在新生代的垃圾回收動做)。若是Minor GC 後, 另外一個Servivo區空間不足以存在原Eden區存活對象, 只好經過分配擔保機制提早轉移到老年代去。
-XX:+PrintGCDetail 告訴虛擬機在垃圾回收的時候打印回收日誌
-XX:SurvivorRatio=8 設置新生代Eden區與一個Survivor區空間比例
打對象指須要大量連續內存空間的Java對象,例如很長的字符串以及數組。
-XX:PretenureSizeThreshold參數,大於這個設定值的參數直接在老年代分配。以免在Eden區與兩個Servivor區之間發生大量的內存複製(新生代採用複製算法收集內存)。
Eden區出生通過一次Minor GC後仍存活,而且能被Servivor容納的話,將被移動到Servivor空間, 年齡默認爲1歲,每通過一次Minor GC,年齡就增長一歲,累積到必定的年齡就會被晉升到老年代。
-XX:MaxTenuringThreshold 設置對象晉升到老年代的年齡閾值
虛擬機並非永遠要求對象的年齡必須達到MaxTenuringThreshold才能晉升到老年代,若是在Servivor空間中相同年齡對象空間總和大於Servivor空間的一半,年齡大於或者等於該年齡的對象就能夠直接進入到老年代中。
在Minor GC以前,虛擬機會先檢查老年代最大可用連續空間是否大於新生代全部對象的總空間,條件成立則Minor GC是安全的。若是不成立,則查看HadlePromotionFailure設置值是否容許擔保失敗。若是容許,會檢查老年代最大可用連續空間是否大於歷次晉升到老年代對象的平均大小,大於則進行一次Minor GC,有風險;若是小於,或者HandlePromotionFailure不容許擔保失敗,也要更改成執行一次Full GC。
JConsole java監視與管理平臺
Visualvm 多合一故障處理工具
Class類文件結構
JVM類加載機制分爲五個部分:加載,驗證,準備,解析,初始化
虛擬機規範嚴格規定了有且只有5中狀況(jdk1.7)必須對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):
1.經過子類來引用父類中定義的靜態字段,只會觸發父類的初始化,不會觸發子類的初始化。
2.經過數組定義引用類,不會觸發類的初始化,會由虛擬機自動生成直接繼承Object的子類。
3.常亮在編譯階段會存入調用類的常量池中,本質上並無直接引用定義常量的類,所以不會觸發定義常量類的初始化。常量被存儲到NotInitialzation類的常量池中,對於常量的引用被轉化爲NotInitialization類對自身常量池的引用。
接口與類區別:當一個類初始化時,要求其父類所有都已經初始化過了,可是一個接口在初始化時,並不要求其父類接口所有都完成了初始化,只有在真正使用父接口的時候(引用接口中定義的常量)纔會初始化。
加載階段,虛擬機完成的三件事情:
1.經過類的權限定名來獲取定義此類的二進制字節流;
2.將這個字節流所表明的靜態存儲結構轉換爲方法去的運行時數據結構;
3.在內存中生成一個表明這個類的java.lang.Class對象,做爲方法去這個類的各類數據的訪問入口;
目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,不會危害虛擬機。
1.文件格式驗證,演示字節流是否符合Class文件格式規範,能被當前版本的虛擬機處理;
2.元數據驗證,對字節碼描述的信息進行語義分析;
3.字節碼驗證:經過數據流和控制流分析,確認程序語義合法、符合邏輯,對類的方法體進行分析;
4.符號引用分析:發生在虛擬機將符號引用轉化爲直接引用時,對類自身之外(常量池中的各類符號引用)的信息進行匹配性效驗。
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,內存都在方法區中分配。內存分配僅包含類變量(static的),實例變量在對象實例化時隨對象一塊兒分配在堆中。
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
此階段才真正開始執行類中定的Java程序代碼。
執行類構造器<clinit>:
<clinit>收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生,收集順序由語句在源文件中出現的順序所決定,靜態語句塊中只能訪問定義在靜態塊以前的變量,定義在它以後的變量,能夠在前面的靜態語句塊中賦值可是不能訪問。
<clinit>方法與類的構造不一樣,不須要顯示的調用父類構造器,虛擬機會保證父類的<clinit>在子類執行前執行完畢,第一個確定是Object。
<clinit>因爲父類的先執行,也就意味着父類的靜態語句塊要優先與子類的變量賦值操做。
<clinit>不是必須的,若是沒有靜態語句塊,也沒有變量賦值操做,不會生成<clinit>方法。
接口中不能用靜態語句塊,但會有賦值操做,所以<clinit>不須要先執行父類的<clinit>方法。只有當父類接口中定義的變量使用時,父接口才會初始化。接口的實現類在初始化時也不會執行接口的<clinit>。
<clinit>在多線程中被正確的加鎖、同步,多個線程同時初始化一個類時,只有一個線程執行<clinit>,其餘的阻塞等待直到活動線程執行完。
類加載階段中「經過一個類的全限定名來獲取描述此類的二進制字節流」動做放到虛擬機外部實現,以便讓程序本身決定如何去獲取所須要的類。
類加載器和類自己一同確立其在虛擬機中的惟一性。比較兩個類是否相等,前提是由同一個類加載器加載。
雙親委派模型的工做過程:若是一個類加載器收到了類加載器的請求.它首先不會本身去嘗試加載這個類.而是把這個請求委派給父加載器去完成。每一個層次的類加載器都是如此。所以全部的加載請求最終都會傳送到Bootstrap類加載器(啓動類加載器)中.只有父類加載反饋本身沒法加載這個請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
啓動類加載器(Bootstrap ClassLoader):負責將<JAVA_HOME>/lib目錄或者指定路徑下虛擬機可識別的類庫(*.jar)加載到虛擬機內存中。該加載器沒法被Java程序直接引用,用戶編寫的自定義類加載器須要把加載請求委派給引導類加載器。
擴展類加載器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader實現,負責加載%JAVA_HOME%\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。
應用程序類加載器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader實現,負責加載用戶類路徑classpath上所指定的類庫,是類加載器ClassLoader中的getSystemClassLoader()方法的返回值,開發者能夠直接使用應用程序類加載器,若是程序中沒有自定義過類加載器,該加載器就是程序中默認的類加載器。
注意:並非強制性的。
JDK1.2向上兼容,JNDI,程序動態性。
線程上下問類加載器(Thread Context ClassLoader): 可實現父類加載器請求子類加載器完成類加載。
局部變量表: 變量值存取空間,存放方法參數和方法內部定義的局部變量。局部變量表是創建在線程的棧上,是線程的私有數據,所以不存在數據安全問題。不會初始化默認值。
操做數棧:後入先出棧,由字節碼指令往棧中存數據和取數據,棧中的任何一個元素都是能夠任意的Java數據類型。
動態連接:每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有該引用是爲了支持方法調用過程當中的動態鏈接。
方法返回地址:存放調用調用該方法的pc計數器的值。當一個方法開始以後,只有兩種方式能夠退出這個方法:一、執行引擎遇到任意一個方法返回的字節碼指令,也就是所謂的正常完成出口。二、在方法執行的過程當中遇到了異常,而且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出,這種方式成爲異常完成出口。正常完成出口和異常完成出口的區別在於:經過異常完成出口退出的不會給他的上層調用者產生任何的返回值。 不管經過哪一種方式退出,在方法退出後都返回到該方法被調用的位置,方法正常退出時,調用者的pc計數器的值做爲返回地址,而經過異常退出的,返回地址是要經過異常處理器表來肯定,棧幀中通常不會保存這部分信息。本質上,方法的退出就是當前棧幀出棧的過程。
全部依賴靜態類型來定位方法執行版本的分派成爲靜態分派,發生在編譯階段,典型應用是方法重載。
在運行期間根據實際類型4來肯定方法執行版本的分派成爲動態分派,發生在程序運行期間,典型的應用是方法的重寫。
Java是一門靜態多分配,動態單分配的語言。
動態分派在Java中被大量使用,使用頻率及其高,若是在每次動態分派的過程當中都要從新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率,所以JVM在類的方法區中創建虛方法表(virtual method table)來提升性能。每一個類中都有一個虛方法表,表中存放着各個方法的實際入口。若是某個方法在子類中沒有被重寫,那子類的虛方法表中該方法的地址入口和父類該方法的地址入口同樣,即子類的方法入口指向父類的方法入口。若是子類重寫父類的方法,那麼子類的虛方法表中該方法的實際入口將會被替換爲指向子類實現版本的入口地址。虛方法表會在類加載的鏈接階段被建立並開始初始化,類的變量初始值準備完成以後,JVM會把該類的方法表也初始化完畢。
Java編譯器輸入的指令流基本上是一種基於棧的指令集架構,指令流中的指令大部分是零地址指令,其執行過程依賴於操做棧。另一種指令集架構則是基於寄存器的指令集架構。最直接的區別是,基於棧的指令集架構不須要硬件的支持,而基於寄存器的指令集架構則徹底依賴硬件,這意味基於寄存器的指令集架構執行效率更高,單可移植性差,而基於棧的指令集架構的移植性更高,但執行效率相對較慢,初次以外,相同的操做,基於棧的指令集每每須要更多的指令。
OSGi(Open Service Gateway Initiative) 是 OSGi 聯盟(OSGi Alliance)制定的一個基於 Java 語言的動態模塊化規範。
OSGi 在提供強大功能的同時,也引入了額外的複雜度,帶來了線程死鎖和內存泄露的風險。
Sum Javac編譯過程:
1.解析與填充符號表
2.插入式註解處理器的註解處理過程
3.分析與字節碼生成過程
語法糖(Syntactic Sugar),也稱糖衣語法,指在計算機語言中添加的某種語法,這種語法對語言自己功能來講沒有什麼影響,只是爲了方便程序員的開發,提升開發效率。說白了,語法糖就是對現有語法的一個封裝。
Java中的語法糖主要有如下幾種:
1. 泛型與類型擦除
2. 自動裝箱與拆箱,變長參數、
3. 加強for循環
4. 內部類與枚舉類
自動裝箱。拆箱在編譯以後被轉化成了對應的包裝盒還原方法。
循環遍歷還原成了迭代器的實現,這也是循環遍歷須要被遍歷的類實現Iterable接口的緣由。
變長參數 使用有兩個條件,一是變長的那一部分參數具備相同的類型,二是變長參數必須位於方法參數列表的最後面。變長參數一樣是Java中的語法糖,其內部實現是Java數組。
自動裝箱陷阱:包裝類的「==」運算在不遇到算術運算的狀況下不會自動拆箱, 以及它們equals()方法不處理數據類型轉換的關係。
Java的條件編譯,也是語法糖,根據條件值的布爾值的真假,編譯器把分支中不成立的代碼去掉,這一階段在編譯器接觸語法糖的階段完成。
實戰:代碼格式掃描
Java程序在運行的期間,可能會有某個方法或者代碼塊的運行特別頻繁時,就會把這些代碼認定爲「熱點代碼」。爲了提升熱點代碼的執行效率,在運行時JVM會將這些代碼編譯成與本地平臺相關的機器碼,並進行各類層次的優化,完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler,JIT編譯器)。
運行過程當中會被即時編譯器編譯的「熱點代碼」有兩類:
一、被屢次調用的方法;
二、被屢次執行的循環體。
對於第一種狀況,因爲是由方法調用觸發的編譯,所以編譯器會以整個方法做爲編譯對象,這種編譯也是虛擬機中標準的JIT編譯方式。而對第二種狀況,儘管編譯動做是由循環體所觸發的,但編譯器依然會以整個方法做爲編譯對象,這種編譯方式由於編譯發生在方法執行過程之中,所以形象地被稱爲棧上替換,簡稱爲OSR編譯,即方法棧幀還在棧上,方法就被替換了。
主要的熱點探測斷定方法有兩種:
公共子表達式消除: 若是一個表達式E已經計算過了,而且從先前的計算到如今E中全部變量的值都沒有發生變化,那E的此次出現就成公共子表達式,能夠用原先的表達式進行消除。
數組邊界檢查消除:系統將自動進行上下界的範圍檢查。隱式異常處理:Java中空指針和算術運算中除數爲零的檢查。此外還有:自動裝箱消除、安全點消除、消除反射等等。
方法內聯:把目標方法的代碼「複製」到發起調用的方法之中,避免發生真實的方法調用。
逃逸分析:分析對象的動態做用域,當一個對象在方法中被定義後,它可能被外部方法所引用,例如做爲調用參數傳遞給其餘方法,稱爲方法逃逸。甚至還有可能被外部線程訪問到,好比賦值給類變量或能夠在其餘線程中訪問到的實例變量,稱爲線程逃逸。
若是能證實一個對象不會逃逸到方法或線程以外,也就是別的方法或線程沒法經過任何途徑訪問到這個對象,就能夠爲這個變量進行一些高效的優化:如:棧上分配、同步消除、標量替換等。
在多處理器系統中,每一個處理器都有本身的高速緩存,而他們又共享同一主內存(Main Memory),如上圖所示。當多個處理器的運算任務都涉及到主內存中的同一塊區域,那麼將高速緩存中的數據同步回主內存時,爲了保證數據的一致性,須要各個處理器訪問緩存時都遵循一些協議,即緩存一致性協議。
Java虛擬機規範中試圖定義一種Java內存模型來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺上都能達到一致的內存訪問效果。
Java內存模型的主要目標:定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。注意,此處的變量與Java編程語言中所說的變量有所區別,它包括實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,天然就不會存在競爭問題。
主內存:Java內存模型規定了全部的變量都存儲在主內存中,此處的主內存僅僅是虛擬機內存的一部分,而虛擬機內存也僅僅是計算機物理內存的一部分(爲虛擬機進程分配的那一部分)
工做內存,線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝。線程對變量的全部操做(讀取、賦值),都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成
虛擬機實現時必須保證下面說起的每一種操做都是原子操做。
lock(鎖定):做用於主內存的變量,它把一個變量標誌爲一條線程獨佔的狀態。
unlock(解鎖):做用於主內存中的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
load(載入):做用於工做內存的變量,把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎。
assign(賦值):做用於工做內存的變量,它把一個從執行引擎接受到的值賦給工做內存的變量。
store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
write(寫入):做用於主內存中的變量,它把store操做從主內存中獲得的變量值放入主內存的變量中。
long和double類型的特殊規則:java虛擬機容許將64位數據的讀寫操做劃分爲兩次32位的操做。
1.保證了新值能當即存儲到主內存,每次使用前當即從主內存中刷新。
2.禁止指令重排序優化。
注:volatile關鍵字不能保證在多線程環境下對共享數據的操做的正確性。可使用在本身狀態改變以後須要當即通知全部線程的狀況下。
原子性:
在java內存模型來直接保證的原子性變量操做包括read、load、assign、use、stroe、write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(例外就是long和double的非原子性協定)
可見性:
可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。
java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的。
除了volatile以外,java還有兩個關鍵字能實現可見性,即synchronized和final。
有序性:
若是在本線程內觀察,全部的操做都是有序的,表現爲線程內串行的語義;若是在一個線程中觀察另外一個線程,全部的線程都是無序的,表現爲指令重排序現象和工做內存與主內存同步延遲現象。
volitile關鍵字自己就包含了禁止指令重排序的語義,而synchronized的規則「一個變量在同一個時刻只容許一條線程對其進行lock操做」決定了持有同一個鎖的兩個同步塊只能串行地進入。
兩項操做之間的偏序關係。若是操做A先於操做B發生,那麼操做A產生的影響能被操做B觀察到,影響包括修改了內存中共享變量的值、發送了消息、調用了方法等。
先行發生規則:
1)程序次序規則:在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確的說,應該是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構
2)管程鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做,後面是指時間上的前後順序
3)volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做。後面也是時間上的順序
4)線程啓動規則:Thread對象的start()方法先行發生於此線程的每個動做
5)線程終止規則:線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行
6)線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生
7)對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始
8)傳遞性:若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論
實現線程主要有3種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現
內核線程就是直接由操做系統內核(Kernel)支持的線程,這種線程由內核來完成線程切換,內核經過操做調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上。每一個內核線程能夠視爲內核的一個分身,這樣操做系統就有能力同時處理多件事情,支持多線程的內核就叫作多線程內核。是1:1的關係
用戶線程指徹底創建在用戶空間的線程庫上,系統內核不能感知線程存在的實現。用戶線程的創建、同步、銷燬和調度徹底在用戶態中完成,不須要內核的幫助。是1:N的關係
內核線程和用戶線程混合使用,推薦。是N:M的關係
系統爲線程分配處理器使用權主要有兩種調度方式:協同式線程調度和搶佔式線程調度。java使用的是搶佔式,能夠經過分配優先級來控制。
1. 新建狀態(New):新建立了一個線程對象。
2. 就緒狀態(Runnable):線程對象建立後,其餘線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
3. 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
4. 阻塞狀態(Blocked):阻塞狀態是線程由於某種緣由放棄CPU使用權,暫時中止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的狀況分三種:
(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
(三)、其餘阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入就緒狀態。
5. 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。
線程安全定義:當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象是線程安全的;
1. 不可變:
不可變的對象必定是線程安全的。如String類。
2. 絕對線程安全
3. 相對線程安全
4. 線程兼容
線程兼容是指對象自己並非線程安全的,可是能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用;
5. 線程對立
指不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼;
互斥同步:是常見的併發正確性保障手段;
同步:是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻被一個線程使用。
互斥:互斥是實現同步的一種手段;臨界區,互斥量和信號量都是主要的互斥實現方式。所以,在這4個字裏面,互斥是因,同步是果;互斥是方法,同步是目的;
(1)最基本的互斥同步手段就是 synchronized關鍵字:synchronized關鍵字通過 編譯以後,會在同步塊的先後分別造成 monitorenter 和 monitorexit 這個兩個字節碼指令,這兩個字節碼都須要一個 reference類型的參數來指明要鎖定和解鎖的對象;若是java程序中的synchronized明確指定了對象參數,那就是這個對象的reference;若是沒有明確指定,那就根據 synchronized修飾的實例方法仍是類方法,去取對應的對象實例或Class 對象來做爲鎖對象;(最基本的互斥同步手段就是 synchronized關鍵字)
根據虛擬機規範的要求:在執行monitorenter指令時,若是這個對象沒有鎖定或當前線程已經擁有了那個對象的鎖,鎖的計數器加1,相應的,在執行 monitorexit 指令時會將鎖計數器減1;當計數器爲0時,鎖就被釋放了;
synchronized同步塊對同一條線程來講是可重入的, 不會出現本身把本身鎖死的問題;同步塊在已進入的線程執行完以前,會阻塞後面其餘線程 的進入;
(2)java.util.concurrent 包中的重入鎖(ReentrantLock)來實現同步
synchronized 和 ReentrantLock 的區別: 一個表現爲 API 層面的互斥鎖(lock() 和 unlock() 方法配合 try/finally 語句塊來完成),另外一個表現爲 原生語法層面的互斥鎖;
ReentrantLock增長了一些高級功能:主要有3項:等待可中斷,可實現公平鎖, 鎖能夠綁定多個條件;
等待可中斷:指當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情,可中斷特性對處理執行時間很是長的同步塊頗有幫助;
公平鎖:指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖;
鎖綁定多個條件:指一個 ReentrantLock對象能夠同時綁定多個 Condition對象,而在 synchronized中,鎖對象的wait() 和 notify() 或 notifyAll() 方法能夠實現一個隱含的條件,若是要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock 則無需這樣作,只須要屢次調用 newCondition() 方法便可;
阻塞同步(互斥同步)的問題:就是進行線程阻塞和喚醒所帶來的性能問題,互斥同步屬於一種悲觀的併發策略,不管共享數據是否真的會出現競爭,它都要進行加鎖,用戶態核心態轉換,維護鎖計數器和檢查是否有被阻塞的線程須要喚醒等操做。
非阻塞同步定義:基於衝突檢測的樂觀併發策略,通俗的說,就是先進行操做,若是沒有其餘線程爭用共享數據,那操做就成功了;若是共享數據有爭用,產生了衝突,那就再採用其餘的補償措施,這種樂觀的併發策略的許多實現都不須要把線程掛起,所以這種同步操做稱爲 非阻塞同步;
若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性,所以會有一些代碼天生就是線程安全的;
第一類線程安全代碼——可重入代碼:也叫做純代碼,能夠在代碼執行的任什麼時候刻中斷它,轉而去執行另一段代碼,而在控制權返回後,原來的程序不會出現任何錯誤; 全部的可重入代碼都是線程安全的;
如何判斷代碼是否具有可重入性:若是一個方法,它的返回結果是能夠預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就知足可重入性的要求,固然也就是線程安全的;
第二類線程安全代碼——線程本地存儲:若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否可以保證在同一線程中執行? 若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程內,這樣,無需同步也能夠保證線程間不出現數據爭用問題;
自旋鎖:當兩個或兩個以上的線程同時並行執行,讓後面請求鎖的那個線程「稍等一下」,但不放棄處理器的執行時間,看看持有所的線程是否能很快釋放鎖。
缺點:自旋的線程只會白白消耗處理器資源,帶來性能上的浪費。故須要使等待時間有必定的限度
改進:等待時間要自適應。等待時間隨着程序運行和性能監控信息的不斷完善。
鎖消除:虛擬機及時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。
判斷依據:源於逃逸分析的數據支持。若堆上的全部數據都不會逃逸出去從而被其餘線程訪問到,那就能夠把它們看成棧上的數據看待。
出現的問題:若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中,那即便沒有線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。
措施:若是虛擬機探測到有這樣一串零碎的操做都對同一個對象加鎖,【將會把加鎖同步的範圍擴展到整個操做序列外部】。
HotSpot虛擬機的對象頭分爲兩部分信息:
第一部分:用於存儲對象自身的運行時數據,如哈希碼,GC分代年齡等;這部分數據的長度在32位和64位的虛擬機中分別爲 32bit 和 64bit,官方稱它爲 Mark Word,它是實現輕量級鎖和偏向鎖的關鍵;(Mark Word 是實現輕量級鎖和偏向鎖的關鍵)
第二部分:用於存儲指向方法區對象類型數據的指針,若是是數組對象的話,還會有一個額外的部分用於存儲數組長度;
對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會工具對象的狀態複用本身的存儲空間;
加鎖過程:
(1)若是此同步對象沒有被鎖定(鎖標誌位爲01狀態):虛擬機首先將在當前線程的棧幀中創建一個名爲 鎖記錄的空間,用於存儲對象目前的Mark Word 的拷貝;
(2)而後,虛擬機將使用CAS 操做嘗試將對象的 Mark Word 更新爲指向 Lock Record的指針;
(3)若是這個更新工做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位將轉變爲 00,即表示 此對象處於輕量級鎖定狀態;
(4)若是這個更新失敗了,虛擬機首先會檢查對象的Mark Word 是否指向當前線程的棧幀,若是隻說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明這個鎖對象以及被其餘線程搶佔了。若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌的狀態值變爲 10,Mark Word中存儲的就是指向重量級(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態;
解鎖過程:
(1)若是對象的Mark Word仍然指向着線程的鎖記錄,那就用CAS 操做把對象當前的Mark Word 和 線程中複製的 Dispatched Mard Word替換回來;
(2)若是替換成功,整個同步過程就over了;
(3)若是替換失敗,說明有其餘線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程;
結論:
輕量級鎖能提高程序同步性能的依據是: 對於絕大部分的鎖,在整個同步週期內都是不存在競爭的;
若是沒有競爭,輕量級鎖使用CAS 操做避免了使用互斥量的開銷;但若是存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS 操做,所以在有競爭的case下, 輕量級鎖會比傳統的重量級鎖更慢;
偏向鎖的目的:消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能;
若是說輕量級鎖是在無競爭的狀況使用CAS 操做去消除同步使用的互斥量:那偏向鎖就是在無競爭的狀況下把整個同步都消除掉,連CAS 操做都不作了;
偏向鎖的偏: 它的意思是這個鎖會偏向於 第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步;
偏向鎖的原理:若當前虛擬機啓用了偏向鎖,那麼,當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲01, 即偏向模式;同時使用CAS 操做把獲取到這個鎖的線程的ID 記錄在對象的 Mark Word之中,若是 CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做;
當有另外一個線程去嘗試獲取這個鎖時,偏向模式就結束了:根據鎖對象目前是否處於被鎖定的狀態, 撤銷偏向後恢復到未鎖定(標誌位爲01)或輕量級鎖定(標誌位爲00)的狀態,後續的同步操做就如上面介紹的輕量級鎖那樣執行;
結論:
偏向鎖能夠提升帶有同步但無競爭的程序性能;
若是程序中大多數的鎖老是被多個不一樣的線程訪問:那偏向模式是多餘的;