多線程併發 (一) 瞭解 Java 虛擬機 - JVM

章節:
多線程併發 (一) 瞭解 Java 虛擬機 - JVM
多線程併發 (二) 瞭解 Thread
多線程併發 (三) 鎖 synchronized、volatile
多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量,CAS機制
多線程併發 (五) ReentrantLock 使用和源碼

           目錄

1.Java 虛擬機執行流程

2.Java虛擬機結構

3.運行時數據區域

4.對象的創建過程

5.對象在堆中的內存佈局

6.Java對象在虛擬機中的生命週期

7.Java中的引用

8.垃圾標記算法

9.垃圾收集算法思想


Java虛擬機引入併發編程

從java虛擬機一環一環的去引入多線程併發編程,有時候學的知識聯繫不到一起。我們瞭解java虛擬機,瞭解了多線程併發,瞭解了鎖。可曾想過爲什麼會出現鎖這個概念?爲什麼會有多線程?是由java虛擬機那部分結構引入的呢。希望你能從中找到答案!

1.Java 虛擬機執行流程

當我們運行一個java程序時,他的運行流程如下:

java流程

一段程序的運行分爲兩部分:1.編譯時期將.java通過 javac 命令編譯成.class文件。2.java虛擬機運行時期 classloader 加載.class文件。
注:Java 虛擬機與 Java 語言沒有什麼必然的聯繫,它只與特定的二進制文件: Class 文件有關 。 因此無論任何語言 只要能編譯成 Class 文件,就可以被 Java 虛擬機識別並執行。

2.Java虛擬機結構

java虛擬機內存模型大致分爲5個區域:java虛擬機棧,堆,本地方法區,方法區,程序計數器。

方法區和 Java 堆就是所有線程共享的數據區域

3.運行時數據區域

程序計數器、 Java 虛擬機棧、本地方法棧、 Java 堆和方法區。
1.程序計數器:爲了保證程序能不間斷的運行,程序計數器起到記錄當前運行的指令的地址。Java 虛擬機的多線程是通過輪流切換並分配處理器執行時間的方式來實現的,在一個確定的時刻只有一個處理器執行一 條線程中的指令,爲了在線程切換後能恢復到正確的執行位置 ,每個線程都會 有一個獨立的程序計數器,因此程序計數器是線程私有的。(每個線程在創建的時候,都會分配一個程序計數器,用來記錄當前線程運行的位置。比如當有兩個線程A,B。同一時間處理器只會處理一個線程的任務,當從A線程切換到B,再從B切換到A,這時候就需要從A程序計數器中獲取之前程序運行的位置。所以是線程私有的。)

2.Java 虛擬機棧(涉及到java內存模型):每一條線程都有一個線程私有的 Java 虛擬機棧(Java Virtual MachineStacks )。它的生命週期與線程相同,與線程是同時創建的 。Java 虛擬機棧存儲線程中 Java方法調用的狀態,包括局部變量、參數、返回值以及運算的中間結果等。(每個線程在創建的時候,都會分配一個對應的虛擬機棧空間,運行方法時方法入棧,會儲存方法內局部變量等,所以是線程私有的。)

3.本地方法棧:Java 虛擬機實現可能要用到 C Stacks 來支持 Native 語言 ,這個 C Stacks 就是本地方怯枝( Native Method Stack )。它與 Java 虛擬機枝類似,只不過本地方怯枝是用來支持 Native方峙的。(和java虛擬機棧一樣,本地方法棧是native方法引用的內存棧空間,也是線程私有的。)

4. Java 堆:Java 堆 (Java Heap )是被所有線程共享 的運行時內存區域。 Java 堆用來存放對象實例,Java 堆存儲的對象被垃圾收集器管理,這些受管理的對象無法顯式地銷燬。從內存回收的角度來分, Java 堆可以粗略地分爲新生代和老年代。Java 堆所使用的內存在物理上不需要連續,邏輯上連續即可。(java虛擬機堆保存了幾乎所有的java對象,因此堆是GC垃圾回收的主要戰場。線程共享的。)

5. 方法區(Hotspot中的永久代):方法區( Method Area )是被所有線程共享 的運行時內存區域,用來存儲已經被 Java虛擬機加載的類的結構信息,包括運行時常量地、字段和方法信息、靜態變量等數據。Java 堆所使用的內存在物理上不需要連續,邏輯上連續即可。(方法區主要存放類結構數據,常量池,靜態屬性是線程共享的,且一般GC滿意度比較低。)

4.對象的創建過程

一、類的加載過程
首先,Jvm在執行時,遇到一個新的類時,會到內存中的方法區去找class的信息,如果找到就直接拿來用,如果沒有找到,就會去將類文件加載到方法區。在類加載時,靜態成員變量加載到方法區的靜態區域,非靜態成員變量加載到方法區的非靜態區域。靜態代碼塊是在類加載時自動執行的代碼,非靜態代碼塊是在創建對象時自動執行的代碼,不創建對象不執行該類的非靜態代碼塊。
加載過程:
1、JVM會先去方法區中找有沒有相應類的.class存在。如果有,就直接使用;如果沒有,則把相關類的.clss加載到方法區。
2、在.class加載到方法區時,先加載父類再加載子類;先加載靜態內容,再加載非靜態內容
3、加載靜態內容:把.class中的所有靜態內容加載到方法區下的靜態區域內靜態內容加載完成之後,對所有的靜態變量進行默認初始化所有的靜態變量默認初始化完成之後,再進行顯式初始化當靜態區域下的所有靜態變量顯式初始化完後,執行靜態代碼塊
4、加載非靜態內容:把.class中的所有非靜態變量及非靜態代碼塊加載到方法區下的非靜態區域內。5、執行完之後,整個類的加載就完成了。
對於靜態方法和非靜態方法都是被動調用,即系統不會自動調用執行,所以用戶沒有調用時都不執行,主要區別在於靜態方法可以直接用類名直接調用(實例化對象也可以),而非靜態方法只能先實例化對象後才能調用。
二、對象的創建過程
1、new一個對象時,在堆內存中開闢一塊空間。
2、給開闢的空間分配一個地址。
3、把對象的所有非靜態成員加載到所開闢的空間下。
4、所有的非靜態成員加載完成之後,對所有非靜態成員變量進行默認初始化。
5、所有非靜態成員變量默認初始化完成之後,調用構造函數。
6、在構造函數入棧執行時,分爲兩部分:先執行構造函數中的隱式三步,
====①執行super()語句   ②對開闢空間下的所有非靜態成員變量進行顯示初始化  ③執行構造代碼塊====
再執行構造函數中書寫的代碼。
7、在整個構造函數執行完並彈棧後,把空間分配的地址賦給引用對象。
注:  super語句,可能出現以下三種情況:
1)構造方法體的第一行是this()語句,則不會執行隱式三步,而是調用this()語句所對應的的構造方法,最終肯定會有第一行不是this語句的構造方法。
2)構造方法體的第一行是super()語句,則調用相應的父類的構造方法, 
3)構造方法體的第一行既不是this()語句也不是super()語句,則隱式調用super(),即其父類的默認構造方法,這也是爲什麼一個父類通常要提供默認構造方法的原因。
引用:https://www.cnblogs.com/ttty/p/10431676.html

總結三步:開闢空間創建屬性對象賦默認值,調用構造方法賦初始值,給棧裏的對象賦引用。(cpu指令重排提高效率)

5.對象在堆中的內存佈局

                                                                                         對象分佈

                                                                                       hotspot

過程描述:一個對象創建之後包括四個部分如上圖,其中markword對象頭中保存了對象的hashCode,分代年齡,鎖標誌。
new出來的對象還是個無鎖的狀態,當有一個線程訪問時候,該鎖就升級成了偏向鎖/這時的hashCode就被轉移到了該對象的stack棧空間中,對象的markword中保存當前線程id等,此時偏向鎖標誌被標記爲1,。如果這時出現了另外一個線程在等待此對象的鎖,那麼這個鎖就升級成了輕量及鎖(自旋鎖:一直在for循環等待試探是否可以獲取鎖,消耗cpu)如果自旋超過10次,該鎖就被升級爲重量級鎖,重量級鎖是操作系統OS處理的,重量級鎖會把當前等待的線程丟到等待隊列中去,等鎖釋放了再從隊列中拿出來。GC每回收一次分代年齡就會加1 當gc年齡到達6歲對象就升到老年代區域。

6.Java對象在虛擬機中的生命週期

1. 創建階段
 總結三步:開闢空間創建屬性對象賦默認值,調用構造方法賦初始值,給棧裏的對象賦引用。(cpu指令重排提高效率)
2. 應用階段
當對象被創建,並分配給變量賦值時,狀態就切換到了應用階段。
3. 不可見階段
在程序中找不到對象的任何強引用,程序的執行已經超出了該對象的作用域。在不可見階段,對象仍可能被特殊的強引用 GC Roots 持有着,比如被運行中的線程引用等。
4. 不可達階段
在程序中找不到對象的任何強引用,並且垃圾收集器發現對象不可達。
5. 收集階段
垃圾收集器已經發現對象不可達,並且垃圾收集器已經準備好要對該對象的內存空間重新進行分配,這個時候如果該對象重寫了 finalize 方法則會調用該方法。
6. 終結階段
在對象執行完 finalize 方法後仍然處於不可達狀態時,或者對象沒有重寫 finalize 方法,
則該對象進入終結階段,並等待垃圾收集器回收該對象空間。
7. 對象空間重新分配階段
當垃圾收集器對對象的內存空間進行回收或者再分配時,這個對象就會徹底消失。

被標記爲不可達的對象會立即被垃圾收集器回收嗎?
很顯然是不會的,被標記爲不可達的對象會進入收集階段,這時會執行該對象重寫的 finalize 方法,如果沒有重寫 finalize 方法或者finalize 方法中沒有重新與一個可達的對象進行關聯纔會進入終結階段,並最終被回收。

7.Java中的引用

1.強引用
    默認創建一個對象就是強引用,具有強引用的對象垃圾收集器不會回收他,除非該對象對應的gc root 對象的引用被釋放,否則就算拋出oom也不會回收該對象的內存。
2.軟引用
    用SoftReference<O>標記的對象是軟引用,當內存不足時就會被回收。
3.弱引用
    用WeakReference<O>標記的對象是弱引用,當發生GC時候就會被回收。
4.虛引用
    PhantomReference<O>標記虛引用,虛引用在任何時候都有可能會被回收。

8.垃圾標記算法

對於垃圾收集要知道三個問題
    1.那些對象需要回收?
    2.什麼時候回收?
    3.怎麼回收?

對於程序計數器,java虛擬機棧,本地方法棧這三部分內存隨着方法結束和線程的結束內存會被回收,但對於方法區,堆則需要GC來回收。
(一)引用計數算法
    對於引用計數算法虛擬機並沒使用這種算法,引用計數算法是給一個對象加一個計數器,每當有一個地方引用他時候,計數器就加一,失效就減一,當計數器爲0時候就是沒有被引用,這時候垃圾收集器就可以回收。但是這種算法有一個問題就是兩個脫離GC root 的對象相互引用,導致計數器都不爲0的現象。
(二)根搜索算法
    通過GC root 對象向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象沒有在GC root引用鏈上,說明當前對象沒有GC root 對象引用,此對象可以回收。
    一下可作爲GC roots 對象:
    1.虛擬機棧中引用的對象
    2.方法區中的累靜態屬性引用的對象
    3.方法區中常量引用的對象
    4.本地方法棧中JNI引用的對象

9.垃圾收集算法思想

    (一)標記 - 請除算法
            標記清除算法包括兩個階段1.標記,2.清除。標記其實就是我們8中所說的標記算法。後續的垃圾收集算法都是基於這個思路對其進行改進。他的兩個缺點:1.效率不高,2.空間間隔問題,標記清除後會有大量不連續的內存碎片,當程序再次給對象分配內存時沒有足夠的內存空間,就會再一次出發GC。

    (二)複製算法
            爲了提高效率,在標記-清除算法基礎上產生了複製算法。複製算法是將內存一份爲大小相等的兩份。每次使用其中的一份,當發生GC時候,就會把使用的那塊內存中的存活的對象複製到另一半內存中,然後把之前那塊內存清空。這樣就解決了內存碎片的問題。但是這樣有一半內存是空的對於資源是十分浪費。

    (三)標記-整理算法
            根據老年代的特點,提出了標記-整理算法。在標記-清除的算法基礎上,讓存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

    (四)分代收集算法
            當前商業虛擬機都採用此種算法,這種算法只是根據對象的存貨週期的不同將內存分爲幾塊。一般是把java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最合適的收集算法。
    新生代中:GC 之後只有少量對象存活採用的是複製算法。
    老年代中:由於對象存活率高,空間少,採用的標記-清除 或 標記-整理算法。

由於本人記性不好學者忘着,所以才一字一句的記錄下來,若有錯誤或其他問題請評論區教訓我~。

摘學於:
深入理解Java虛擬機一書

 

發佈了120 篇原創文章 · 獲贊 147 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章