「JVM」關(guān)于JVM,你需要掌握這些
最近,一直有小伙伴讓我整理下關(guān)于JVM的知識,經(jīng)過十幾天的收集與整理,初版算是整理出來了。希望對大家有所幫助。
JDK 是什么?
JDK 是用于支持 Java 程序開發(fā)的最小環(huán)境。
- Java 程序設(shè)計語言
- Java 虛擬機
- Java API類庫
JRE 是什么?
JRE 是支持 Java 程序運行的標(biāo)準(zhǔn)環(huán)境。
Java SE API 子集
Java 虛擬機
Java歷史版本的特性?
Java Version SE 5.0
- 引入泛型;
- 增強循環(huán),可以使用迭代方式;
- 自動裝箱與自動拆箱;
- 類型安全的枚舉;
- 可變參數(shù);
- 靜態(tài)引入;
- 元數(shù)據(jù)(注解);
- 引入Instrumentation。
Java Version SE 6
- 支持腳本語言;
- 引入JDBC 4.0 API;
- 引入Java Compiler API;
- 可插拔注解;
- 增加對Native PKI(Public Key Infrastructure)、Java GSS(Generic Security Service)、Kerberos和LDAP(Lightweight Directory Access Protocol)的支持;
- 繼承Web Services;
- 做了很多優(yōu)化。
Java Version SE 7
- switch語句塊中允許以字符串作為分支條件;
- 在創(chuàng)建泛型對象時應(yīng)用類型推斷;
- 在一個語句塊中捕獲多種異常;
- 支持動態(tài)語言;
- 支持try-with-resources;
- 引入Java NIO.2開發(fā)包;
- 數(shù)值類型可以用2進制字符串表示,并且可以在字符串表示中添加下劃線;
- 鉆石型語法;
- null值的自動處理。
Java 8
- 函數(shù)式接口
- Lambda表達式
- Stream API
- 接口的增強
- 時間日期增強API
- 重復(fù)注解與類型注解
- 默認(rèn)方法與靜態(tài)方法
- Optional 容器類
運行時數(shù)據(jù)區(qū)域包括哪些?
- 程序計數(shù)器
- Java 虛擬機棧
- 本地方法棧
- Java 堆
- 方法區(qū)
- 運行時常量池
- 直接內(nèi)存
程序計數(shù)器(線程私有)
程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行字節(jié)碼的行號指示器。分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器完成。
由于 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式實現(xiàn)的。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個獨立的程序計數(shù)器,各線程之間的計數(shù)器互不影響,獨立存儲。
如果線程正在執(zhí)行的是一個 Java 方法,計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;
如果正在執(zhí)行的是 Native 方法,這個計數(shù)器的值為空。
程序計數(shù)器是唯一一個沒有規(guī)定任何 OutOfMemoryError 的區(qū)域。
Java 虛擬機棧(線程私有)
Java 虛擬機棧(Java Virtual Machine Stacks)是線程私有的,生命周期與線程相同。 虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會創(chuàng)建一個棧幀(Stack Frame),存儲
- 局部變量表
- 操作棧
- 動態(tài)鏈接
- 方法出口
每一個方法被調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
這個區(qū)域有兩種異常情況:
- StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度
- OutOfMemoryError:虛擬機棧擴展到無法申請足夠的內(nèi)存時
本地方法棧(線程私有)
虛擬機棧為虛擬機執(zhí)行 Java 方法(字節(jié)碼)服務(wù)。
本地方法棧(Native Method Stacks)為虛擬機使用到的 Native 方法服務(wù)。
Java 堆(線程共享)
Java 堆(Java Heap)是 Java 虛擬機中內(nèi)存最大的一塊。Java 堆在虛擬機啟動時創(chuàng)建,被所有線程共享。
作用:存放對象實例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上可以不連續(xù),只要邏輯上連續(xù)即可。
方法區(qū)(線程共享)
方法區(qū)(Method Area)被所有線程共享,用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
和 Java 堆一樣,不需要連續(xù)的內(nèi)存,可以選擇固定的大小,更可以選擇不實現(xiàn)垃圾收集。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。保存 Class 文件中的符號引用、翻譯出來的直接引用。運行時常量池可以在運行期間將新的常量放入池中。
Java 中對象訪問是如何進行的?
對于上述最簡單的訪問,也會涉及到 Java 棧、Java 堆、方法區(qū)這三個最重要內(nèi)存區(qū)域。
如果出現(xiàn)在方法體中,則上述代碼會反映到 Java 棧的本地變量表中,作為 reference 類型數(shù)據(jù)出現(xiàn)。
反映到 Java 堆中,形成一塊存儲了 Object 類型所有對象實例數(shù)據(jù)值的內(nèi)存。Java堆中還包含對象類型數(shù)據(jù)的地址信息,這些類型數(shù)據(jù)存儲在方法區(qū)中。
如何判斷對象是否“死去”?
- 引用計數(shù)法
- 根搜索算法
什么是引用計數(shù)法?
給對象添加一個引用計數(shù)器,每當(dāng)有一個地方引用它,計數(shù)器就+1,;當(dāng)引用失效時,計數(shù)器就-1;任何時刻計數(shù)器都為0的對象就是不能再被使用的。
引用計數(shù)法的缺點?
很難解決對象之間的循環(huán)引用問題。
什么是根搜索算法?
通過一系列的名為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可用的。
Java 的4種引用方式?
在 JDK 1.2 之后,Java 對引用的概念進行了擴充,將引用分為
- 強引用 Strong Reference
- 軟引用 Soft Reference
- 弱引用 Weak Reference
- 虛引用 Phantom Reference
強引用
代碼中普遍存在的,像上述的引用。只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用
用來描述一些還有用,但并非必須的對象。軟引用所關(guān)聯(lián)的對象,有在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍,并進行第二次回收。如果這次回收還是沒有足夠的內(nèi)存,才會拋出內(nèi)存異常。提供了 SoftReference 類實現(xiàn)軟引用。
弱引用
描述非必須的對象,強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象,只能生存到下一次垃圾收集發(fā)生前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。提供了 WeakReference 類來實現(xiàn)弱引用。
虛引用
一個對象是否有虛引用,完全不會對其生存時間夠成影響,也無法通過虛引用來取得一個對象實例。為一個對象關(guān)聯(lián)虛引用的唯一目的,就是希望在這個對象被收集器回收時,收到一個系統(tǒng)通知。提供了 PhantomReference 類來實現(xiàn)虛引用。
有哪些垃圾收集算法?
- 標(biāo)記-清除算法
- 復(fù)制算法
- 標(biāo)記-整理算法
- 分代收集算法
標(biāo)記-清除算法(Mark-Sweep)
什么是標(biāo)記-清除算法?
分為標(biāo)記和清除兩個階段。首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收被標(biāo)記的對象。
有什么缺點?
效率問題:標(biāo)記和清除過程的效率都不高。
空間問題:標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能導(dǎo)致,程序分配較大對象時無法找到足夠的連續(xù)內(nèi)存,不得不提前出發(fā)另一次垃圾收集動作。
復(fù)制算法(Copying)- 新生代
將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊。當(dāng)這一塊的內(nèi)存用完了,就將存活著的對象復(fù)制到另一塊上面,然后再把已經(jīng)使用過的內(nèi)存空間一次清理掉。
優(yōu)點
復(fù)制算法使得每次都是針對其中的一塊進行內(nèi)存回收,內(nèi)存分配時也不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。
缺點
將內(nèi)存縮小為原來的一半。在對象存活率較高時,需要執(zhí)行較多的復(fù)制操作,效率會變低。
應(yīng)用
商業(yè)的虛擬機都采用復(fù)制算法來回收新生代。因為新生代中的對象容易死亡,所以并不需要按照1:1的比例劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間。每次使用 Eden 和其中的一塊 Survivor。
當(dāng)回收時,將 Eden 和 Survivor 中還存活的對象一次性拷貝到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。Hotspot 虛擬機默認(rèn) Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90%(80% + 10%),只有10%的內(nèi)存是會被“浪費”的。
標(biāo)記-整理算法(Mark-Compact)-老年代
標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但不是直接對可回收對象進行清理,而是讓所有存活的對象向一端移動,然后直接清理掉邊界以外的內(nèi)存。
分代收集算法
根據(jù)對象的存活周期,將內(nèi)存劃分為幾塊。一般是把 Java 堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點,采用最適當(dāng)?shù)氖占惴ā?/p>
- 新生代:每次垃圾收集時會有大批對象死去,只有少量存活,所以選擇復(fù)制算法,只需要少量存活對象的復(fù)制成本就可以完成收集。
- 老年代:對象存活率高、沒有額外空間對它進行分配擔(dān)保,必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法進行回收。
Minor GC 和 Full GC有什么區(qū)別?
Minor GC:新生代 GC,指發(fā)生在新生代的垃圾收集動作,因為 Java 對象大多死亡頻繁,所以 Minor GC 非常頻繁,一般回收速度較快。 Full GC:老年代 GC,也叫 Major GC,速度一般比 Minor GC 慢 10 倍以上。
Java 內(nèi)存
為什么要將堆內(nèi)存分區(qū)?
對于一個大型的系統(tǒng),當(dāng)創(chuàng)建的對象及方法變量比較多時,即堆內(nèi)存中的對象比較多,如果逐一分析對象是否該回收,效率很低。分區(qū)是為了進行模塊化管理,管理不同的對象及變量,以提高 JVM 的執(zhí)行效率。
堆內(nèi)存分為哪幾塊?
- Young Generation Space 新生區(qū)(也稱新生代)
- Tenure Generation Space養(yǎng)老區(qū)(也稱舊生代)
- Permanent Space 永久存儲區(qū)
分代收集算法
內(nèi)存分配有哪些原則?
- 對象優(yōu)先分配在 Eden
- 大對象直接進入老年代
- 長期存活的對象將進入老年代
- 動態(tài)對象年齡判定
- 空間分配擔(dān)保
Young Generation Space (采用復(fù)制算法)
主要用來存儲新創(chuàng)建的對象,內(nèi)存較小,垃圾回收頻繁。這個區(qū)又分為三個區(qū)域:一個 Eden Space 和兩個 Survivor Space。
- 當(dāng)對象在堆創(chuàng)建時,將進入年輕代的Eden Space。
- 垃圾回收器進行垃圾回收時,掃描Eden Space和A Suvivor Space,如果對象仍然存活,則復(fù)制到B Suvivor Space,如果B Suvivor Space已經(jīng)滿,則復(fù)制 Old Gen
- 掃描A Suvivor Space時,如果對象已經(jīng)經(jīng)過了幾次的掃描仍然存活,JVM認(rèn)為其為一個Old對象,則將其移到Old Gen。
- 掃描完畢后,JVM將Eden Space和A Suvivor Space清空,然后交換A和B的角色(即下次垃圾回收時會掃描Eden Space和B Suvivor Space。
Tenure Generation Space(采用標(biāo)記-整理算法)
主要用來存儲長時間被引用的對象。它里面存放的是經(jīng)過幾次在 Young Generation Space 進行掃描判斷過仍存活的對象,內(nèi)存較大,垃圾回收頻率較小。
Permanent Space
存儲不變的類定義、字節(jié)碼和常量等。
Class文件
Java虛擬機的平臺無關(guān)性
Class文件的組成?
Class文件是一組以8位字節(jié)為基礎(chǔ)單位的二進制流,各個數(shù)據(jù)項目間沒有任何分隔符。當(dāng)遇到8位字節(jié)以上空間的數(shù)據(jù)項時,則會按照高位在前的方式分隔成若干個8位字節(jié)進行存儲。
魔數(shù)與Class文件的版本
每個Class文件的頭4個字節(jié)稱為魔數(shù)(Magic Number),它的唯一作用是用于確定這個文件是否為一個能被虛擬機接受的Class文件。OxCAFEBABE。
接下來是Class文件的版本號:第5,6字節(jié)是次版本號(Minor Version),第7,8字節(jié)是主版本號(Major Version)。
使用JDK 1.7編譯輸出Class文件,格式代碼為:
前四個字節(jié)為魔數(shù),次版本號是0x0000,主版本號是0x0033,說明本文件是可以被1.7及以上版本的虛擬機執(zhí)行的文件。
- 33:JDK1.7
- 32:JDK1.6
- 31:JDK1.5
- 30:JDK1.4
- 2F:JDK1.3
類加載器
類加載器的作用是什么?
類加載器實現(xiàn)類的加載動作,同時用于確定一個類。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。即使兩個類來源于同一個Class文件,只要加載它們的類加載器不同,這兩個類就不相等。
類加載器有哪些?
啟動類加載器(Bootstrap ClassLoader):使用C++實現(xiàn)(僅限于HotSpot),是虛擬機自身的一部分。負(fù)責(zé)將存放在\lib目錄中的類庫加載到虛擬機中。其無法被Java程序直接引用。
擴展類加載器(Extention ClassLoader)由ExtClassLoader實現(xiàn),負(fù)責(zé)加載\lib\ext目錄中的所有類庫,開發(fā)者可以直接使用。
應(yīng)用程序類加載器(Application ClassLoader):由APPClassLoader實現(xiàn)。負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫。
類加載機制
什么是雙親委派模型?
雙親委派模型(Parents Delegation Model)要求除了頂層的啟動類加載器外,其余加載器都應(yīng)當(dāng)有自己的父類加載器。類加載器之間的父子關(guān)系,通過組合關(guān)系復(fù)用。 工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有到父加載器反饋自己無法完成這個加載請求(它的搜索范圍沒有找到所需的類)時,子加載器才會嘗試自己去加載。
為什么要使用雙親委派模型,組織類加載器之間的關(guān)系?
Java類隨著它的類加載器一起具備了一種帶優(yōu)先級的層次關(guān)系。比如java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都是委派給啟動類加載器進行加載,因此Object類在程序的各個類加載器環(huán)境中,都是同一個類。
如果沒有使用雙親委派模型,讓各個類加載器自己去加載,那么Java類型體系中最基礎(chǔ)的行為也得不到保障,應(yīng)用程序會變得一片混亂。
什么是類加載機制?
Class文件描述的各種信息,都需要加載到虛擬機后才能運行。虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
虛擬機和物理機的區(qū)別是什么?
這兩種機器都有代碼執(zhí)行的能力,但是:
物理機的執(zhí)行引擎是直接建立在處理器、硬件、指令集和操作系統(tǒng)層面的。
虛擬機的執(zhí)行引擎是自己實現(xiàn)的,因此可以自行制定指令集和執(zhí)行引擎的結(jié)構(gòu)體系,并且能夠執(zhí)行那些不被硬件直接支持的指令集格式。
運行時棧幀結(jié)構(gòu)
棧幀是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu), 存儲了方法的
- 局部變量表
- 操作數(shù)棧
- 動態(tài)連接
- 方法返回地址
每一個方法從調(diào)用開始到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧里面從入棧到出棧的過程。
Java 方法調(diào)用
什么是方法調(diào)用?
方法調(diào)用唯一的任務(wù)是確定被調(diào)用方法的版本(調(diào)用哪個方法),暫時還不涉及方法內(nèi)部的具體運行過程。
Java的方法調(diào)用,有什么特殊之處?
Class文件的編譯過程不包含傳統(tǒng)編譯的連接步驟,一切方法調(diào)用在Class文件里面存儲的都只是符號引用,而不是方法在實際運行時內(nèi)存布局中的入口地址。這使得Java有強大的動態(tài)擴展能力,但使Java方法的調(diào)用過程變得相對復(fù)雜,需要在類加載期間甚至到運行時才能確定目標(biāo)方法的直接引用。
Java虛擬機調(diào)用字節(jié)碼指令有哪些?
- invokestatic:調(diào)用靜態(tài)方法
- invokespecial:調(diào)用實例構(gòu)造器方法、私有方法和父類方法
- invokevirtual:調(diào)用所有的虛方法
- invokeinterface:調(diào)用接口方法
虛擬機是如何執(zhí)行方法里面的字節(jié)碼指令的?
解釋執(zhí)行(通過解釋器執(zhí)行) 編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼)
解釋執(zhí)行
當(dāng)主流的虛擬機中都包含了即時編譯器后,Class文件中的代碼到底會被解釋執(zhí)行還是編譯執(zhí)行,只有虛擬機自己才能準(zhǔn)確判斷。
Javac編譯器完成了程序代碼經(jīng)過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性的字節(jié)碼指令流的過程。因為這一動作是在Java虛擬機之外進行的,而解釋器在虛擬機的內(nèi)部,所以Java程序的編譯是半獨立的實現(xiàn)。
基于棧的指令集和基于寄存器的指令集
什么是基于棧的指令集?
Java編譯器輸出的指令流,里面的指令大部分都是零地址指令,它們依賴操作數(shù)棧進行工作。
計算“1+1=2”,基于棧的指令集是這樣的:
兩條iconst_1指令連續(xù)地把兩個常量1壓入棧中,iadd指令把棧頂?shù)膬蓚€值出棧相加,把結(jié)果放回棧頂,最后istore_0把棧頂?shù)闹捣诺骄植孔兞勘淼牡?個Slot中。
什么是基于寄存器的指令集?
最典型的是x86的地址指令集,依賴寄存器工作。 計算“1+1=2”,基于寄存器的指令集是這樣的:
mov指令把EAX寄存器的值設(shè)為1,然后add指令再把這個值加1,結(jié)果就保存在EAX寄存器里。
基于棧的指令集的優(yōu)缺點?
「優(yōu)點:」
可移植性好:用戶程序不會直接用到這些寄存器,由虛擬機自行決定把一些訪問最頻繁的數(shù)據(jù)(程序計數(shù)器、棧頂緩存)放到寄存器以獲取更好的性能。
代碼相對緊湊:字節(jié)碼中每個字節(jié)就對應(yīng)一條指令
編譯器實現(xiàn)簡單:不需要考慮空間分配問題,所需空間都在棧上操作
「缺點:」
執(zhí)行速度稍慢
完成相同功能所需的指令熟練多
頻繁的訪問棧,意味著頻繁的訪問內(nèi)存,相對于處理器,內(nèi)存才是執(zhí)行速度的瓶頸。
Javac編譯過程分為哪些步驟?
- 解析與填充符號表
- 插入式注解處理器的注解處理
- 分析與字節(jié)碼生成
什么是即時編譯器?
Java程序最初是通過解釋器進行解釋執(zhí)行的,當(dāng)虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁,就會把這些代碼認(rèn)定為“熱點代碼”(Hot Spot Code)。
為了提高熱點代碼的執(zhí)行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關(guān)的機器碼,并進行各種層次的優(yōu)化,完成這個任務(wù)的編譯器成為即時編譯器(Just In Time Compiler,JIT編譯器)。
解釋器和編譯器
許多主流的商用虛擬機,都同時包含解釋器和編譯器。
當(dāng)程序需要快速啟動和執(zhí)行時,解釋器首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行。
當(dāng)程序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼,可以提高執(zhí)行效率。
如果內(nèi)存資源限制較大(部分嵌入式系統(tǒng)),可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來提升效率。同時編譯器的代碼還能退回成解釋器的代碼。
為什么要采用分層編譯?
因為即時編譯器編譯本地代碼需要占用程序運行時間,要編譯出優(yōu)化程度更高的代碼,所花費的時間越長。
分層編譯器有哪些層次?
分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模和耗時,劃分不同的編譯層次,包括:
第0層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能,可出發(fā)第1層編譯。
第1層:也成為C1編譯,將字節(jié)碼編譯為本地代碼,進行簡單可靠的優(yōu)化,如有必要加入性能監(jiān)控的邏輯。
第2層:也成為C2編譯,也是將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進行一些不可靠的激進優(yōu)化。
用Client Compiler和Server Compiler將會同時工作。用Client Compiler獲取更高的編譯速度,用Server Compiler獲取更好的編譯質(zhì)量。
編譯對象與觸發(fā)條件
熱點代碼有哪些?
- 被多次調(diào)用的方法
- 被多次執(zhí)行的循環(huán)體
如何判斷一段代碼是不是熱點代碼?
要知道一段代碼是不是熱點代碼,是不是需要觸發(fā)即時編譯,這個行為稱為熱點探測。主要有兩種方法:
- 基于采樣的熱點探測,虛擬機周期性檢查各個線程的棧頂,如果發(fā)現(xiàn)某個方法經(jīng)常出現(xiàn)在棧頂,那這個方法就是“熱點方法”。實現(xiàn)簡單高效,但是很難精確確認(rèn)一個方法的熱度。
- 基于計數(shù)器的熱點探測,虛擬機會為每個方法建立計數(shù)器,統(tǒng)計方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值,就認(rèn)為它是熱點方法。
HotSpot虛擬機使用第二種,有兩個計數(shù)器:
- 方法調(diào)用計數(shù)器
- 回邊計數(shù)器(判斷循環(huán)代碼)
方法調(diào)用計數(shù)器統(tǒng)計方法
統(tǒng)計的是一個相對的執(zhí)行頻率,即一段時間內(nèi)方法被調(diào)用的次數(shù)。當(dāng)超過一定的時間限度,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時編譯器編譯,那這個方法的調(diào)用計數(shù)器就會被減少一半,這個過程稱為方法調(diào)用計數(shù)器的熱度衰減,這個時間就被稱為半衰周期。
有哪些經(jīng)典的優(yōu)化技術(shù)(即時編譯器)?
- 語言無關(guān)的經(jīng)典優(yōu)化技術(shù)之一:公共子表達式消除
- 語言相關(guān)的經(jīng)典優(yōu)化技術(shù)之一:數(shù)組范圍檢查消除
- 最重要的優(yōu)化技術(shù)之一:方法內(nèi)聯(lián)
- 最前沿的優(yōu)化技術(shù)之一:逃逸分析
公共子表達式消除
普遍應(yīng)用于各種編譯器的經(jīng)典優(yōu)化技術(shù),它的含義是:
如果一個表達式E已經(jīng)被計算過了,并且從先前的計算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化,那么E的這次出現(xiàn)就成了公共子表達式。沒有必要重新計算,直接用結(jié)果代替E就可以了。
數(shù)組邊界檢查消除
因為Java會自動檢查數(shù)組越界,每次數(shù)組元素的讀寫都帶有一次隱含的條件判定操作,對于擁有大量數(shù)組訪問的程序代碼,這無疑是一種性能負(fù)擔(dān)。
如果數(shù)組訪問發(fā)生在循環(huán)之中,并且使用循環(huán)變量來進行數(shù)組訪問,如果編譯器只要通過數(shù)據(jù)流分析就可以判定循環(huán)變量的取值范圍永遠在數(shù)組區(qū)間內(nèi),那么整個循環(huán)中就可以把數(shù)組的上下界檢查消除掉,可以節(jié)省很多次的條件判斷操作。
方法內(nèi)聯(lián)
內(nèi)聯(lián)消除了方法調(diào)用的成本,還為其他優(yōu)化手段建立良好的基礎(chǔ)。
編譯器在進行內(nèi)聯(lián)時,如果是非虛方法,那么直接內(nèi)聯(lián)。如果遇到虛方法,則會查詢當(dāng)前程序下是否有多個目標(biāo)版本可供選擇,如果查詢結(jié)果只有一個版本,那么也可以內(nèi)聯(lián),不過這種內(nèi)聯(lián)屬于激進優(yōu)化,需要預(yù)留一個逃生門(Guard條件不成立時的Slow Path),稱為守護內(nèi)聯(lián)。
如果程序的后續(xù)執(zhí)行過程中,虛擬機一直沒有加載到會令這個方法的接受者的繼承關(guān)系發(fā)現(xiàn)變化的類,那么內(nèi)聯(lián)優(yōu)化的代碼可以一直使用。否則需要拋棄掉已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新進行編譯。
逃逸分析
逃逸分析的基本行為就是分析對象動態(tài)作用域:當(dāng)一個對象在方法里面被定義后,它可能被外部方法所引用,這種行為被稱為方法逃逸。被外部線程訪問到,被稱為線程逃逸。
如果對象不會逃逸到方法或線程外,可以做什么優(yōu)化?
- 棧上分配:一般對象都是分配在Java堆中的,對于各個線程都是共享和可見的,只要持有這個對象的引用,就可以訪問堆中存儲的對象數(shù)據(jù)。但是垃圾回收和整理都會耗時,如果一個對象不會逃逸出方法,可以讓這個對象在棧上分配內(nèi)存,對象所占用的內(nèi)存空間就可以隨著棧幀出棧而銷毀。如果能使用棧上分配,那大量的對象會隨著方法的結(jié)束而自動銷毀,垃圾回收的壓力會小很多。
- 同步消除:線程同步本身就是很耗時的過程。如果逃逸分析能確定一個變量不會逃逸出線程,那這個變量的讀寫肯定就不會有競爭,同步措施就可以消除掉。
- 標(biāo)量替換:不創(chuàng)建這個對象,直接創(chuàng)建它的若干個被這個方法使用到的成員變量來替換。
Java與C/C++的編譯器對比
- 即時編譯器運行占用的是用戶程序的運行時間,具有很大的時間壓力。
- Java語言雖然沒有virtual關(guān)鍵字,但是使用虛方法的頻率遠大于C++,所以即時編譯器進行優(yōu)化時難度要遠遠大于C++的靜態(tài)優(yōu)化編譯器。
- Java語言是可以動態(tài)擴展的語言,運行時加載新的類可能改變程序類型的繼承關(guān)系,使得全局的優(yōu)化難以進行,因為編譯器無法看見程序的全貌,編譯器不得不時刻注意并隨著類型的變化,而在運行時撤銷或重新進行一些優(yōu)化。
- Java語言對象的內(nèi)存分配是在堆上,只有方法的局部變量才能在棧上分配。C++的對象有多種內(nèi)存分配方式。
物理機如何處理并發(fā)問題?
運算任務(wù),除了需要處理器計算之外,還需要與內(nèi)存交互,如讀取運算數(shù)據(jù)、存儲運算結(jié)果等(不能僅靠寄存器來解決)。 計算機的存儲設(shè)備和處理器的運算速度差了幾個數(shù)量級,所以不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache),作為內(nèi)存與處理器之間的緩沖:將運算需要的數(shù)據(jù)復(fù)制到緩存中,讓運算快速運行。
當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存,這樣處理器就無需等待緩慢的內(nèi)存讀寫了。 基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾,但是引入了一個新的問題:緩存一致性。在很多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,它們又共享同一主內(nèi)存。當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存時,可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。 為了解決一致性的問題,需要各個處理器訪問緩存時遵循一致性協(xié)議。同時為了使得處理器充分被利用,處理器可能會對輸出代碼進行亂序執(zhí)行優(yōu)化。Java虛擬機的即時編譯器也有類似的指令重排序優(yōu)化。
Java 內(nèi)存模型
什么是Java內(nèi)存模型?
Java虛擬機的規(guī)范,用來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各個平臺下都能達到一致的并發(fā)效果。
Java內(nèi)存模型的目標(biāo)?
定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出這樣的底層細節(jié)。此處的變量包括實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但是不包括局部變量和方法參數(shù),因為這些是線程私有的,不會被共享,所以不存在競爭問題。
主內(nèi)存與工作內(nèi)存
所以的變量都存儲在主內(nèi)存,每條線程還有自己的工作內(nèi)存,保存了被該線程使用到的變量的主內(nèi)存副本拷貝。線程對變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進行,不能直接讀寫主內(nèi)存的變量。不同的線程之間也無法直接訪問對方工作內(nèi)存的變量,線程間變量值的傳遞需要通過主內(nèi)存。
內(nèi)存間的交互操作
一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存,Java內(nèi)存模型定義了8種操作:
原子性、可見性、有序性
- 原子性:對基本數(shù)據(jù)類型的訪問和讀寫是具備原子性的。對于更大范圍的原子性保證,可以使用字節(jié)碼指令monitorenter和monitorexit來隱式使用lock和unlock操作。這兩個字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關(guān)鍵字。因此synchronized塊之間的操作也具有原子性。
- 可見性:當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取之前從主內(nèi)存刷新變量值來實現(xiàn)可見性的。volatile的特殊規(guī)則保證了新值能夠立即同步到主內(nèi)存,每次使用前立即從主內(nèi)存刷新。synchronized和final也能實現(xiàn)可見性。final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒有把this的引用傳遞出去,那么其他線程中就能看見final字段的值。
- 有序性:Java程序的有序性可以總結(jié)為一句話,如果在本線程內(nèi)觀察,所有的操作都是有序的(線程內(nèi)表現(xiàn)為串行的語義);如果在一個線程中觀察另一個線程,所有的操作都是無序的(指令重排序和工作內(nèi)存與主內(nèi)存同步延遲線性)。
volatile
什么是volatile?
關(guān)鍵字volatile是Java虛擬機提供的最輕量級的同步機制。當(dāng)一個變量被定義成volatile之后,具備兩種特性:
- 保證此變量對所有線程的可見性。當(dāng)一條線程修改了這個變量的值,新值對于其他線程是可以立即得知的。而普通變量做不到這一點。
- 禁止指令重排序優(yōu)化。普通變量僅僅能保證在該方法執(zhí)行過程中,得到正確結(jié)果,但是不保證程序代碼的執(zhí)行順序。
為什么基于volatile變量的運算在并發(fā)下不一定是安全的?
volatile變量在各個線程的工作內(nèi)存,不存在一致性問題(各個線程的工作內(nèi)存中volatile變量,每次使用前都要刷新到主內(nèi)存)。但是Java里面的運算并非原子操作,導(dǎo)致volatile變量的運算在并發(fā)下一樣是不安全的。
為什么使用volatile?
在某些情況下,volatile同步機制的性能要優(yōu)于鎖(synchronized關(guān)鍵字),但是由于虛擬機對鎖實行的許多消除和優(yōu)化,所以并不是很快。
volatile變量讀操作的性能消耗與普通變量幾乎沒有差別,但是寫操作則可能慢一些,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。
并發(fā)與線程
并發(fā)與線程的關(guān)系?
并發(fā)不一定要依賴多線程,PHP中有多進程并發(fā)。但是Java里面的并發(fā)是多線程的。
什么是線程?
線程是比進程更輕量級的調(diào)度執(zhí)行單位。線程可以把一個進程的資源分配和執(zhí)行調(diào)度分開,各個線程既可以共享進程資源(內(nèi)存地址、文件I/O),又可以獨立調(diào)度(線程是CPU調(diào)度的最基本單位)。
實現(xiàn)線程有哪些方式?
- 使用內(nèi)核線程實現(xiàn)
- 使用用戶線程實現(xiàn)
- 使用用戶線程+輕量級進程混合實現(xiàn)
Java線程的實現(xiàn)
操作系統(tǒng)支持怎樣的線程模型,在很大程度上就決定了Java虛擬機的線程是怎樣映射的。
Java線程調(diào)度
什么是線程調(diào)度?
線程調(diào)度是系統(tǒng)為線程分配處理器使用權(quán)的過程。
線程調(diào)度有哪些方法?
- 協(xié)同式線程調(diào)度:實現(xiàn)簡單,沒有線程同步的問題。但是線程執(zhí)行時間不可控,容易系統(tǒng)崩潰。
- 搶占式線程調(diào)度:每個線程由系統(tǒng)來分配執(zhí)行時間,不會有線程導(dǎo)致整個進程阻塞的問題。
雖然Java線程調(diào)度是系統(tǒng)自動完成的,但是我們可以建議系統(tǒng)給某些線程多分配點時間——設(shè)置線程優(yōu)先級。Java語言有10個級別的線程優(yōu)先級,優(yōu)先級越高的線程,越容易被系統(tǒng)選擇執(zhí)行。
但是并不能完全依靠線程優(yōu)先級。因為Java的線程是被映射到系統(tǒng)的原生線程上,所以線程調(diào)度最終還是由操作系統(tǒng)說了算。如Windows中只有7種優(yōu)先級,所以Java不得不出現(xiàn)幾個優(yōu)先級相同的情況。同時優(yōu)先級可能會被系統(tǒng)自行改變。Windows系統(tǒng)中存在一個“優(yōu)先級推進器”,當(dāng)系統(tǒng)發(fā)現(xiàn)一個線程執(zhí)行特別勤奮,可能會越過線程優(yōu)先級為它分配執(zhí)行時間。
線程安全的定義?
當(dāng)多個線程訪問一個對象時,如果不用考慮這些線程在運行時環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進行額外的同步,或者在調(diào)用方法進行任何其他的協(xié)調(diào)操作,調(diào)用這個對象的行為都可以獲得正確的結(jié)果,那這個對象就是線程安全的。
Java語言操作的共享數(shù)據(jù),包括哪些?
- 不可變
- 絕對線程安全
- 相對線程安全
- 線程兼容
- 線程對立
不可變
在Java語言里,不可變的對象一定是線程安全的,只要一個不可變的對象被正確構(gòu)建出來,那其外部的可見狀態(tài)永遠也不會改變,永遠也不會在多個線程中處于不一致的狀態(tài)。
如何實現(xiàn)線程安全?
虛擬機提供了同步和鎖機制。
- 阻塞同步(互斥同步)
- 非阻塞同步
阻塞同步(互斥同步)
互斥是實現(xiàn)同步的一種手段,臨界區(qū)、互斥量和信號量都是主要的互斥實現(xiàn)方式。Java中最基本的同步手段就是synchronized關(guān)鍵字,其編譯后會在同步塊的前后分別形成monitorenter和monitorexit兩個字節(jié)碼指令。這兩個字節(jié)碼都需要一個Reference類型的參數(shù)指明要鎖定和解鎖的對象。如果Java程序中的synchronized明確指定了對象參數(shù),那么這個對象就是Reference;如果沒有明確指定,那就根據(jù)synchronized修飾的是實例方法還是類方法,去獲取對應(yīng)的對象實例或Class對象作為鎖對象。 在執(zhí)行monitorenter指令時,首先要嘗試獲取對象的鎖。
- 如果這個對象沒有鎖定,或者當(dāng)前線程已經(jīng)擁有了這個對象的鎖,把鎖的計數(shù)器+1;當(dāng)執(zhí)行monitorexit指令時將鎖計數(shù)器-1。當(dāng)計數(shù)器為0時,鎖就被釋放了。
- 如果獲取對象失敗了,那當(dāng)前線程就要阻塞等待,知道對象鎖被另外一個線程釋放為止。
除了synchronized之外,還可以使用java.util.concurrent包中的重入鎖(ReentrantLock)來實現(xiàn)同步。ReentrantLock比synchronized增加了高級功能:等待可中斷、可實現(xiàn)公平鎖、鎖可以綁定多個條件。
等待可中斷:當(dāng)持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,對處理執(zhí)行時間非常長的同步塊很有用。
公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。synchronized中的鎖是非公平的。
非阻塞同步
互斥同步最大的問題,就是進行線程阻塞和喚醒所帶來的性能問題,是一種悲觀的并發(fā)策略??偸钦J(rèn)為只要不去做正確的同步措施(加鎖),那就肯定會出問題,無論共享數(shù)據(jù)是否真的會出現(xiàn)競爭,它都要進行加鎖、用戶態(tài)核心態(tài)轉(zhuǎn)換、維護鎖計數(shù)器和檢查是否有被阻塞的線程需要被喚醒等操作。
隨著硬件指令集的發(fā)展,我們可以使用基于沖突檢測的樂觀并發(fā)策略。先進行操作,如果沒有其他線程征用數(shù)據(jù),那操作就成功了;如果共享數(shù)據(jù)有征用,產(chǎn)生了沖突,那就再進行其他的補償措施。這種樂觀的并發(fā)策略的許多實現(xiàn)不需要線程掛起,所以被稱為非阻塞同步。
鎖優(yōu)化是在JDK的那個版本?
JDK1.6的一個重要主題,就是高效并發(fā)。HotSpot虛擬機開發(fā)團隊在這個版本上,實現(xiàn)了各種鎖優(yōu)化:
- 適應(yīng)性自旋
- 鎖消除
- 鎖粗化
- 輕量級鎖
- 偏向鎖
為什么要提出自旋鎖?
互斥同步對性能最大的影響是阻塞的實現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性帶來很大壓力。同時很多應(yīng)用共享數(shù)據(jù)的鎖定狀態(tài),只會持續(xù)很短的一段時間,為了這段時間去掛起和恢復(fù)線程并不值得。先不掛起線程,等一會兒。
自旋鎖的原理?
如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時并行執(zhí)行,讓后面請求鎖的線程稍等一會,但不放棄處理器的執(zhí)行時間,看看持有鎖的線程是否很快就會釋放。為了讓線程等待,我們只需讓線程執(zhí)行一個忙循環(huán)(自旋)。
自旋的缺點?
自旋等待本身雖然避免了線程切換的開銷,但它要占用處理器時間。所以如果鎖被占用的時間很短,自旋等待的效果就非常好;如果時間很長,那么自旋的線程只會白白消耗處理器的資源。所以自旋等待的時間要有一定的限度,如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖,那就應(yīng)該使用傳統(tǒng)的方式掛起線程了。
什么是自適應(yīng)自旋?
自旋的時間不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定。
如果一個鎖對象,自旋等待剛剛成功獲得鎖,并且持有鎖的線程正在運行,那么虛擬機認(rèn)為這次自旋仍然可能成功,進而運行自旋等待更長的時間。
如果對于某個鎖,自旋很少成功,那在以后要獲取這個鎖,可能省略掉自旋過程,以免浪費處理器資源。
有了自適應(yīng)自旋,隨著程序運行和性能監(jiān)控信息的不斷完善,虛擬機對程序鎖的狀況預(yù)測就會越來越準(zhǔn)確,虛擬機也會越來越聰明。
鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。主要根據(jù)逃逸分析。
程序員怎么會在明知道不存在數(shù)據(jù)競爭的情況下使用同步呢?很多不是程序員自己加入的。
鎖粗化
原則上,同步塊的作用范圍要盡量小。但是如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,甚至加鎖操作在循環(huán)體內(nèi),頻繁地進行互斥同步操作也會導(dǎo)致不必要的性能損耗。
鎖粗化就是增大鎖的作用域。
輕量級鎖
在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
偏向鎖
消除數(shù)據(jù)在無競爭情況下的同步原語,進一步提高程序的運行性能。即在無競爭的情況下,把整個同步都消除掉。這個鎖會偏向于第一個獲得它的線程,如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要同步。