JVM學習筆記2.0

1.運行時數據區域

    Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域。這些區域都有各自的用途,以及創建和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而建立和銷燬。

  • 程序計數器

        程序計數器(Program Counter Register)是一塊較小的內存空向,它可以看作是當前線程所執行的字節碼的行號指示器。在虛似機的概念模型裏(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作肘就是通過改變這個計數器的値來選取下一條需要抉行的字節碼指令分支、循環、跳裝、異常處理、線程恢夏等基礎功能都需要依賴這個計數器來完成。
        如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器值則爲空(Undefined)。 此內存區域是唯- 一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

  • Java虛擬機棧

        Java虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中人棧到出棧的過程。
        在Java虛擬機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。

  • 本地方法棧

        本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別是虛擬機棧爲虛擬機執行Java方法( 也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。
        本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

  • Java堆

        Java堆是垃圾收集器管理的主要區域。Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。如果在堆中沒有完成實例分配,並且堆也再無法擴展時,將會拋出OutOfMemoryError異常。

  • 方法區

        與堆一樣,也是各個線程共享的一塊內存區域。特別地,在HotSpot虛擬機上,很多人稱方法區爲永久代。,當無法再申請到內存時,會拋出OutOfMemoryError異常。

  • 運行時常量池

        是方法區的一部分,同樣受到方法區內存的限制,當無法再申請到內存時,會拋出OutOfMemoryError異常。

  • 直接內存

        直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現。
        NIO可以直接分配堆外內存

2.hotspot中對象的內存佈局

Hotspot虛擬機中,對象在內存中的存儲佈局分爲三部分:

    Header(對象頭)
    
        自身運行時數據(32位~64位MarkWord):哈希值、GC分代年齡、鎖狀態標誌、線程持有鎖、偏向線程ID、偏向時間戳。
        類型指針
        
    InstanceData:數據實例,即對象的有效信息,相同寬度(如long和double)的字段被分配在一起,父類屬性在子類屬性之前。    

    Padding:佔位符填充內存

3.對象的訪問定位

*句柄訪問

    最大好處是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要修改

  • 直接指針訪問

    最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。Hotspot使用直接指針訪問。

4.內存溢出OutOfMemoryError

  • Java堆溢出

    Java堆用於存儲對象實例,只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。
    Java堆內存的0OM異常是實際應用中常見的內存溢出異常情況。當出現Java堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟着進.步提示“Java heap space"。
    如果是內存泄露,可進一步通過查看泄露對象到GC Roots的引用鏈,就能找到泄露對象是通過怎樣的路徑與GCRoots相關聯並導致垃圾收集器無法自動回收它們的。掌握了泄露對象的類型信息及GCRoots引用鏈的信息,就可以比較準確地定位出泄露代碼的位置。
    如果是不存內存在泄露,應當檢查虛擬機的堆參數(-Xmx 與-Xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。

  • 棧溢出

    hotspot虛擬機並不區分虛擬機棧和本地方法棧。如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMermoryEror異常。
    在單線程下,無論是由於棧幀太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。多線程情況下,爲每個線程的棧分配的內存越大,反而越容易產生內存溢出異常。

  • 常量區和方法區

    提示:java.lang.OutOfMemoryError:PermGen space

5.垃圾回收(GC)

  • 標記清除算法

    主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高:另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

  • 複製算法(多用於新生代)

    將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間爲整個新生代容量的900%(80%+10%),只有10%的內存會被“浪費”。如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。
    複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。

  • 標記整理算法(多用於老年代)

根據老年代的特點,提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向-端移動,然後直接清理掉端邊界以外的內存。

  • 分代收集算法

    Java 虛擬機將堆分爲新生代和老年代,並且對不同代採用不同的垃圾回收算法。其中,新生代分爲 Eden 區和兩個大小一致的 Survivor 區,並且其中一個 Survivor 區是空的。
    在只針對新生代的 Minor GC 中,Eden 區和非空 Survivor 區的存活對象會被複制到空的 Survivor 區中,當 Survivor 區中的存活對象複製次數超過一定數值時,它將被晉升至老年代。
    因爲 Minor GC 只針對新生代進行垃圾回收,所以在枚舉 GC Roots 的時候,它需要考慮從老年代到新生代的引用。爲了避免掃描整個老年代,Java 虛擬機引入了名爲卡表的技術,大致地標出可能存在老年代到新生代引用的內存區域。

6.對象分配

對象分配原則:   
1.對象優先在eden分配
2.大對象直接進入老年代
3.長期存活的對象將進入老年代
4.動態對象年齡判定

7.類加載

    1.七個階段:
    加載,驗證,準備,解析,初始化,使用,卸載。驗證,準備,解析三個部分統稱爲連接。

    2.對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

    3.類加載器類型

  • 啓動類加載器(Bootstrap ClassLoader)
  • 擴展類加載器(Extension ClassLoader)
  • 應用程序類加載器(Application ClassLoader)

    4.雙親委派模型

    雙親委派模型的工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器丟完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到項層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。
    使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。

    5.破壞雙親委派模型

    雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之所以稱爲“基礎”,是因爲它們總是作爲被用戶代碼調用的API,但是如果基礎類又要調用回用戶的代碼時,啓動類加載器不“認識”這些代碼,爲了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLcader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoaser()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
    有了線程上下文類加載器,就可以使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的-般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都採用這種方式,例如JNDI、JDBC、 JCE、 JAXB和JBI等。

8.Java語法糖

  • 泛型擦除
  • 自動拆裝箱
  • 遍歷循環
  • 條件編譯
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章