
堆分配參數:
-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程序響應很慢時如何查找問題

