《深入理解Java虛擬機》筆記

Java Java虛擬機 編程語言 編譯器 Java團長 2017-04-11

一、內存管理

1、運行時的內存區域

線程私有:虛擬機棧、本地方法棧、程序計數器

線程共享:堆、方法區

2、各個內存區域可能拋出的異常

  • 1、當單線程時,棧的深度太大,會發生StackOverflowError,比如無窮的遞歸調用。

    2、當多線程時,若不停地創建線程,則會導致OutOfMemoryError,因為除去堆和方法區之外,剩下的棧總空間是有限的,不停創建線程則會不停申請棧空間,最終會導致內存溢出。

  • 當不停地創建(new)對象時,會導致OutOfMemoryError

  • 方法區

    運行時產生大量的類,去填滿方法區,比如用CGLib去無窮生成類。

  • 直接內存

    使用Unsafe分配本機內存時,可能導致OutOfMemoryError。

3、各個內存區域容量設置的參數

  • -Xss2M:設置棧的容量為2M

  • -Xms10M:設置堆的初始容量為10M

  • -Xmx10M:設置對的最大容量為10M

  • -XX:PermSize=10M:設置方法區的初始容量為10M

  • -XX:MaxPermSize=10M:設置方法區的最大容量為10M

  • -XX:MaxDirectMemorySize=10M:設置直接內存的最大容量為10M

4、對象的創建

  • 如何在堆中分配內存

    根據內存是否規整,即GC收集器是否帶有壓縮整理功能,分為指針碰撞和空閒列表兩種。

  • 如何處理內存分配衝突

    1、CAS+失敗重試;2、TLAB,即本地線程分配緩衝。

  • 對象在內存中的佈局

    對象頭(哈希碼、GC分代年齡、所狀態標誌等)、實例數據、對象填充

  • 如何訪問對象

    1、通過句柄(棧上的指針指向句柄,句柄中分別有指向對象的指針,和指向類信息的指針)

    2、直接指針(棧上的指針直接指向堆中的對象,對象中頭部有一個類型指針,指向類型信息)

5、對象存活判定

即如何判斷一個對象所佔用的內存是否該回收?

有兩種方法:1、引用計數法;2、可達性分析法。

  • 引用計數法

    該方法容易出現循環引用的問題,JVM並未採用。

  • 可達性分析法

    判斷是否能從GCRoots中找到一條到達該對象的路徑。

    GCRoots包括:棧中變量引用的對象、方法區中靜態屬性(static)引用的對象、方法區中常量(final)引用的對象。

《深入理解Java虛擬機》筆記

6、垃圾回收

  • 引用的種類

    1、強引用:通常new出來的對象的引用都是強引用。

    2、軟引用(SoftReference):如果某次回收完之後,還是可能發生內存溢出,則進行第二次回收,在第二次回收時會回收軟引用,若這次回收後仍是內存不夠,這時候才發生內存溢出。

    3、弱引用(WeakReference):其指向的對象只能生存到下一次垃圾收集之前,無論當前內存是否足夠,都會回收弱引用指向的對象。

    4、虛引用(PhantomReference):設置虛引用的目的可能是為了,在該對象被回收前能夠得到一個系統通知。

  • finalize方法

    若某類覆蓋了該方法,則其對象再被回收前會調用此方法(僅限於第一次回收該對象時)。

  • 方法區的回收

    廢棄的常量:當前系統中沒有一個對象引用此常量。

    無用的類:堆中不存在該類的任何實例,該類的類加載器已被回收,該類的Class對象沒有在任何位置被引用。

《深入理解Java虛擬機》筆記

  • 垃圾收集算法

    1、JVM整體上是採用“分代收集算法”。

    根據對象的存活週期的不同將Java堆分為新生代和老年代。

    新生代又分為Eden區,Survivor From區,Survivor To區,其大小比例默認為8:1:1。

    Java的方法區被定義為永久代

    2、對於新生代,一般採用“複製算法”。因為新生代的存活時間相對較短,複製的時候不會複製太多對象,所以整體效率不至於太低。

    當從Eden和Survivor From區向Survivor To區複製時,若Suvivor To區的空間不夠,則需要依賴老年代進行“分配擔保”。

    3、對於老年代,一般採用“標記-清除”(容易產生內存碎片)或“標記-整理”算法。因為老年代的對象存活率較高,若仍是採用複製操作,則需要複製的對象太多,效率會很低。

    4、Stop The World

    在判斷對象是否應該被回收時,是通過GCRoots來判斷的。

    當枚舉GCRoots時,不可以出現在分析過程中,對象的引用關係還在不斷髮生變化,所以這時候必須停頓所有線程,這種現象稱為“Stop The World”。

    5、安全點和安全區域

    安全區域是指:在這段代碼片段內,引用關係不會發生變化。

    6、內存分配和回收策略

    對象優先在Eden區分配,若內存不夠則進行一次Minor GC,將Eden區的活躍對象複製到Survivor To區。

    若Survivor To區大小足夠,則將其中的存活對象的GC年齡加1,並判斷是否應該晉升到老年代區。

    若Survivor To區大小不夠,則進行分配擔保,將對象複製到老年代。

    若此時老年代內存大小不夠,則進行一次Full GC。

  • 垃圾收集器

  • Serial:新生代收集器,單線程收集器,採用複製算法。

  • ParNew:新生代收集器,多線程收集器,採用複製算法。

  • Parallel Scavenge:新生代收集器,多線程收集器,側重於提高程序運行的吞吐量。

  • Serial Old:老年代收集器,單線程收集器,採用標記-整理算法。

  • Parallel Old:老年代收集器,多線程收集器,採用標記-整理算法。

    由於多線程的老年代收集器可以充分利用服務器多CPU的處理能力,所以常用Parallel Scavenge/Parallel Old組合,亦提高了吞吐量。

  • CMS(Concurrent Mark Sweep)

    老年代收集器

    初始標記-->併發標記-->重新標記-->併發清除

  • G1收集器

    初始標記-->併發標記-->最終標記-->篩選回收

二、執行子系統

(一)、類文件結構

1、平臺無關性和語言無關性

平臺無關性:通過Java虛擬機,Java代碼可以運行在不同的操作系統上。

語言無關性:不同的語言通過編譯成字節碼,均可以運行在JVM上。

2、Class文件結構

(1)、 兩種數據類型:無符號數,表。

(2)、魔數:“CAFEBABE”

(3)、次版本號,主版本號

(4)、常量池:常量個數,常量項(類型tag+內容)。包括字面量和符號引用。

(5)、訪問標誌:是否 public,final,super,interface,abstract,synthetic,annotation,enum

(6)、類索引,父類索引,接口索引

(7)、字段表

(8)、方法表

(9)、屬性表

3、字節碼指令

(1)、字節碼中的數據類型:byte、short、int、long、float、double、char、reference

(2)、加載和存儲指令:將數據加載到操作數棧,將數據存儲到局部變量表

(3)、運算指令:加減乘除、取餘、取反、位移、位運算(與、或、異或)、局部變量自增、比較

(4)、類型轉換

(5)、對象創建和訪問

(6)、操作數棧相關指令

(7)、控制轉移指令:條件分支等

(8)、方法調用和返回:

invokevirtual(實例方法)

invokeinterface(接口方法)

invokespecial(構造方法,私有方法,父類方法)

invokestatic(靜態方法)

(9)、同步指令:monitorenter、monitorexit。通過管程實現,用於支持synchronized關鍵字。

(二)、類加載機制

1、類在什麼時候會加載

(1)、new(使用new實例化對象的時候),getstatic(讀取一個類的靜態字段,不包括final的),putstatic(設置一個類的靜態字段,不包括final的),invokestatic(調用一個類的靜態方法時)。

(2)、通過java.lang.reflect包的方法,對類進行反射調用的時候,若類未初始化,則會對其進行初始化。

(3)、當初始化一個類時,若其父類還未初始化,則先初始化其父類。

(4)、虛擬機啟動時,會先初始化包含main方法的那個類,即主類。

(5)、被動引用不會導致類加載,比如:通過子類引用父類的靜態字段,不會導致子類初始化;定義某類的數組,則該類不會初始化;引用某類的靜態常量,則該類不會初始化。

2、類加載的過程

(1)、加載

通過類的全限定類名獲取二進制流;將字節流轉換為方法區內的運行時數據結構;在內存中生成一個代表該類的Class對象,作為方法區裡這個類的各種訪問數據的入口。

(2)、驗證

校驗Class文件中的信息是否複合JVM的要求。

文件格式驗證(基於二進制字節流,校驗主次版本號是否支持等);

元數據驗證(是否繼承的final類,是否實現了接口的所有方法等);

字節碼驗證(通過數據流和控制流分析,驗證程序語義,比如只能父類引用指向子類對象,子類引用不能指向父類對象);

符號引用驗證(當JVM將符號引用轉換為直接引用時,會檢查是否能根據名稱找到相應的類,方法,字段等);

驗證階段其實不是必須的,如果該字節碼被反覆驗證過,其實可以關閉驗證。

(3)、準備

為靜態變量設置初始值(int為0,reference為null等);

為常量設置初始值。

(4)、解析

將常量池裡的符號引用解析為直接引用(即指向內存中某個區域的指針)。

解析的符號引用有:類或接口、字段、方法、接口方法等。

(5)、初始化

若父類沒有加載,則先加載父類;

然後為靜態變量設置初始值,執行靜態代碼塊等;

3、類加載器

(1)、每個類,都要由“加載它的類加載器”和“這個類本身”一塊確定該類在虛擬機裡的唯一性。

(2)、類加載器種類

啟動類加載器:加載<JAVA_HOME>/lib目錄中的類;

擴展類加載器:加載<JAVA_HOME>/lib/ext目錄中的類;

應用程序類加載器:加載CLASSPATH中的類,如果應用程序沒有自定義過自己的類加載器,這個便是程序中默認的類加載器。

(3)、雙親委派模型

《深入理解Java虛擬機》筆記

當一個類加載器加載類的時候,首先不會親自加載這個類,而是會把這個請求委派給父加載器去加載,如此遞歸下去,所有的請求最終會傳到頂層的引啟動類加載器。只有當父加載器無法加載該類時,才會讓子類去嘗試加載。

這樣做可以保證,同一個類在虛擬機中不會被不同的類加載器加載很多次。

(三)、字節碼執行引擎

1、運行時棧幀結構

《深入理解Java虛擬機》筆記

程序執行時,內存中的棧,裡面是一個一個的棧幀。每一個方法的調用及其執行,都對應著一個棧幀。

2、方法調用

(1)、方法調用指令

invokestatic:調用靜態方法

invokespecial:調用實例構造器、私有方法、父類方法

invokesvirtual:調用所有的虛方法(非static非final方法)

invokeinterface:調用接口方法

(2)、分派

靜態分派:對應於方法參數上的重載。編譯器在重載時,是通過參數的靜態類型,而不是實際類型作為判斷依據的。

動態分派:對應於多態。當invokespecial指令執行時,第一步就是在運行期確定接受者的實際類型。

三、代碼優化

  • 編譯期優化

    (1)、編譯過程

    詞法分析,語法分析-->

    填充符號表-->

    處理註解-->

    語義分析(標註檢查、數據及控制流分析、解語法糖)-->

    字節碼生成。

    (2)、Java的語法糖

    泛型與類型擦除

    自動裝箱、拆箱

    foreach循環

    變長參數

    注意:1、JVM在字節碼裡,用Signature屬性存儲了方法在字節碼層面的方法簽名,通過這項元數據,可以通過反射獲取類的泛型信息。2、包裝類的“==”運算,在不遇到算數運算時不會自動拆箱,這時候比較的是引用是否相等。

  • 運行期優化

    (1)JIT編譯器(Just In Time)

    Java程序最初是通過解釋執行的,當虛擬機發現“某個方法或某段代碼塊

    ”運行特別頻繁時,會把這些代碼認定為“熱點代碼(Hot Spot Code)”。為了提高熱點代碼的執行效率,在運行時,會把這些代碼編譯為與本地平臺相關的機器碼,並進行各種優化。完成這個任務的編譯器叫做即時編譯器。

    (2)、解釋器與編譯器並存

《深入理解Java虛擬機》筆記

  • 1、編譯對象

    被多次調用的方法:將整個方法作為編譯對象。

    被多次執行的循環體:依然將整個方法作為編譯對象,進行棧上替換(On Stack Replacement),即替換棧幀。

    2、熱點探測的方式

    基於採樣:週期性的檢查棧頂,若某方法經常出現在棧頂,則其是熱點方法。

    基於計數器:統計方法執行次數,到了一定的閾值,則其是熱點方法。

    3、兩種計數器

    JVM的熱點探測是基於計數器的,有兩種計數器:方法調用計數器和回邊計數器。

    方法調用計數器:即統計方法執行次數。

    回邊計數器:循環體中代碼執行的次數。“回邊”,即在字節碼中遇到控制流向後跳轉的指令。

    4、編譯優化技術

    公共子表達式消除、數組邊界檢查消除、方法內聯、逃逸分析等。

四、併發

1、JVM內存模型和線程

(1)、JVM內存模型

《深入理解Java虛擬機》筆記

《深入理解Java虛擬機》筆記

1、JVM內存模型主要是定義程序中變量的訪問規則,即在虛擬機中將變量存儲到內存,和從內存中取出變量這樣的細節。這些變量指的是實例字段、靜態字段等,不包括局部變量(因為局部變量是線程私有的,不被共享,不存在競爭問題)。

2、工作內存中保存了該線程使用到的變量的在主存中的拷貝。線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行,不能直接讀寫主存中的變量。

3、Java內存模型中的工作內存只是個抽象概念,並不真實存在,它涵蓋了緩存、寫緩衝區、寄存器以及其它的硬件和編譯器優化。

4、內存間的交互操作

lock、unlock

read、load、use、assign、store、write

5、volatile變量

6、原子性、可見性、有序性

7、happens-before原則

(2)、線程

1、線程的實現方式

三種方式:1:1、1:N、N:M。

Java是通過將線程映射到操作系統的線程上去實現的。

2、線程的調度方式

兩種方式:協同式線程調度、搶佔式線程調度。

Java中使用的是搶佔式線程調度。

3、線程的狀態轉換

1、線程安全和鎖優化

  • 線程安全

    (1)、共享數據的種類:

    1、不可變:不可變對象是值“對象中帶有狀態的變量都聲明為final”。

    2、絕對線程安全

    3、相對線程安全:Vector、Hashtable等

    4、線程兼容:ArrayList、HashMap

    5、線程對立

    (2)、線程安全的實現方法

    1、互斥同步(Mutual Exclution & Synchronization)

    同步是指在多個線程併發訪問共享數據時,保證共享數據在同一時刻只被一個(一些)線程使用。

    互斥是實現同步的一種手段,臨界區、互斥量、信號量都是互斥的實現方式。

    互斥是因,同步是果;互斥是方法,同步是目的。

    互斥同步又稱為“阻塞同步”,屬於一種悲觀的併發策略。

    2、非阻塞同步

    非阻塞同步是一種基於“衝突檢測”的樂觀併發策略;

    即先進行操作,如果“沒有其它線程爭用共享數據”,那操作就成功了。如果共享數據有爭用,產生了衝突,那就再採取其它的補償措施(比如不斷重試,直到成功為止)。

    因為這種策略不需要將線程掛起,所以成為非阻塞同步。

    “操作和衝突檢測”這兩個步驟,需要具備原子性,這可以通過硬件的CAS來實現。

    CAS指令需要三個操作數:內存位置,舊的預期值,新值。當且僅當“內存位置的值”符合“舊的預期值”時,處理器用“新值”更新“內存位置的值”,否則就不更新。最終無論是否更新,均會返回“舊值”。

    3、不須同步

    可重入代碼:不依賴堆上的數據和共用的系統資源,用到的狀態量都由參數中傳入,不調用非可重入的方法。如果一個方法,它的返回結果可預測,相同的輸入,均能返回相同的輸出,它便是可重入的。

    線程本地存儲:即ThreadLocal,以當前線程哈希碼為鍵,某變量位置的一個鍵值對。

  • 鎖優化

    1、自旋鎖

    因為當線程阻塞,或從阻塞中恢復時,掛起線程和恢復線程的操作都需要轉入到內核態去完成,這給系統的性能帶來了很大壓力。

    如果某個鎖被佔用的時間很短,這時候可以讓“後面那個請求鎖的線程”稍微等一下,不放棄CPU的執行時間,執行一個忙循環,直到獲得鎖。

    2、鎖消除

    將一些代碼上進行了同步,但實際不會存在共享數據競爭的鎖進行消除。

    3、鎖粗化

    如果對一個對象反覆加鎖和解鎖,甚至這個對象在循環體內,則可以把鎖加到循環體外,這樣可以消除反覆加解鎖所帶來的損耗。

    4、輕量級鎖

    5、偏向鎖

學習Java的同學注意了!!!

學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群,群號碼:495273252【長按複製】我們一起學Java!

《深入理解Java虛擬機》筆記

相關推薦

推薦中...