先上一個最容易理解的類實例化的內存模型案例截圖:java
周志明著的《深刻理解 Java 虛擬機》的乾貨~若有錯誤,歡迎指出 O(∩_∩)O 轉載請保留以上信息。算法
JDK 是用於支持 Java 程序開發的最小環境。數組
Java 程序設計語言緩存
Java 虛擬機安全
Java API類庫markdown
JRE 是支持 Java 程序運行的標準環境。數據結構
Java SE API 子集多線程
Java 虛擬機併發
程序計數器
Java 虛擬機棧
本地方法棧
Java 堆
方法區
運行時常量池
直接內存
程序計數器(Program Counter Register)是一塊較小的內存空間,能夠看做是當前線程所執行字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器完成。
因爲 Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式實現的。爲了線程切換後能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器,各線程之間的計數器互不影響,獨立存儲。
若是線程正在執行的是一個 Java 方法,計數器記錄的是正在執行的虛擬機字節碼指令的地址;
若是正在執行的是 Native 方法,這個計數器的值爲空。
程序計數器是惟一一個沒有規定任何 OutOfMemoryError 的區域。
Java 虛擬機棧(Java Virtual Machine Stacks)是線程私有的,生命週期與線程相同。
虛擬機棧描述的是 Java 方法執行的內存模型:每一個方法被執行的時候都會建立一個棧幀(Stack Frame),存儲
局部變量表
操做棧
動態連接
方法出口
每個方法被調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
這個區域有兩種異常狀況:
StackOverflowError:線程請求的棧深度大於虛擬機所容許的深度
OutOfMemoryError:虛擬機棧擴展到沒法申請足夠的內存時
虛擬機棧爲虛擬機執行 Java 方法(字節碼)服務。
本地方法棧(Native Method Stacks)爲虛擬機使用到的 Native 方法服務。
Java 堆(Java Heap)是 Java 虛擬機中內存最大的一塊。Java 堆在虛擬機啓動時建立,被全部線程共享。
做用:存放對象實例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上能夠不連續,只要邏輯上連續便可。
方法區(Method Area)被全部線程共享,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
和 Java 堆同樣,不須要連續的內存,能夠選擇固定的大小,更能夠選擇不實現垃圾收集。
運行時常量池(Runtime Constant Pool)是方法區的一部分。保存 Class 文件中的符號引用、翻譯出來的直接引用。運行時常量池能夠在運行期間將新的常量放入池中。
Object obj = new Object();
對於上述最簡單的訪問,也會涉及到 Java 棧、Java 堆、方法區這三個最重要內存區域。
若是出如今方法體中,則上述代碼會反映到 Java 棧的本地變量表中,做爲 reference 類型數據出現。Object obj
new Object()
反映到 Java 堆中,造成一塊存儲了 Object 類型全部對象實例數據值的內存。Java堆中還包含對象類型數據的地址信息,這些類型數據存儲在方法區中。
引用計數法
根搜索算法
給對象添加一個引用計數器,每當有一個地方引用它,計數器就+1,;當引用失效時,計數器就-1;任什麼時候刻計數器都爲0的對象就是不能再被使用的。
很難解決對象之間的循環引用問題。
經過一系列的名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來講就是從 GC Roots 到這個對象不可達)時,則證實此對象是不可用的。
能夠做爲GC Root 引用點的是:
GC管理的主要區域是Java堆,通常狀況下只針對堆進行垃圾回收。方法區、棧和本地方法區不被GC所管理,於是選擇這些區域內的對象做爲GC roots,被GC roots引用的對象不被GC回收。
在 JDK 1.2 以後,Java 對引用的概念進行了擴充,將引用分爲
強引用 Strong Reference
軟引用 Soft Reference
弱引用 Weak Reference
虛引用 Phantom Reference
Object obj = new Object();
代碼中廣泛存在的,像上述的引用。只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
用來描述一些還有用,但並不是必須的對象。軟引用所關聯的對象,有在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍,並進行第二次回收。若是此次回收仍是沒有足夠的內存,纔會拋出內存異常。提供了 SoftReference 類實現軟引用。
描述非必須的對象,強度比軟引用更弱一些,被弱引用關聯的對象,只能生存到下一次垃圾收集發生前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。提供了 WeakReference 類來實現弱引用。
一個對象是否有虛引用,徹底不會對其生存時間夠成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象關聯虛引用的惟一目的,就是但願在這個對象被收集器回收時,收到一個系統通知。提供了 PhantomReference 類來實現虛引用。
標記-清除算法
複製算法
標記-整理算法
分代收集算法
分爲標記和清除兩個階段。首先標記出全部須要回收的對象,在標記完成後統一回收被標記的對象。
1. 效率問題。標記和清除過程的效率都不高。
2. 空間問題。標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能致使,程序分配較大對象時沒法找到足夠的連續內存,不得不提早出發另外一次垃圾收集動做。
將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當這一塊的內存用完了,就將存活着的對象複製到另外一塊上面,而後再把已經使用過的內存空間一次清理掉。
複製算法使得每次都是針對其中的一塊進行內存回收,內存分配時也不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。
將內存縮小爲原來的一半。在對象存活率較高時,須要執行較多的複製操做,效率會變低。
商業的虛擬機都採用複製算法來回收新生代。由於新生代中的對象容易死亡,因此並不須要按照1:1的比例劃份內存空間,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間。每次使用 Eden 和其中的一塊 Survivor。
當回收時,將 Eden 和 Survivor 中還存活的對象一次性拷貝到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。Hotspot 虛擬機默認 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80% + 10%),只有10%的內存是會被「浪費」的。
標記過程仍然與「標記-清除」算法同樣,但不是直接對可回收對象進行清理,而是讓全部存活的對象向一端移動,而後直接清理掉邊界之外的內存。
根據對象的存活週期,將內存劃分爲幾塊。通常是把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色,採用最適當的收集算法。
新生代:每次垃圾收集時會有大批對象死去,只有少許存活,因此選擇複製算法,只須要少許存活對象的複製成本就能夠完成收集。
老年代:對象存活率高、沒有額外空間對它進行分配擔保,必須使用「標記-清理」或「標記-整理」算法進行回收。
Minor GC:新生代 GC,指發生在新生代的垃圾收集動做,由於 Java 對象大多死亡頻繁,因此 Minor GC 很是頻繁,通常回收速度較快。
Full GC:老年代 GC,也叫 Major GC,速度通常比 Minor GC 慢 10 倍以上。
對於一個大型的系統,當建立的對象及方法變量比較多時,即堆內存中的對象比較多,若是逐一分析對象是否該回收,效率很低。分區是爲了進行模塊化管理,管理不一樣的對象及變量,以提升 JVM 的執行效率。
Young Generation Space 新生區(也稱新生代)
Tenure Generation Space養老區(也稱舊生代)
Permanent Space 永久存儲區
對象優先分配在 Eden
大對象直接進入老年代
長期存活的對象將進入老年代
動態對象年齡斷定
主要用來存儲新建立的對象,內存較小,垃圾回收頻繁。這個區又分爲三個區域:一個 Eden Space 和兩個 Survivor Space。
當對象在堆建立時,將進入年輕代的Eden Space。
垃圾回收器進行垃圾回收時,掃描Eden Space和A Suvivor Space,若是對象仍然存活,則複製到B Suvivor Space,若是B Suvivor Space已經滿,則複製 Old Gen
掃描A Suvivor Space時,若是對象已經通過了幾回的掃描仍然存活,JVM認爲其爲一個Old對象,則將其移到Old Gen。
掃描完畢後,JVM將Eden Space和A Suvivor Space清空,而後交換A和B的角色(即下次垃圾回收時會掃描Eden Space和B Suvivor Space。
主要用來存儲長時間被引用的對象。它裏面存放的是通過幾回在 Young Generation Space 進行掃描判斷過仍存活的對象,內存較大,垃圾回收頻率較小。
存儲不變的類定義、字節碼和常量等。
Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目間沒有任何分隔符。當遇到8位字節以上空間的數據項時,則會按照高位在前的方式分隔成若干個8位字節進行存儲。
每一個Class文件的頭4個字節稱爲魔數(Magic Number),它的惟一做用是用於肯定這個文件是否爲一個能被虛擬機接受的Class文件。OxCAFEBABE。
接下來是Class文件的版本號:第5,6字節是次版本號(Minor Version),第7,8字節是主版本號(Major Version)。
使用JDK 1.7編譯輸出Class文件,格式代碼爲:
前四個字節爲魔數,次版本號是0x0000,主版本號是0x0033,說明本文件是能夠被1.7及以上版本的虛擬機執行的文件。
33:JDK1.7
32:JDK1.6
31:JDK1.5
30:JDK1.4
2F:JDK1.3
類加載器實現類的加載動做,同時用於肯定一個類。對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性。即便兩個類來源於同一個Class文件,只要加載它們的類加載器不一樣,這兩個類就不相等。
啓動類加載器(Bootstrap ClassLoader):使用C++實現(僅限於HotSpot),是虛擬機自身的一部分。負責將存放在\lib目錄中的類庫加載到虛擬機中。其沒法被Java程序直接引用。
擴展類加載器(Extention ClassLoader)由ExtClassLoader實現,負責加載\lib\ext目錄中的全部類庫,開發者能夠直接使用。
應用程序類加載器(Application ClassLoader):由APPClassLoader實現。負責加載用戶類路徑(ClassPath)上所指定的類庫。
雙親委派模型(Parents Delegation Model)要求除了頂層的啓動類加載器外,其他加載器都應當有本身的父類加載器。類加載器之間的父子關係,經過組合關係複用。
工做過程:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每一個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有到父加載器反饋本身沒法完成這個加載請求(它的搜索範圍沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
Java類隨着它的類加載器一塊兒具有了一種帶優先級的層次關係。好比java.lang.Object,它存放在rt.jar中,不管哪一個類加載器要加載這個類,最終都是委派給啓動類加載器進行加載,所以Object類在程序的各個類加載器環境中,都是同一個類。
若是沒有使用雙親委派模型,讓各個類加載器本身去加載,那麼Java類型體系中最基礎的行爲也得不到保障,應用程序會變得一片混亂。
Class文件描述的各類信息,都須要加載到虛擬機後才能運行。虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
這兩種機器都有代碼執行的能力,可是:
物理機的執行引擎是直接創建在處理器、硬件、指令集和操做系統層面的。
虛擬機的執行引擎是本身實現的,所以能夠自行制定指令集和執行引擎的結構體系,而且可以執行那些不被硬件直接支持的指令集格式。
棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構, 存儲了方法的
局部變量表
操做數棧
動態鏈接
方法返回地址
每個方法從調用開始到執行完成的過程,就對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。
方法調用惟一的任務是肯定被調用方法的版本(調用哪一個方法),暫時還不涉及方法內部的具體運行過程。
Class文件的編譯過程不包含傳統編譯的鏈接步驟,一切方法調用在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存佈局中的入口地址。這使得Java有強大的動態擴展能力,但使Java方法的調用過程變得相對複雜,須要在類加載期間甚至到運行時才能肯定目標方法的直接引用。
invokestatic:調用靜態方法
invokespecial:調用實例構造器方法、私有方法和父類方法
invokevirtual:調用全部的虛方法
invokeinterface:調用接口方法
解釋執行(經過解釋器執行)
編譯執行(經過即時編譯器產生本地代碼)
當主流的虛擬機中都包含了即時編譯器後,Class文件中的代碼到底會被解釋執行仍是編譯執行,只有虛擬機本身才能準確判斷。
Javac編譯器完成了程序代碼通過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性的字節碼指令流的過程。由於這一動做是在Java虛擬機以外進行的,而解釋器在虛擬機的內部,因此Java程序的編譯是半獨立的實現。
Java編譯器輸出的指令流,裏面的指令大部分都是零地址指令,它們依賴操做數棧進行工做。
計算「1+1=2」,基於棧的指令集是這樣的:
iconst_1 iconst_1 iadd istore_0
兩條iconst_1指令連續地把兩個常量1壓入棧中,iadd指令把棧頂的兩個值出棧相加,把結果放回棧頂,最後istore_0把棧頂的值放到局部變量表的第0個Slot中。
最典型的是x86的地址指令集,依賴寄存器工做。
計算「1+1=2」,基於寄存器的指令集是這樣的:
mov eax, 1 add eax, 1
mov指令把EAX寄存器的值設爲1,而後add指令再把這個值加1,結果就保存在EAX寄存器裏。
優勢:
可移植性好:用戶程序不會直接用到這些寄存器,由虛擬機自行決定把一些訪問最頻繁的數據(程序計數器、棧頂緩存)放到寄存器以獲取更好的性能。
代碼相對緊湊:字節碼中每一個字節就對應一條指令
編譯器實現簡單:不須要考慮空間分配問題,所需空間都在棧上操做
缺點:
執行速度稍慢
完成相同功能所需的指令熟練多
頻繁的訪問棧,意味着頻繁的訪問內存,相對於處理器,內存纔是執行速度的瓶頸。
解析與填充符號表
插入式註解處理器的註解處理
分析與字節碼生成
Java程序最初是經過解釋器進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁,就會把這些代碼認定爲「熱點代碼」(Hot Spot Code)。
爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各類層次的優化,完成這個任務的編譯器成爲即時編譯器(Just In Time Compiler,JIT編譯器)。
許多主流的商用虛擬機,都同時包含解釋器和編譯器。
當程序須要快速啓動和執行時,解釋器首先發揮做用,省去編譯的時間,當即執行。
當程序運行後,隨着時間的推移,編譯器逐漸發揮做用,把愈來愈多的代碼編譯成本地代碼,能夠提升執行效率。
若是內存資源限制較大(部分嵌入式系統),可使用解釋執行節約內存,反之可使用編譯執行來提高效率。同時編譯器的代碼還能退回成解釋器的代碼。
由於即時編譯器編譯本地代碼須要佔用程序運行時間,要編譯出優化程度更高的代碼,所花費的時間越長。
分層編譯根據編譯器編譯、優化的規模和耗時,劃分不一樣的編譯層次,包括:
第0層:程序解釋執行,解釋器不開啓性能監控功能,可出發第1層編譯。
第1層:也成爲C1編譯,將字節碼編譯爲本地代碼,進行簡單可靠的優化,若有必要加入性能監控的邏輯。
第2層:也成爲C2編譯,也是將字節碼編譯爲本地代碼,可是會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
用Client Compiler和Server Compiler將會同時工做。用Client Compiler獲取更高的編譯速度,用Server Compiler獲取更好的編譯質量。
被屢次調用的方法
被屢次執行的循環體
要知道一段代碼是否是熱點代碼,是否是須要觸發即時編譯,這個行爲稱爲熱點探測。主要有兩種方法:
基於採樣的熱點探測,虛擬機週期性檢查各個線程的棧頂,若是發現某個方法常常出如今棧頂,那這個方法就是「熱點方法」。實現簡單高效,可是很難精確確認一個方法的熱度。
基於計數器的熱點探測,虛擬機會爲每一個方法創建計數器,統計方法的執行次數,若是執行次數超過必定的閾值,就認爲它是熱點方法。
方法調用計數器
回邊計數器(判斷循環代碼)
統計的是一個相對的執行頻率,即一段時間內方法被調用的次數。當超過必定的時間限度,若是方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減小一半,這個過程稱爲方法調用計數器的熱度衰減,這個時間就被稱爲半衰週期。
語言無關的經典優化技術之一:公共子表達式消除
語言相關的經典優化技術之一:數組範圍檢查消除
最重要的優化技術之一:方法內聯
最前沿的優化技術之一:逃逸分析
廣泛應用於各類編譯器的經典優化技術,它的含義是:
若是一個表達式E已經被計算過了,而且從先前的計算到如今E中全部變量的值都沒有發生變化,那麼E的此次出現就成了公共子表達式。沒有必要從新計算,直接用結果代替E就能夠了。
由於Java會自動檢查數組越界,每次數組元素的讀寫都帶有一次隱含的條件斷定操做,對於擁有大量數組訪問的程序代碼,這無疑是一種性能負擔。
若是數組訪問發生在循環之中,而且使用循環變量來進行數組訪問,若是編譯器只要經過數據流分析就能夠斷定循環變量的取值範圍永遠在數組區間內,那麼整個循環中就能夠把數組的上下界檢查消除掉,能夠節省不少次的條件判斷操做。
內聯消除了方法調用的成本,還爲其餘優化手段創建良好的基礎。
編譯器在進行內聯時,若是是非虛方法,那麼直接內聯。若是遇到虛方法,則會查詢當前程序下是否有多個目標版本可供選擇,若是查詢結果只有一個版本,那麼也能夠內聯,不過這種內聯屬於激進優化,須要預留一個逃生門(Guard條件不成立時的Slow Path),稱爲守護內聯。
若是程序的後續執行過程當中,虛擬機一直沒有加載到會令這個方法的接受者的繼承關係發現變化的類,那麼內聯優化的代碼能夠一直使用。不然須要拋棄掉已經編譯的代碼,退回到解釋狀態執行,或者從新進行編譯。
逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法裏面被定義後,它可能被外部方法所引用,這種行爲被稱爲方法逃逸。被外部線程訪問到,被稱爲線程逃逸。
棧上分配:通常對象都是分配在Java堆中的,對於各個線程都是共享和可見的,只要持有這個對象的引用,就能夠訪問堆中存儲的對象數據。可是垃圾回收和整理都會耗時,若是一個對象不會逃逸出方法,可讓這個對象在棧上分配內存,對象所佔用的內存空間就能夠隨着棧幀出棧而銷燬。若是能使用棧上分配,那大量的對象會隨着方法的結束而自動銷燬,垃圾回收的壓力會小不少。
同步消除:線程同步自己就是很耗時的過程。若是逃逸分析能肯定一個變量不會逃逸出線程,那這個變量的讀寫確定就不會有競爭,同步措施就能夠消除掉。
標量替換:不建立這個對象,直接建立它的若干個被這個方法使用到的成員變量來替換。
運算任務,除了須要處理器計算以外,還須要與內存交互,如讀取運算數據、存儲運算結果等(不能僅靠寄存器來解決)。
計算機的存儲設備和處理器的運算速度差了幾個數量級,因此不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache),做爲內存與處理器之間的緩衝:將運算須要的數據複製到緩存中,讓運算快速運行。當運算結束後再從緩存同步回內存,這樣處理器就無需等待緩慢的內存讀寫了。
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是引入了一個新的問題:緩存一致性。在多處理器系統中,每一個處理器都有本身的高速緩存,它們又共享同一主內存。當多個處理器的運算任務都涉及同一塊主內存時,可能致使各自的緩存數據不一致。
爲了解決一致性的問題,須要各個處理器訪問緩存時遵循緩存一致性協議。同時爲了使得處理器充分被利用,處理器可能會對輸出代碼進行亂序執行優化。Java虛擬機的即時編譯器也有相似的指令重排序優化。
Java虛擬機的規範,用來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各個平臺下都能達到一致的併發效果。
定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出這樣的底層細節。此處的變量包括實例字段、靜態字段和構成數組對象的元素,可是不包括局部變量和方法參數,由於這些是線程私有的,不會被共享,因此不存在競爭問題。
因此的變量都存儲在主內存,每條線程還有本身的工做內存,保存了被該線程使用到的變量的主內存副本拷貝。線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,不能直接讀寫主內存的變量。不一樣的線程之間也沒法直接訪問對方工做內存的變量,線程間變量值的傳遞須要經過主內存。
一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存,Java內存模型定義了8種操做:
原子性:對基本數據類型的訪問和讀寫是具有原子性的。對於更大範圍的原子性保證,可使用字節碼指令monitorenter和monitorexit來隱式使用lock和unlock操做。這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字。所以synchronized塊之間的操做也具備原子性。
可見性:當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取以前從主內存刷新變量值來實現可見性的。volatile的特殊規則保證了新值可以當即同步到主內存,每次使用前當即從主內存刷新。synchronized和final也能實現可見性。final修飾的字段在構造器中一旦被初始化完成,而且構造器沒有把this的引用傳遞出去,那麼其餘線程中就能看見final字段的值。
有序性:Java程序的有序性能夠總結爲一句話,若是在本線程內觀察,全部的操做都是有序的(線程內表現爲串行的語義);若是在一個線程中觀察另外一個線程,全部的操做都是無序的(指令重排序和工做內存與主內存同步延遲線性)。
關鍵字volatile是Java虛擬機提供的最輕量級的同步機制。當一個變量被定義成volatile以後,具有兩種特性:
保證此變量對全部線程的可見性。當一條線程修改了這個變量的值,新值對於其餘線程是能夠當即得知的。而普通變量作不到這一點。
禁止指令重排序優化。普通變量僅僅能保證在該方法執行過程當中,獲得正確結果,可是不保證程序代碼的執行順序。
volatile變量在各個線程的工做內存,不存在一致性問題(各個線程的工做內存中volatile變量,每次使用前都要刷新到主內存)。可是Java裏面的運算並不是原子操做,致使volatile變量的運算在併發下同樣是不安全的。
在某些狀況下,volatile同步機制的性能要優於鎖(synchronized關鍵字),可是因爲虛擬機對鎖實行的許多消除和優化,因此並非很快。
volatile變量讀操做的性能消耗與普通變量幾乎沒有差異,可是寫操做則可能慢一些,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
併發不必定要依賴多線程,PHP中有多進程併發。可是Java裏面的併發是多線程的。
線程是比進程更輕量級的調度執行單位。線程能夠把一個進程的資源分配和執行調度分開,各個線程既能夠共享進程資源(內存地址、文件I/O),又能夠獨立調度(線程是CPU調度的最基本單位)。
使用內核線程實現
使用用戶線程實現
使用用戶線程+輕量級進程混合實現
操做系統支持怎樣的線程模型,在很大程度上就決定了Java虛擬機的線程是怎樣映射的。
線程調度是系統爲線程分配處理器使用權的過程。
協同式線程調度:實現簡單,沒有線程同步的問題。可是線程執行時間不可控,容易系統崩潰。
搶佔式線程調度:每一個線程由系統來分配執行時間,不會有線程致使整個進程阻塞的問題。
雖然Java線程調度是系統自動完成的,可是咱們能夠建議系統給某些線程多分配點時間——設置線程優先級。Java語言有10個級別的線程優先級,優先級越高的線程,越容易被系統選擇執行。
可是並不能徹底依靠線程優先級。由於Java的線程是被映射到系統的原生線程上,因此線程調度最終仍是由操做系統說了算。如Windows中只有7種優先級,因此Java不得不出現幾個優先級相同的狀況。同時優先級可能會被系統自行改變。Windows系統中存在一個「優先級推動器」,當系統發現一個線程執行特別勤奮,可能會越過線程優先級爲它分配執行時間。
當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方法進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象就是線程安全的。
不可變
絕對線程安全
相對線程安全
線程兼容
線程對立
在Java語言裏,不可變的對象必定是線程安全的,只要一個不可變的對象被正確構建出來,那其外部的可見狀態永遠也不會改變,永遠也不會在多個線程中處於不一致的狀態。
虛擬機提供了同步和鎖機制。
阻塞同步(互斥同步)
非阻塞同步
互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式。Java中最基本的同步手段就是synchronized關鍵字,其編譯後會在同步塊的先後分別造成monitorenter和monitorexit兩個字節碼指令。這兩個字節碼都須要一個Reference類型的參數指明要鎖定和解鎖的對象。若是Java程序中的synchronized明確指定了對象參數,那麼這個對象就是Reference;若是沒有明確指定,那就根據synchronized修飾的是實例方法仍是類方法,去獲取對應的對象實例或Class對象做爲鎖對象。
在執行monitorenter指令時,首先要嘗試獲取對象的鎖。
若是這個對象沒有鎖定,或者當前線程已經擁有了這個對象的鎖,把鎖的計數器+1;當執行monitorexit指令時將鎖計數器-1。當計數器爲0時,鎖就被釋放了。
若是獲取對象失敗了,那當前線程就要阻塞等待,知道對象鎖被另一個線程釋放爲止。
除了synchronized以外,還可使用java.util.concurrent包中的重入鎖(ReentrantLock)來實現同步。ReentrantLock比synchronized增長了高級功能:等待可中斷、可實現公平鎖、鎖能夠綁定多個條件。
等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,對處理執行時間很是長的同步塊頗有用。
公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。synchronized中的鎖是非公平的。
互斥同步最大的問題,就是進行線程阻塞和喚醒所帶來的性能問題,是一種悲觀的併發策略。老是認爲只要不去作正確的同步措施(加鎖),那就確定會出問題,不管共享數據是否真的會出現競爭,它都要進行加鎖、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程須要被喚醒等操做。
隨着硬件指令集的發展,咱們可使用基於衝突檢測的樂觀併發策略。先進行操做,若是沒有其餘線程徵用數據,那操做就成功了;若是共享數據有徵用,產生了衝突,那就再進行其餘的補償措施。這種樂觀的併發策略的許多實現不須要線程掛起,因此被稱爲非阻塞同步。
JDK1.6的一個重要主題,就是高效併發。HotSpot虛擬機開發團隊在這個版本上,實現了各類鎖優化:
適應性自旋
鎖消除
鎖粗化
輕量級鎖
偏向鎖
互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操做都須要轉入內核態中完成,這些操做給系統的併發性帶來很大壓力。同時不少應用共享數據的鎖定狀態,只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。先不掛起線程,等一下子。
若是物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,讓後面請求鎖的線程稍等一會,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放。爲了讓線程等待,咱們只需讓線程執行一個忙循環(自旋)。
自旋等待自己雖然避免了線程切換的開銷,但它要佔用處理器時間。因此若是鎖被佔用的時間很短,自旋等待的效果就很是好;若是時間很長,那麼自旋的線程只會白白消耗處理器的資源。因此自旋等待的時間要有必定的限度,若是自旋超過了限定的次數仍然沒有成功得到鎖,那就應該使用傳統的方式掛起線程了。
自旋的時間不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
若是一個鎖對象,自旋等待剛剛成功得到鎖,而且持有鎖的線程正在運行,那麼虛擬機認爲此次自旋仍然可能成功,進而運行自旋等待更長的時間。
若是對於某個鎖,自旋不多成功,那在之後要獲取這個鎖,可能省略掉自旋過程,以避免浪費處理器資源。
有了自適應自旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的情況預測就會愈來愈準確,虛擬機也會愈來愈聰明。
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被檢測到不可能存在共享數據競爭的鎖進行消除。主要根據逃逸分析。
程序員怎麼會在明知道不存在數據競爭的狀況下使用同步呢?不少不是程序員本身加入的。
原則上,同步塊的做用範圍要儘可能小。可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做在循環體內,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。
鎖粗化就是增大鎖的做用域。
在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。
消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。即在無競爭的狀況下,把整個同步都消除掉。這個鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要同步。