JVM基礎之java內存管理以及GC

jopen 10年前發布 | 28K 次閱讀 JVM Java開發

內存管理簡介 


內存管理的職責為分配內存,回收內存。 

沒有自動內存管理的語言/平臺容易發生錯誤。 
典型的問題包括懸掛指針問題,一個指針引用了一個已經被回收的內存地址,導致程序的運行完全不可知。 
另一個典型問題為內存泄露,內存已經分配,但是已經沒有了指向該內存的指針,導致內存泄露。 
程序員要花費大量時間在調試該類問題上。 

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。關注點在內存空間利用率上。 
JVM基礎之java內存管理以及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 代上)。 

JVM基礎之java內存管理以及GC 


快速內存分配 


大部分的內存分配請求發生時,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       10680160    
   8:        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文件。

 

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!