JVM基礎之java內存管理以及GC
內存管理簡介
內存管理的職責為分配內存,回收內存。
沒有自動內存管理的語言/平臺容易發生錯誤。
典型的問題包括懸掛指針問題,一個指針引用了一個已經被回收的內存地址,導致程序的運行完全不可知。
另一個典型問題為內存泄露,內存已經分配,但是已經沒有了指向該內存的指針,導致內存泄露。
程序員要花費大量時間在調試該類問題上。
GC簡介
因此引入了Garbage Collector機制,由運行時環境來自動管理內存。
Garbage Collector解決了懸掛指針和內存泄露大部分的問題(不是全部)。
注意Garbage Collector(簡稱Collector)和Garbage Collection(簡稱GC)的區別。
Collector的職責:
分配內存。
保證有引用的內存不被釋放。
回收沒有指針引用的內存。
對象被引用稱為活對象,對象沒有被引用稱為垃圾對象/垃圾/垃圾內存,找到垃圾對象并回收是Collector的一個主要工作,該過程稱為GC。
Collector一般使用一個稱為堆的內存池來進行內存的分配和回收。
一般的,當堆內存滿或者達到一個閥值時,堆內存或者部分堆內存被GC。
好的Collector的特性
保證有引用的對象不被GC。
快速的回收內存垃圾。
在程序運行期間GC要高效,盡量少的影響程序運行。和大部分的計算機問題一樣,這是一個關于空間,時間,效率平衡的問題。
避免內存碎片,內存碎片導致占用大量內存的大對象內存申請難以滿足。可以采用Compaction技術避免內存碎片。Compaction技術:把活對象移向連續內存區的一端,回收其余的內存以便以后的分配。
良好的擴展性,內存分配和GC在多核機器上不應該成為性能瓶頸。
設計或選擇Collector
串行或并行。
串行Collector在多核上也只有一個線程在運行,并行Collector可以同時有多個線程執行GC,但是其算法更復雜。
并發或Stop the World。
Stop the World Collection執行GC時,需要凍住所有內存,因此更簡單一些,但是,在GC時,程序是被掛起的。并發GC時,程序和GC同時執行,當然,一般的并發GC算法還是需要一些Stop the World時間。
Compacting or Non-compacting or Copying
Compacting: 去除內存碎片,回收內存慢,分配內存快。
Non-compacting: 容易產生內存碎片,回收內存快,分配內存慢,對大對象內存分配支持不好。
Copying: 復制活對象到新的內存區域,原有內存直接回收,需要額外的時間來做復制,額外的空間來做存儲。
GC性能指標
Throughput: 程序時間(不包含GC時間)/總時間。
GC overhead: GC時間/總時間。
Pause time: GC運行時程序掛起時間。
Frequency of GC: GC頻率。
Footprint: a measure of size, such as heap size。
Promptness:對象變為垃圾到該垃圾被回收后內存可用的時間。
依賴于不同的場景,對于GC的性能指標的關注點也不一樣。
分代GC
分代GC把內存劃分為多個代(內存區域),每個代存儲不同年齡的對象。 常見的分為2代,young和old。
分配內存時,先從young代分配,如果young代已滿,可以執行GC(可能導致對象提升),如果有空間,則分配,如果young代還是沒有空間,可以對整個內存堆GC。
young代GC后還存活的對象可以提升到old代。
該機制基于以下觀察事實:
1 大部分新分配的對象很快就沒有引用了,變成垃圾。
2 很少有old代對象引用young代對象。
基于代內存存儲對象的特性,對不同代的內存可以使用不同的GC算法。
Young代GC需要高效,快速,頻繁的執行,關注點主要在速度上。
Old代由于增長緩慢,因此GC不頻繁,但是其內存空間比較大,因此,需要更長時間才能執行完GC。關注點在內存空間利用率上。
Java Collector
Jvm的內存分為3代。Young, Old, Permanent。
大部分對象存儲在Young代。
在Young代中經歷數次GC存活的對象可以提升到Old代,大對象也可以直接分配到Old代。
Permanent代保存虛擬機自己的靜態(refective)數據,例如類(class)和方法(method)對象。
Young代由一個Eden和2個survivor組成。大部分的對象的內存分配和回收在這里完成。
Survivor存儲至少經過一次GC存活下來的對象,以增大該對象在提升至old代前被回收的機會。2個survivor中有一個為空。分別為From和to survivor。
當young代內存滿,執行young代GC(minor GC)。
當old或permanent代內存滿,執行full GC(major GC),所有代都被GC。一般先執行young GC,再執行old, permanent GC。
有時old代太滿,以至于如果young GC先運行,則無法存儲提升的對象。這時,Young GC不運行,old GC算法在整個堆上運行(CMS collector是個例外,該collector不能運行在young 代上)。
快速內存分配
大部分的內存分配請求發生時,Collector都有一塊大的連續內存塊,簡單的內存大小計算和指針移動就可以分配內存了。因此非常快速。該技術稱為bump –the-pointer技術。
對于多線程的內存分配,每個線程使用Thread Local Allocation Buffer(TLAB)進行分配,因此還是很高效。TLAB可以看作一個線程的特殊代。只有TLAB滿的時候才需要進行同步操作。
GC根集合
GC運行時當前程序可以直接訪問的對象。如線程中當前調用棧的方法參數,局部變量,靜態變量,當前線程對象等等。
Collector根據GC根集合來尋找所有活對象。GC根集合不可達對象自然就是垃圾了。
Serial Collector
單線程,Young and old GC是串行,stop the world GC的。
Young GC。
Eden中活對象copy到to survivor中,大對象直接進old代。
From survivor中相對老的活對象進入old代,相對年輕的對象進入to survivor中。
如果to survivor放不下活對象,則這些活對象直接進入old。
經歷過young GC,Eden和from survivor都變成空的內存區域,to survivor存儲有活的對象。To survivor和from survivor角色互換。
Old permanent GC。
Mark-sweep-compact算法。
S1 標識哪些對象是活的對象。
S2 標識哪些對象是垃圾。
S3 把活的對象壓縮到內存的一端,以便可以使用bump –the-pointer處理以后的內存分配請求。
非server-class machine 的默認GC。
也可以使用命令行參數來設定。
-XX:+UseSerialGC
Parallel Collector/Throughput Collector
利用了現代計算機大部分都是多核的事實。
Young GC。
和Serial Collector一樣,是一個stop the world和copying Collector。只不過是多線程并行掃描和做copy,提高速度,減少了stop the world的時間,增大了throughput。
Old permanent GC。
和serial collector一樣。Mark-sweep-compact算法。單線程。
Server-class machine的默認GC。
也可以使用命令行參數來設定。
-XX:+UseParallelGC
Parallel Compacting Collector
Young GC。
和Parallel Collector一樣。
Old Permanent GC。
Stop the world,并且多線程并發GC。
每一代被劃分為一些長度固定的區域。
第1步(mark phase),GC根集合劃分后分發給多個GC線程,每個GC線程更新可達活對象所在區域的信息(活對象的內存位置,大小)。
第2步(summary phase),操作在區域上,而不是對象上。由于以前GC的影響,內存的一端活對象的密度比較高,在該階段找到一個臨界點,該臨界點以前的區域由于活對象內存密度高,不參與GC,不做compact。該臨界點之后的區域參與GC,做compact。該階段為單線程執行。
第3步(compact phase)。GC多線程使用summary info做回收和compact工作。
可以設置GC線程數,防止GC線程長時間占有整臺機器的資源。
-XX:ParallelGCThreads=n
使用命令行參數來設定。
-XX:+UseParallelOldGC
Concurrent Mark Sweep Collector (CMS)
Young GC。
和Parallel Collector一樣。
Old permanent GC。
GC和程序并發執行。
Initial Phase:短暫停,標記GC根集合。單線程執行。
Concurrent marking phase: GC多線程標記從根集合可達的所有活對象。程序和GC并發運行。由于是并發運行,有可能有活對象沒有被標記上。
concurrent pre-clean:單線程,并發執行。
Remark phase: 短暫停,多線程標記在Concurrent marking phase中有變化的相關對象。
Concurrent sweep phase:和程序并發執行。單線程執行。不做compacting。
concurrent reset:單線程,并發執行。
CMS不做compacting,不能使用bump-the-pointer技術,只能使用傳統的內存空閑鏈表技術。
導致內存分配變慢,影響了Young代的GC速度,因為Young的GC如果有對象提升的話依賴于Old的內存分配。
CMS需要更多的內存空間,因為mark phase時程序還是在運行,程序可以申請更多的old空間。在mark phase中,CMS保證標識活對象,但是該過程中,活對象可能轉變為垃圾,只能等待下一次GC才能回收。
和其他Collector不同,CMS不是等到old滿時才GC,基于以前的統計數據(GC時間,Old空間消耗速度)來決定何時GC。CMS GC也可以基于old空間的占用率。
命令行參數:
-XX:CMSInitiatingOccupancyFraction=n,n為百分比,默認68。
可以設置
-XX:+UseCMSInitiatingOccupancyOnly 來使vm只使用old內存占用比來觸發CMS GC。
Incremental Mode。
CMS的concurrent phase可以是漸進式執行。以減少程序的一次暫停時間。
命令行參數:
-XX:+UseConcMarkSweepGC
-XX:+CMSIncrementalMode
4種Collector的對比和適用場景。
直到jdk1.3.1,java只提供Serial Collector,Serial Collector在多核的機器上表現比較差。主要是throughput比較差。
大型應用(大內存,多核)應該選用并行Collector。
Serial Collector:大多數client-style機器。對于低程序暫停時間沒有需求的程序。
Parallel Collector:多核機器,對于低程序暫停時間沒有需求的程序。
Parallel Compacting Collector:多核機器,對于低程序暫停時間有需求的程序。
CMS Collector:和Parallel Compacting Collector相比,降低了程序暫停時間,但是young GC程序暫停時間變長,需要更大的堆空間,降低了程序的throughput。
Ergonomics
J2SE 5.0后,Collector的選擇,堆大小的選擇,VM(client還是server)的選擇,都可以依賴平臺和OS來做自動選擇。
JVM會自動選擇使用server mode還是client mode。但是我們一樣可以手工設置。
java -server -client
Server-class machine的選擇:
2個或更多的處理器
And
2G或更多的物理內存
And
不是32bits,windows OS。
Client-class
The client JVM
The serial collector
Initial heap size = 4M
Max heap size=64M
Server-class
The server JVM
The parallel collector
Initial heap size= 1/64物理內存(>=32M),最大1G。
Max heap size=1/4物理內存,最大1G。
基于行為的調優。
可以基于最大暫停時間或throughput。
-XX:MaxGCPauseMillis=n
指示vm調整堆大小和其他參數來滿足這個時間需求。如果vm不滿足該目標,則減小堆大小來滿足該目標。該目標沒有默認值。
-XX:GCTimeRatio=n
GC time/APP time=1/(1+n)
如n=99表示GC時間占整個運行時間的1%。
如果該目標不能滿足,則增大堆大小來滿足該目標。默認值n=99。
Footprint Goal
如果最大暫停時間和Throughput目標都滿足了,則減少堆大小直到有一個目標不滿足,然后又回調。
目標優先級:
最大暫停時間>Throughput>footprint。
GC調優
由于有了Ergonomics,第一個建議就是不要手工去配置各種參數。讓系統自己去根據平臺和OS來選擇。然后觀測性能,如果OK的話,呵呵,不用搞了。
但是Ergonomics也不是萬能的。因此還是需要程序員來手工搞。
注意性能問題一定要測量/調優/測量/調優不停的循環下去。
Vm mode 選擇。
Java -server server mode.
Java -client client mode.
觀測性能主要使用gc的統計信息。
-XX:+PrintGC 輸出GC信息。
-XX:+PrintGCDetails輸出GC詳細信息。
-XX:+PrintGCTimeStamps 輸出時間戳,和–XX:+PrintGC 或–XX:+PrintGCDetails一起使用。
-Xloggc: gc.log 輸出到指定文件。
1 決定堆內存大小。
決定整個堆內存的大小。內存的大小對于Collector的性能影響是最大的。
使用以下參數來決定堆內存的大小。
可以決定堆空間的起始值和最大值,大型程序可以考慮把起始值調大,避免程序啟動時頻繁GC和內存擴展申請。
以及堆空間中可用內存的比例范圍,vm會動態管理堆內存來滿足該比例范圍。
-XX:MinHeapFreeRatio=n
-XX:MaxHeapFreeRatio=n
-Xmsn Young和Old的起始內存
-Xmxn Young和Old的最大內存
2 決定代空間大小。
Young代空間越大,則minor GC的頻率越小。但是,給定堆內存大小,Young代空間大,則major GC頻率變大。
如果沒有過多的Full GC或者過長的暫停時間問題,給young代盡量大的空間。
Young Generation Guarantee。
對于serial collector,當執行minor GC時,必須保證old代中有可用的空間來處理最壞情況(即eden和survivor空間中的對象都是活對象,需要提升至old空間),如果不滿足,則該minor GC觸發major GC。所以對于serial collector,設置eden+survivor的內存不要大過old代內存。
其他collector不做該保證,只有old代無法存儲提升對象時才觸發major GC。
對于其他collector,由于多線程做minor GC時,考慮到最壞情況,每個線程要在old代內存預留一定空間做對象提升,因此可能導致內存碎片。因此old代內存應該調整的更大一些。
-XX:NewSize=n young代空間下限。
-XX:MaxNewSize=n young代空間上限。
-XX:NewRatio=n young和old代的比例。
-XX:SurvivorRatio=n Eden和單個survivor的比例。
-XX:PermSize=n Permanent起始值。
-XX:MaxPermSize=n Permanent最大值。
3 決定使用Collector
可以考慮是否需要換一個Collector。
Collector選擇
-XX:+UseSerialGC Serial
-XX:+UseParallelGC Parallel
-XX:+UseParallelOldGC Parallel compacting
-XX:+UseConcMarkSweepGC Concurrent mark–sweep (CMS)
Parallel和Parallel Compacting Collector.
-XX:ParallelGCThreads=n
-XX:MaxGCPauseMillis=n
-XX:GCTimeRatio=n
設定目標好于明確設定參數值。
為了增大Throughput,堆大小需要變大。可以把堆大小設為物理內存允許的最大值(同時程序不swapping)來檢測該環境可以支持的最大throughput。
為了減小最大暫停時間和footprint,堆大小需要變小。
2個目標有一定的矛盾,因此要視具體應用場景,做平衡。
CMS Collector
-XX:+CMSIncrementalMode 和CMS同時使用。
-XX:+CMSIncrementalPacing 和CMS同時使用。
-XX:ParallelGCThreads=n
-XX:CMSInitiatingOccupancyFraction=n,n為百分比,默認68。
OutOfMemoryError
可以指定
-XX:+HeapDumpOnOutOfMemoryError
當發生OutOfMemoryError時dump出堆內存。
發生OutOfMemoryError時可以觀測該Error的詳細信息。
Java heap space:
調整堆大小。
程序中含有大量帶有finalize方法的對象。執行finalize方法的線程頂不住了。
PermGen space:
Permanent代內存不夠用了。
Requested array size exceeds VM limit。
堆內存不夠用。
程序bug,一次分配太多內存。
freeMemory(),totalMemory(),maxMemory()
java.lang.Runtime類中的 freeMemory(), totalMemory(), maxMemory()這幾個方法的反映的都是 java這個進程的內存情況,跟操作系統的內存根本沒有關系。
maxMemory()這個方法返回的是java虛擬機(這個進程)能構從操作系統那里挖到的最大的內存,以字節為單位,如果在運行java程序的時 候,沒有添加-Xmx參數,那么就是jvm默認的可以使用內存大小,client為64M,server為1G。如果添加了-Xmx參數,將以這個參數后面的值為準。
totalMemory()這個方法返回的是java虛擬機現在已經從操作系統那里挖過來的內存大小,也就是java虛擬機這個進程當時所占用的所有內存。如果在運行java的時候沒有添加-Xms參數,那么,在java程序運行的過程的,內存總是慢慢的從操作系統那里挖的,基本上是用多少挖多少,直 到挖到maxMemory()為止,所以totalMemory()是慢慢增大的。如果用了-Xms參數,程序在啟動的時候就會無條件的從操作系統中挖 -Xms后面定義的內存數,然后在這些內存用的差不多的時候,再去挖。
freeMemory()是什么呢,剛才講到如果在運行java的時候沒有添加-Xms參數,那么,在java程序運行的過程的,內存總是慢慢的從操 作系統那里挖的,基本上是用多少挖多少,但是java虛擬機100%的情況下是會稍微多挖一點的,這些挖過來而又沒有用上的內存,實際上就是 freeMemory(),所以freeMemory()的值一般情況下都是很小的,但是如果你在運行java程序的時候使用了-Xms,這個時候因為程 序在啟動的時候就會無條件的從操作系統中挖-Xms后面定義的內存數,這個時候,挖過來的內存可能大部分沒用上,所以這個時候freeMemory()可能會有些大。
jmap工具的使用
jmap pid 查看共享對象。
jmap -heap pid 查看java進程堆的相關信息。
$ jmap -heap 5695
Attaching to process ID 5695, please wait... Debugger attached successfully. Server compiler detected. JVM version is 17.0-b16 using parallel threads in the new generation. using thread-local object allocation. Concurrent Mark-Sweep GC Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 1342177280 (1280.0MB) NewSize = 134217728 (128.0MB) MaxNewSize = 134217728 (128.0MB) OldSize = 4194304 (4.0MB) NewRatio = 2 SurvivorRatio = 20000 PermSize = 100663296 (96.0MB) MaxPermSize = 134217728 (128.0MB) Heap Usage: New Generation (Eden + 1 Survivor Space): capacity = 134152192 (127.9375MB) used = 34518744 (32.919639587402344MB) free = 99633448 (95.01786041259766MB) 25.731032408326207% used Eden Space: capacity = 134086656 (127.875MB) used = 34518744 (32.919639587402344MB) free = 99567912 (94.95536041259766MB) 25.743608670500368% used From Space: capacity = 65536 (0.0625MB) used = 0 (0.0MB) free = 65536 (0.0625MB) 0.0% used To Space: capacity = 65536 (0.0625MB) used = 0 (0.0MB) free = 65536 (0.0625MB) 0.0% used concurrent mark-sweep generation: capacity = 671088640 (640.0MB) used = 287118912 (273.81793212890625MB) free = 383969728 (366.18206787109375MB) 42.7840518951416% used Perm Generation: capacity = 100663296 (96.0MB) used = 41864504 (39.92510223388672MB) free = 58798792 (56.07489776611328MB) 41.58864816029867% used
jmap –histo pid 查詢各種對象占用的內存大小。
$ jmap -histo 5695 | less num #instances #bytes class name ---------------------------------------------- 1: 320290 63305456 [C 2: 1457010 46624320 java.util.concurrent.ConcurrentHashMap$Segment 3: 1502500 36060000 java.util.concurrent.locks.ReentrantLock$NonfairSync 4: 87785 29987632 [I 5: 1457010 23638928 [Ljava.util.concurrent.ConcurrentHashMap$HashEntry; 6: 285668 15240784 [Ljava.lang.Object; 7: 87239 106801608: 399482 9587568 java.lang.String 9: 16533 7466624 [B 10: 91065 7285072 [Ljava.util.concurrent.ConcurrentHashMap$Segment; 11: 87239 6983288 12: 125750 5868720 13: 45409 5449080 java.net.SocksSocketImpl 14: 63574 4936176 [S 15: 45294 4710576 sun.nio.ch.SocketChannelImpl
jmap –permstat pid 查看Class Loader。
jmap –dump:file=filename,format=b pid dump內存到文件。
可以使用Jprofiler工具分析java dump文件。