JVM個人看書記錄

  • 部分轉載至其他博客,但當時爲總結筆記在雲筆記中,找不到原文地址,見諒。
  • JVM運行時數據區域

分爲 方法區、堆、虛擬機棧、本地方法棧、程序計數器

其中方法區和堆爲所有線程共享的數據區,其餘的虛擬機棧、本地方法棧、程序計數器爲線程隔離的數據區

即每個線程有單獨虛擬機棧、本地方法棧、程序計數器。

關於他們的作用如下:

程序計數器:一塊很小的內存空間,當前線程所執行的字節碼的行號指示器(由於JAVA虛擬機的多線程是通過線程輪流切換並分配處理器執行時間來實現的,在任意時刻,一個處理器內核都只會執行一條線程中的指令,因此爲了線程切換後能恢復到正確的執行位置,每個線程都要有單獨的程序計數器),若線程執行一個JAVA方法,那該計數器記錄的是正在執行的虛擬機字節碼指令地址。若程序執行的是Native方法(下面會介紹),這個計數器值則爲空。該區域爲JAVA虛擬機區域沒有規定任何OutOfMemoryError(內存溢出導致的錯誤)情況的區域。

JAVA虛擬機棧

      該區域也是線程私有的,它的生命週期和線程相同。虛擬機棧描述的是JAVA方法執行的內存模型,即每個方法在執行期間時會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。人們口頭常說的棧,其實爲虛擬機棧用的局部變量表部分,局部變量表存放了編譯器可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(refernece類型)和returnAddress類型(指向一條字節碼指令的地址)。其中64位長度的long和double類型會佔用2個局部變量空間,其餘的數據類型佔用1個。局部變量表所需的內存空間在編譯器間分配完成,即進入一個方法時,該方法的局部變量空間是固定的,在方法運行期間不會改變。

     該區域會有兩種異常情況:若線程請求深度大於虛擬機棧中所允許的深度,則拋出StackOverflowError異常;若虛擬機棧可以動態擴展,那麼在擴展時無法申請到足夠內存時,則拋出OutOfMemoryError異常。需要注意的是,在單線程環境下,虛擬機棧都只會發生StackOverflowError異常,因爲當棧空間無法分配時,無法確定是內存不足還是已使用棧空間太大,本質上是對一件事情的兩種描述。在多線程環境下,通過不斷建立線程的方式的確會發生OutOfMemoryError異常,但是無法確定是由於棧空間申請內存不足產生的,因爲操作系統分配給每個進程的內存資源是有限的,例如32位的windows位2GB。虛擬機可以通過參數控制JAVA堆和方法區兩個區域的最大值,其餘的一些內存耗費忽略不計的情況下,剩餘的內存空間就由虛擬機棧和本地方法棧瓜分,當線程分配到的棧容量越大(即這裏棧能分配到很多空間,但是依然發生內存溢出),所能構造的線程數量就會減少。這時可以通過減少最大堆容量和減少棧容量來換取獲得更多線程。

      棧容量大小通過-Xss控制。

本地方法棧

          Native Method Stack,與虛擬機棧發揮的作用是相似的。區別不過是虛擬機棧位執行JAVA方法(字節碼)服務,而本地方法棧則爲虛擬機使用的Native方法服務。該區域也會拋出OutOfMemoryError、StackOverflowError異常。

JAVA堆:

     對於大多數應用,JAVA堆是JVM所管理的內存中最大的一塊,JAVA堆是被所有線程共享的一塊內存區域,該區域的唯一目的就是存放對象實例,幾乎所有的對象實例在這裏分配內存。該區域也是垃圾收集器(GC)的主要區域,從內存回收角度看,由於現在垃圾收集器都採用分代算法,所以JAVA堆可以細分爲:新生代和老年代。再細緻一點的有Eden空間、From Survivor 空間、To Survior空間等。無論哪個區域都是存放的對象實例、細分只是爲了更好更快地進行內存回收。JAVA堆可以處於物理上不連續的內存空間中,只要邏輯上是連續即可,當堆無法擴展時,將會拋出OutOfMemoryError異常。

       堆的最小值通過-Xms控制、最大值通過-Xmx控制。

    對於堆的OutOfMemoryError異常,一般是通過內存映像分析工具(如Eclipse Memory Analyzer)堆Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,即要先分清楚是內存泄漏Memory Leak還是內存溢出Memory Overflow,若是內存泄漏則通過工具查看泄漏對象到GC Root的引用鏈(下面會介紹到垃圾收集),於是就能找到是通過什麼路徑與GC Roots相關聯導致無法進行垃圾回收。若是內存溢出,則檢查虛擬機參數-Xmx和-Xms和機器物理內存。看是否能調大或者檢查是否存在生命週期過長的對象。

方法區

      即人們所說的永久代(實際並不等價),該區域也是線程共享的內存區域。它用於存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,虛擬機規範將方法區描述爲堆的一個邏輯部分。當方法區無法滿足內存分配需求時,拋出OutOfMemoryError異常。 

      運行時常量池是方法區的一部分。Class文件有一個常量池用來存放編譯器生成的各種字面量和符號引用,這部分內容在類加載後存放到方法區的運行時常量池中。

      運行時常量池相對於Class 文件常量池的另外一個重要特徵是具備動態性,Java 語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。

     可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,間接限制常量池容量。

     String.intern()是一個Native方法,作用爲 如果字符串常量池中已經包含一個等於此String對象的字符串,則返回代表池中這個字符串的String對象,若是首次出現則將此String對象添加到常量池中並返回此String的引用。

     在JDK1.7中,對於String類的intern()方法,將不再複製對象實例,而是在常量池中記錄首次出現的實例的引用。即JDK1.7的HotSpot虛擬機中,已經把原本放在永久代的字符串常量池移出。

直接內存:

       直接內存並不是虛擬機運行時數據區的一部分,也不是JVM規範中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError 異常出現。
         爲了提高IO速度,在JDK 1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可以使用Native 函數庫直接分配堆外內存,然後通過一個存儲在Java 堆裏面的DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java 堆和Native 堆中來回複製數據。顯然,本機直接內存的分配不會受到Java 堆大小的限制,但是,既然是內存,則肯定還是會受到本機總內存(包括RAM 及SWAP 區或者分頁文件)的大小及處理器尋址空間的限制。服務器管理員配置虛擬機參數時,一般會根據實際內存設置-Xmx等參數信息,但經常會忽略掉直接內存,使得各個內存區域的總和大於物理內存限制(包括物理上的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

     大小默認爲堆最大值-Xmx,也可手動通過-XX:MaxDirectMemorySize設置。


垃圾收集GC

    JAVA內存回收技術

    即使有了垃圾回收機制,程序員還是需要排查各種內存溢出和內存泄漏問題,當垃圾收集成爲系統達到高併發的瓶頸時,我們就更要對這項技術進行監控和調節。

      判斷一個對象的是否存活用可達性分析算法:

      即通過一系列GC Roots對象作爲根節點,從這些節點向下搜索,搜索走過的路徑爲引用鏈,當一個對象到GC Roots沒有任何引用鏈時,則證明對象不可用,需要進行回收。

      可作爲GC Roots的對象:1)虛擬機棧中棧幀的本地變量表引用的對象 

                                               2)方法區中類靜態屬性的引用對象

                                               3)方法區中常量的引用對象

                                                4)本地方法棧中JNI(即一般的Native方法)引用的對象

    JDK1.2後對引用概念進行擴充,將引用劃分爲強引用、軟引用、弱引用、虛引用

     強引用:指程序代碼中普遍存在,類似“Object obj = new Object ()”這類引用,只要強引用存在,垃圾收集器永遠不會回收被引用的對象。

     軟引用:指有用但不是必須的對象,在系統將要發生內存溢出異常前,將會把這類引用對象列入回收範圍進行第二次回收,若果第二次回收後還是內存溢出,則會拋出異常。

     弱引用:也指非必須的對象,但強度比軟引用要更弱一些。被弱引用關聯的對象只能生存到下一次垃圾回收之前,即垃圾收集器工作時,無論內存是否足夠,都會回收弱引用關聯對象。

     虛引用:也稱爲幽靈引用,最弱的引用關係,無法通過虛引用獲取對象實例,虛引用也不影響對象生存與否,唯一目的就是能在這個對象唄垃圾收集器回收時收到一個系統通知。

     當對象經過可達性分析發現是不可達對象時,並不是立即回收,還要有一個宣告死亡的過程,即判斷是否有必要執行finalize()方法,若對象沒有覆蓋該方法或虛擬機已經調用過該方法則不執行。若判斷爲要執行則將對象放置入一個隊列中,並由虛擬機建立一個線程去執行,但並不保證該方法的正常執行,也就是該方法可能執行緩慢或死循環,這時GC將會對隊列中的對象進行二次標記,若此時對象已經與引用鏈上的對象建立關聯,則可以被移出隊列死裏逃生,若沒有則進行垃圾回收。即一個對象唄執行finelize()後對象不一定死亡。

  方法區的垃圾回收

     在JDK1.7中,HotSpot虛擬機中的永久代,即PermGen space(兩者並不等價,前者爲JVM的一種規範,後者爲JVM規範的具體實現)。永久代垃圾回收主要爲兩部分:廢棄常量和無用的類,且效率低下,判斷廢棄常量即爲判斷常量池中常量是否存在引用,但判斷無用的類則必須同時滿足3個條件:

                 1)該類所有實例被回收,堆中不存在任何該類的實例

                 2)加載該類的ClassLoader已經被回收

                 3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法通過反射訪問該類的方法

      由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出。但是在 JDK 1.8 中, HotSpot 已經沒有 “PermGen space”這個區間了,取而代之是一個叫做 Metaspace(元空間) 的東西。下面我們就來看看 Metaspace 與 PermGen space 的區別。

 

Metaspace(元空間)

  其實,移除永久代的工作從JDK1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。 JDK 1.8中 PermSize 和 MaxPermGen 已經無效,JDK 1.7 和 1.8 將字符串常量由永久代轉移到堆中,並且 JDK 1.8 中已經不存在永久代的結論,而類的元數據與類的加載器被一同放入Metasapce中。

      元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:

  -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
  -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

  除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:
  -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少爲分配空間所導致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少爲釋放空間所導致的垃圾收集

     爲什麼JDK1.8要把永久代轉爲元空間呢

    原因:

      1、字符串存在永久代中,容易出現性能問題和內存溢出。

  2、類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。

  3、永久代會爲 GC 帶來不必要的複雜度,並且回收效率偏低。

  4、Oracle 可能會將HotSpot 與 JRockit 合二爲一。

     

  1. Metaspace不再與“老年代”綁定,由元數據虛擬機單獨管理,分配本地內存;這樣有幾個好處:
  • 在full gc時,元空間的數據不會被掃描到;
  • CMS中特定於Permgen的複雜代碼可以移除;
  1. Metaspace可以動態增長,Permgen(永久代)在運行時不可變;
  2. 在元空間中,類和其元數據的生命週期和其對應的類加載器是相同的;每個類加載器一塊虛擬內存,內部再分成不同的小塊;
  3. 元空間虛擬機管理內存的數據結構是鏈表,分配方式是分組分配,目前的缺點是有碎片;
  4. 內存分佈對比


堆的分代以及垃圾回收
        現在的垃圾回收器都採用分代收集算法,堆分代的唯一理由就是優化GC性能,如果沒有分代,那麼所有的對象都在一塊,GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放到某一地方,當GC的時候先把這塊存“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。
        堆可以細分爲年輕代與年老代,並且年輕代還分爲了三部分:1個Eden區和2個Survivor區(分別叫From和To),默認比例爲8:1:1。一般情況下,新創建的對象都會被分配到Eden區(大對象直接分配到年老代),這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每經過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中(一般爲>15)。

    在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊,當這一塊內存用完,就將還活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都對整個半區進行內存回收,分配時不用考慮碎片的情況。

   在GC開始的時候,對象存在於Eden區和名爲“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有對象移動到年老代中。Eden區和Survivor區並不是按照1:1的比例來劃分內存空間的,當Survivor空間不夠用時,需要依賴老年代的空間來進行分配。

保。如何理解分配擔保呢,其實就是,內存不足時,去老年代內存空間分配,然後等新生代內存緩過來了之後,把內存歸還給老年代,保持新生代中的Eden:Survivor=8:1。空間分配擔保的首要判斷條件就是,老年代最大連續空間是否大於新生代多有對象總空間,若不能則會通過判斷是否有設置允許擔保失敗,若允許擔保失敗則判斷老年代最大連續空間是否大於歷代晉升到老年代的對象平均大小,則嘗試一次Minor GC,若都不能則進行一次Full GC來保證老年代有足夠的連續內存空間。

  年輕代採用複製算法,這種算法在對象存活率較高時就要進行較多的複製操作,效率將變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配,以應對一些極端情況,所以這種算法對於年老代不適用。對於年老代,採用“標記-清除”或者“標記-整理”算法進行回收。“標記-清除”算法分爲兩個過程:標記的過程其實就是,遍歷所有的GC Roots,然後將所有GC Roots可達的對象標記爲存活的對象;清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。
  “標記-整理”算法前期跟“標記-清除”算法一樣,但後續的整理步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。這樣就不會造成內存碎片的問題。

  關於一些內存分配:

   默認情況新對象優先分配到Eden空間,需要大量連續存儲空間的JAVA對象直接分配到老年代

  收集算法是垃圾收集的方法論對於垃圾收集器的具體實現,還有不同的垃圾收集器,如G1收集器等等等等。




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