1. 程式人生 > >《深入理解Java虛擬機器》讀書筆記

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

-------------------------------------------------------------------------------------------------------------------------

堆分配引數:

-XX:+PrintGC 使用該引數,虛擬機器啟動後,只要遇到GC就會列印日誌;

-XX:+UseSerialGC 配置序列回收器;

-XX:+PrintGCDeltails 可以檢視詳細資訊,包括各個區的情況

-Xms:設定Java程式啟動時初始堆的大小(主要引數)

-Xmx:設定Java程式能獲得的最大堆大小(主要引數)

新生代的配置:

-Xmn: 可以設定新生代的大小,設定一個比較大的新生代會減少老年代的大小,這個設定對系統性能以及GC行為有很大的影響,新生代大小一般會設定整個堆空間的1/3到1/4左右。

備註:在實際工作中,可以直接將初始的堆大小與最大堆大小設定相等,這樣的好處是可以減少程式執行時的垃圾回收次數,從而提高效能。

常見異常:

         java.lang.OutOfMemoryError.Java heap spacess---heap

                   JVM中如果98%的時間是用於GC且可用的Heap size不足2%的時候將丟擲此異常資訊。

                   -Xms -Xmx

         java.lang.OutOfMemoryError.PermGen space ---no heap

                   -XX:PermSize

                   -XX:MaxPermSize

         StrackOverflowError

                   Java虛擬機器在執行時,呼叫方法時,都需要建立棧幀,當棧的空間不夠時就會產生StrackOverflowError

                            -Xss

JVM中的引數:

         以-X開頭的都是非標準的(這些引數並不保證在所有的JVM上都被實現)。

                   -Xmx

                   -Xmn

                   -Xms

         以-XX開頭的都是不穩定的並且不推薦在生產環境中使用,這些引數的改動也不會發布通知。

                   -XX:Permsize

                   -XX:MaxPermsize

JVM選項的說明:

         布林型引數選項:-XX+表示開啟,-XX-表示關閉。(-XX.+PrintGCDetails)

         數字型引數選項通過-XX=設定。

         字元型引數選項通過-XX=設定,通常用來指定一個檔案、路徑或者一個命令列表

         -Xms:初始堆大小

         -Xmx:最大堆大小

         -XX:NewSize=n 設定年輕代的大小

         -XX:NewRatio=n 設定年輕代和老年代的比值。如3則表示年輕代和老年代的比值為1:3

         -XX:SurvivorRatio=n 年輕代中Eden與兩個Survivor區的比值。如為3,則表示Eden:Survivor=3:2。一個Survivor佔整個年輕代的1/5.

         -XX:MaxPermSize=n 設定永久代的大小

JVM GC

         序列回收器(Serial Collector)

         並行回收器(Parallel Collector)

         並行合併回收器(Parallel Compacting Collection)

         併發標記清除回收器(Concurrent Mark-Sweep Collector)應用廣泛

         G1垃圾收集器(Jdk7+)未來的主流

         並行演算法是用多執行緒進行垃圾回收,回收期間會暫停程式的執行,而併發演算法,也是多執行緒回收,但期間不停止應用執行。所以,併發演算法適用於互動性高的一些程式。經過觀察,併發演算法會減少年輕代的大小,其實就是使用了一個大的年老代,這反過來跟並行演算法相比吞吐量相對較低。

JVM垃圾回收時間:

         當年輕代記憶體滿時,會引發一次普通GC,該GC僅回收年輕代。需要強調的是,年輕代滿是指Eden代滿,Survivor滿不會引發GC

         當老年代滿時會引發Full GC,Full GC將會同時回收年輕代、年老代

         當永久代滿時也會引發Full GC,會導致Class、Method元資訊的解除安裝

常見問題:

   Q:為什麼崩潰前垃圾回收的時間越來越長?

   A:根據記憶體模型和垃圾回收演算法,垃圾回收分兩部分:記憶體標記、清除(複製),標記部分只要記憶體大小固定時間是不變的,變的是複製部分,因為每次垃圾回收都有一些回收不掉的記憶體,所以增加了複製量,導致時間延長。所以,垃圾回收的時間也可以作為判斷記憶體洩漏的依據

   Q:為什麼Full GC的次數越來越多?

   A:因為記憶體的積累,逐漸耗盡了年老代的記憶體,導致新物件分配沒有更多的空間,從而導致頻繁的垃圾回收

   Q:為什麼年老代佔用的記憶體越來越大?

   A:因為年輕代的記憶體無法被回收,越來越多地被Copy到年老代

   Q:什麼是序列回收和並行回收?

   A:序列回收是指在同一時間段內只允許一件事情發生,即當多個CPU可用時,也只能有一個CPU用於執行垃圾回收操作,並且在執行垃圾回收時,程式中的工作執行緒將會被暫停。當垃圾回收工作完成後才會恢復之前被暫停的工作執行緒,這就是序列回收。

   並行回收是指可以運用多個CPU同時執行垃圾回收,因此提升了應用程式的吞吐量,不過並行垃圾回收仍然使用了“Stop-the-World”機制和複製演算法。

   Q:什麼是併發和“Stop-the-World”機制?

   當通過“Stop-the-World”機制回收垃圾時,垃圾收集器會在記憶體回收的過程中暫停程式中的所有工作執行緒,直至完成記憶體回收工作後才會恢復之前被暫停的工作執行緒。

   併發回收是指在同一時間段內,應用程式的工作執行緒和垃圾回收執行緒將會同時執行或者交叉執行。

Q:什麼是快速分配策略?

A:基於執行緒安全的考慮,如果一個類在分配記憶體之前已經成功完成裝載步驟之後,JVM就會優先選擇在TLAB(Thread Local Allocation本地執行緒分配緩衝區)中為物件例項分配記憶體空間,TLAB在Java堆區中是一塊執行緒私有的區域,包含在Eden空間內,除了可以避免一系列的非執行緒安全問題外,同時還能夠提升記憶體分配的吞吐量,因此我們可以將這種記憶體分配方式稱之為快速分配策略。

Q:什麼是逃逸分析和棧上分配?

A:Java堆區已經不再是物件分配記憶體的唯一選擇,如果希望降低GC的回收平率和提升GC的回收效率,那麼則可以使用堆外儲存技術,目前最常見的堆外儲存技術就是利用逃逸分析技術篩選出未發生逃逸的物件,然後避開堆區而直接選擇在棧幀中分配記憶體空間。逃逸分析是JVM在執行效能優化之前的一種分析技術,它的具體目標就是分析出物件的作用域。即當一個物件被定義在方法體內部之後,它的受訪許可權僅限於方法體內,一旦其引用被外部成員引用後,這個物件就發生了逃逸。反之如果定義在方法體內部的物件並沒有被任何外部成員引用時,JVM就會為其在棧幀中分配記憶體空間。

HeapOutOfMemory 當堆上分配的物件大於指定堆的最大值時,丟擲該錯。 可以使用-XX:+HeapDumpOnOutOfMemoryError 檢視記憶體快照進行分析

MethodArea OutOfMemory 方法區記憶體不足,存放類資訊,常量,靜態變數,即時編譯後的程式碼,檢查這幾個資訊是否有異常 大多的原因是因為動態產生過多的類。

ConstantPool OutOfMemory 常量池溢位,檢視是否intern使用不當

DirectMemory OutOfMemory 本機直接記憶體溢位,容量可通過-XX:MaxDirectMemorySize指定,如果不指定,預設和堆最大值相同。這個溢位發生在系統進行直接記憶體分配。例如:unsafe.allocateMemory() 特徵為:OOM後發現Dump問價你很小,程式中直接或間接使用了NIO

Stack OutOfMemory 擴充套件棧時無法獲取足夠的記憶體空間,在建立執行緒時 解決方法之一:減少最大堆

Stack OverFlow 棧深度大於虛擬機器所允許的深度,經常是由於死迴圈的遞迴呼叫

第三章:垃圾收集器和記憶體分配策略

1、JVM垃圾回收區域:

程式計數器、虛擬機器棧、本地方法棧三個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊的執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本是在類結構確定下來時就已知的,因此這三個區域的記憶體分配和回收都具有確定性,在這三個區域內不需要過多考慮回收的問題,因為方法結束或者執行緒結束時,記憶體自然就跟著回收了。而java堆和方法區不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也不一樣,我們只有在程式執行時才能知道會建立哪些物件,這部分記憶體的分配和回收是動態的,垃圾收集器所關注的是這部分記憶體。

2、引用計數演算法:

         給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效計數器值就減1;任何時刻計數器都為0的物件就是不可能再被使用的。Java語言並沒有選用引用計數器演算法來管理記憶體,其中最主要的原因就是它很難解決物件之間相互迴圈引用的問題。

3、根搜尋演算法:

         Java使用根搜尋演算法(GC Root Tracing)判斷物件是否存活。該演算法的基本思路是:通過一系列的名為“GC Root”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個物件不可達)時,則證明此物件是不可引用的,所以它們將會被判定為可回收物件。在Java語言中可作為GC Roots的物件包括下面幾種:

虛擬機器棧(棧幀中的本地變量表)中的引用的物件;

         方法區中的類靜態屬性引用的物件;

         執行時常量池中的物件引用;

         方法區中的常量引用的物件;

         本地方法棧中JNI(即一般所說的native方法)引用的物件。

         物件引用:無論是通過引用計數器演算法判斷物件的引用數量,還是通過根搜尋演算法判斷物件的引用鏈是否可達,判斷物件是否存活都與“引用”有關。JDK1.2之後,Java將引用分為四種:強引用,軟引用,弱引用,虛引用。這四種引用強度依次逐漸減弱。

4、回收方法區:

       永久代(方法區)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的物件非常相似。以常量池中的字面量的回收為例,假如一個字串“abc”已經進入了常量池中,但是當前系統沒有任何一個String物件叫做“abc”的,也沒有其他地方引用了這個字面量,如果這個時候發生記憶體回收,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。判斷一個類是否是“無用的類”需要滿足下面三個條件:

         該類所有的例項都已經被回收,即Java堆中不存在該類的任何例項;

         載入該類的ClassLoader已經被回收;

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

5、垃圾回收演算法之標記-清除演算法:

         這是最基礎的收集演算法。分為兩個階段,標記和清除。首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件。

         缺點:效率低下;

         空間問題,標記清除之後會產生大量不連續的記憶體碎片。

6、垃圾回收演算法之複製演算法:

         該演算法將記憶體按容量劃分為大小相等的兩塊區域,每次只使用其中的一塊。當一塊記憶體用完了,就將其中還存活的物件複製到另一塊區域上,然後再將已經使用過的記憶體區域一次性清理掉。解決了記憶體碎片的問題。

         說明:現在的商業虛擬機器都是採用這種收集演算法來回收新生代,IBM的專門研究表明,新生代中的物件98%是朝生夕死的,所有並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時候,將Eden和Survivor中還存活的物件一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛剛用過的Survivor的空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1:1。

                   缺點:在物件存活率較高時需要執行較多的複製操作,效率將會變低,老年代不能使用這種演算法。

7、垃圾回收演算法之標記-整理演算法:

         根據老年代的特點,有人提出了“標記-整理”演算法。其中標記過程和“標記-清除”演算法一樣,但是後續步驟不是直接對可回收的物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

8、垃圾回收演算法之分代收集演算法:

         當前商業虛擬機器的垃圾回收都是採用“分代收集”演算法,根據物件的存活週期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適合的收集演算法。在新生代中,每次垃圾收集時都會發現有大量物件死去,只有少量物件存活,那就選擇複製演算法;而老年代中因為物件存活率較高,沒有額外空間對它進行分配擔保,就必須採用“標記-清除”或者“標記-整理”演算法來進行回收。

9、垃圾收集器之Serial收集器:

        Serial收集器是最基本、歷史最悠久的收集器。該收集器是一個單執行緒的收集器,即在進行垃圾收集時候,必須暫停其他所有的工作執行緒,直到它收集結束。到目前為止,它依然是虛擬機器執行在Client模式下的預設新生代收集器。優點是簡單而高效。目前停頓時間可以控制在幾十毫秒最多一百多毫秒以內。

10、垃圾收集器之G1收集器:

         G1(Garbage first)收集器是當前收集器技術發展的最前沿成果。G1收集器是垃圾收集器理論進一步發展的產物,它與CMS收集器相比有兩個顯著的改進:一是G1收集器是基於“標記-整理”演算法實現的收集器;二是它可以非常精準地控制停頓,既能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

         G1收集器可以實現在基本不犧牲吞吐量的前提下完成低停頓的記憶體回收,這是由於它能夠極力的避免全區域的垃圾收集,G1收集器將整個Java堆(包括新生代、老年代)劃分為多個大小固定的獨立區域(Region),並且跟蹤這些區域裡面的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(這就是G1名稱的由來)。

垃圾收集器比較

垃圾收集器

演算法

方式

堆區域

機制

Serial收集器

複製演算法

序列

新生代

Stop-the-World

Serial Old收集器

標記-壓縮演算法

序列

老年代

Stop-the-World

ParNew收集器

複製演算法

並行

新生代

Stop-the-World

Parallel收集器

複製演算法

並行

新生代

Stop-the-World

Parallel Old收集

標記-壓縮演算法

並行

老年代

Stop-the-World

CMS收集器

標記-清除演算法

並行

老年代

Stop-the-World/併發

G1收集器

整個堆區

記憶體選項配置

選項

描述

備註

-Xms

設定Java堆區的初始記憶體

當可用的Java堆區記憶體小於40%時,JVM就會將記憶體調整到選項-Xmx所允許的最大值

-Xmx

設定Java堆區的最大記憶體

當可用的Java堆區記憶體大於70%時,JVM就會將記憶體調整到選項-Xms所指定的初始值

-Xmn

設定新生代(YoungGen)的記憶體

-Xmn的記憶體大小為Eden+2個Surivivor空間的值,官方建議配置為整個堆的3/8

-XX:NewSize

設定新生代(YoungGen)的初始記憶體

和選項-Xmn等價,但是推薦使用-Xmn,相當於一次性設定了NewSize/Max-NewSize的記憶體大小

-XX:MaxNewSize

設定新生代(YoungGen)的最大記憶體

-XX:NewRatio

新生代(Eden+2個Surivivor空間)與老年代的比值,不包括永久代

選項-XX:NewRatio=4時,表示新生代與老年代所佔的比值為1:4。如果已經設定了選項-Xmn,則無需設定該選項

-XX:PermSize

設定方法區的初始記憶體

-XX:MaxPermSize

設定方法區的最大記憶體

-XX:SurivivorRatio

Eden空間與2個Surivivor空間的比值大小

Eden空間和另外2個Surivivor空間預設所佔的比值為8:1

-XX:TLABWasteTargetPercent

設定TLAB空間所佔用Eden空間的百分比大小

GC組合配置

GC組合

Minor GC

Full GC

描述

-XX:+UserSerialGC

Serial收集器序列回收

Serial Old收集器序列回收

選項-XX:UseSerialGC

可以手動指定使用Serial收集器+Serial Old收集器組合執行記憶體回收

-XX:+UseParNewGC

ParNew收集器並行回收

Serial Old收集器序列回收

選項-XX:UseParNewGC可以手動指定使用ParNew收集器+Seral Old收集器組合執行記憶體回收

-XX:+UseParallelGC

Parallel收集器並行回收

Serial Old收集器序列回收

通過-XX:+UseParallelGC可以手動指定使用Parallel收集器和Serial Old收集器組合執行記憶體回收

-XX:+UseParallelOldGC

Parallel收集器並行回收

Parallel Old收集器並行回收

通過-XX:+UseParallelOldGC可以手動指定使用Parallel收集器和Parallel Old收集器組合執行記憶體回收

-XX:+UseConcMarkSweepGC

ParNew收集器並行回收

預設使用CMS收集器併發執行回收,備用採用Serial Old收集器序列回收

使用-XX:+UseConcMarkSweepGC可以手動指定使用ParNew收集器+CMS收集器+Serial Old收集器組合執行記憶體回收。優先使用ParNew+CMS組合,當出現ConcurrentNode Failure或者Promotion Failed時,則採用ParNew+Serial Old組合。

-XX:+UseConcMarkSweepGC

-XX:+UseParNewGC

Serial 收集器序列回收

-XX:+UseG1GC

G1收集器併發、並行的記憶體回收

第七章:虛擬機器類載入機制

1、虛擬機器的類載入機制:

虛擬機器把描述類的資料從Class檔案載入到記憶體中,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。

2、類被載入到虛擬機器經過的7個階段:

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括了:載入、校驗、準備、解析、初始化、使用和解除安裝七個階段。其中校驗/驗證、準備和解析三個部分統稱為連線。

3、何時進行類的初始化?

虛擬機器規範嚴格規定了有且只有四種情況必須立即對類進行初始化(而載入、校驗、準備和解析自然需要在初始化之前開始):

1)遇到new、getstatic、putstatic、或者這4條位元組碼指令時,invokestatic如果類沒有進行初始化,則需要先觸發其初始化,生成這4條指令最常見的Java程式碼場景是:使用new關鍵字例項化物件的時候、讀取或者設定一個類的靜態欄位(被final修飾、已經在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候;

2)使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化;

3)當初始化一個子類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化;

4)當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main方法的那個類),虛擬機器會先初始化這個主類。

對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發其父類的初始化而不會觸發子類的初始化。

4、載入:

在載入階段,虛擬機器需要完成以下三件事情:

通過一個類的全限定名來獲取定義此類的二進位制位元組流;

將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;

在Java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。

載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中。然後在java堆中例項化一個java.lang.Class物件,這個物件將作為程式訪問方法區中的這些型別資料的外部介面,載入階段和連線階段的部分內容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,載入階段尚未完成,連線階段可能已經開始,但是這些夾在載入階段之中的動作,仍然屬於連線階段的內容,這兩個階段的開始時間仍然保持著固定的先後順序。

5、驗證:

驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

虛擬機器驗證過程的四個階段:

檔案格式驗證;

元資料驗證;

位元組碼驗證;

符號引用驗證。

6、準備:

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念,首先是這時候進行記憶體分配的僅包含類變數(被static修飾的變數),而不包含例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。其次是這裡所說的初始值“通常情況”下是資料型別的零值,假設一個類變數的定義為:public static int value=123;那麼變數value在準備階段過後的初始值為0,而不是123。

7、解析:

解析階段是虛擬機器將常量池內的符號引用替換成直接引用的過程。

符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關。引用的目標並不一定已經載入到記憶體中。

直接引用:直接引用可以是直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的控制代碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。

7、類與類載入器:

虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來獲取此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所修要的類。實現這個動作的程式碼模組被稱為“類載入器”。

類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠遠不限於類載入階段。對於任意一個類,都需要由載入它的類載入器和這個類本身一同確定其在Java虛擬機器中的唯一性。

8、雙親委派模型:

絕大部分Java程式都會使用到以下三種系統提供的類載入器:

1)啟動類載入器(BootStrap ClassLoader):在HotSpot虛擬機器中這個類載入器由C++語言實現,是虛擬機器自身的一部分。它負責將存放在<JAVA_HOME>\lib目錄中的,並且是虛擬機器標識的(僅按照檔名識別,如rt.jar,名稱不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被Java程式直接引用。

2)擴充套件類載入器(Extention ClassLoader):這個載入器有sun.misc.Launcher$ExtClassLoader實現。它負責載入<JAVA_HOME>/lib/ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。

3)應用程式類載入器(Application ClassLoader):這個類載入器由sun.misc.Launcher$AppClassLoader來實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫。如果應用程式沒有指定自定義的類載入器,一般情況下這個就是程式中預設的類載入器。

我們的應用程式都是由這三個類載入器互相配合進行載入的。

雙親委派模型的工作過程:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委託給父類載入器去完成。每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳遞到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需載入的類)時,子載入器才會自己嘗試去載入。

當一個Java程式響應很慢時如何查詢問題