深入理解Java虛擬機:[1]自動內存管理機制?

一:Java內存區域與內存溢出異常

在運行Java程序時,Java虛擬機會把管理的內存劃分為若干個不同的數據區域。

Java虛擬機運行時數據區

數據區域圖中,除了方法區和堆區是線程共享區外,其他三個是線程隔離的數據區(private)

程序計數器(Program Counter Register):屬於線程私有的,佔用的內存空間較少,可以看成是當前線程所執行字節碼的行號指示器,字節碼解釋器工作時就是通過改變這個計數器的值來選擇下一條,需要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能需要依賴這個計數器來完成,這個區域是jvm規範中沒有規定任何OutOfMemoryError情況區域。

虛擬機棧:和程序計數器一樣,都屬於線程私有,生命週期與線程相同,描述的是java方法執行的內存模型,每個方法執行都會創建一個棧幀,用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息,每一個方法被調用直至執行完成的過程,就對應一個棧幀在jvm stack 從入棧到出棧的過程.局部變量表存放了編譯期可知的各種數據基本類型(Boolean,byte,char,short,int,float,long,double),以及對象的引用。這個區域中定義了2種異常情況,如果線程請求的棧深度大於jvm所允許的深度,將拋出StackOverflowError異常,如果jvm可以動態擴張,當擴張無法申請到足夠的內存空間是會拋出OutOfMemoryError異常。(這些數據區域異常將在下面的例子都講到)。

本地方法棧:與虛擬機棧比較相似。其區別:虛擬機棧為虛擬機執行Java方法服務,而本地方法棧則為虛擬機使用Native方法服務。

堆(Heap):jvm中內存佔用最大的一塊,是所有線程共享的一塊內存區域.在jvm啟動時創建,存放的是所有對象實例(或數組),所有的對象實例都在這裡進行動態分配,當類空間無法再擴張會拋出OutOfMemoryError異常。Java堆是垃圾收集器管理的主要區域,而收集器採用分代收集算法。

方法區(Method Area):與堆類似,也是各個線程共享的內存區域,主要用來存儲加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據,當方法區無法滿足內存分配時,也拋出OutOfMemoryError異常。運行時常量池是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用相對於Class文件常量池的重要特徵是具備動態性(常量並非強制編譯期產生,運行期間也可以新增,例如String類的intern()方法)。

直接內存(DirectMemort):並不屬於數據區,也不屬於java定義的內存區域。由於NIO(New Input/Output)類,引入了一種基於通道與緩衝區(Buffer)的I/O方式。

對象訪問

Object object = new Object();

Object object 這部分存儲在java棧的本地變量表中,作為一個引用(reference)類型存在。

new Object() 這部分存儲在java堆中,形成了一塊存儲了Object類型所有的實例數據值的結構化內存,動態變化,長度不固定。

方法區:在java堆中,必須要找到此對象類型數據,比如,對象類型,基類,實現的接口,方法等都存放在方法區。

對象訪問方式有兩種:句柄和直接指針。

句柄:reference中存儲是對象的句柄地址,而句柄包含了對象實例數據和類型數據各自具體地址信息。好處:在對象移動時只需改變句柄中的實例數據指針,reference本身不需要修改。

直接指針:reference中直接存儲的就是對象地址。好處:速度快,它節省了一次指針定位的時間開銷。

實戰:OutOfMemoryError異常

1. Java堆溢出

調整虛擬機最小值(-Xms)和最大值(-Xmx),並通過參數-XX:+HeapDumpOnOutOfMemoryError生成快照。要解決這個區域的異常,通過內存映像分析工具對快照分析,確認內存中的對象是否是必要的,分清楚出現了內存洩露還是內存溢出。若是內存洩露,通過工具查看洩露對象到GCRoots引用鏈,找到洩露對象是通過怎樣的路徑與GCRoots相關聯並導致垃圾收集器無法自動回收。若不存在洩露,則檢查虛擬機堆參數與機器物理內存對比看是否還能調大或從代碼上檢查某些對象生命週期是否過長,嘗試減少程序運行期的內存消耗。

2. 虛擬機棧和本地方法棧溢出

調節棧容量大小(-Xss)。如果線程請求的棧深度大於虛擬機所允許的最大深度,將會拋出StackOverflowError異常。使用-Xss參數減小棧內存容量或者增加此方法幀中本地變量表的程度都使棧深度縮小。

3. 運行時常量池溢出

調節參數-XX:PermSize和-XX:MaxPermSize限制方法區的大小,然後使用String.intern()這個Native方法向常量池中添加內容。運行時常量池溢出,在OutOfMemoryError後面跟隨提示信息是“PermGen space”,說明運行時常量池屬於方法區(HotSpot虛擬機的永久代)的一部分。

4. 方法區溢出

同樣使用參數-XX:PermSize和-XX:MaxPermSize限制方法區的大小,然後不斷產生大量的class來加載到內存,從而出現OutOfMemoryError。所以在經常動態生成大量Class的應用中,需要特別注意類的回收狀況。

5. 本機直接內存溢出

通過參數-XX:MaxDirectMemorySize指定DirectMemory容量,若不指定則與Java堆最大值一樣。可以直接通過反射獲取Unsafe實例並進行內存分配,使用unsafe.allocateMemory()申請分配內存。不足時會出現OutOfMemoryError。

二.垃圾收集器與內存分配策略

概述

Java內存運行時區域的各個部分,其中程序計數器、VM棧、本地方法棧三個區域隨線程而生,隨線程而滅;棧中的幀隨著方法進入、退出而有條不紊的進行著出棧入棧操作。而Java堆和方法區(包括運行時常量池)則不一樣,我們必須等到程序實際運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的。

判斷對象已死

1)引用計數算法(對象中添加一個引用計數器,當有一個地方引用它,計數器加1,當引用失效,計數器減1,任何時刻計數器為0的對象就是不可能再被使用的),但引用計數算法無法解決對象循環引用的問題。

根搜索算法(通過一系列的稱為“GCRoots”的點作為起始進行向下搜索,當一個對象到GCRoots沒有任何引用鏈(ReferenceChain)相連,則證明此對象是不可用的),主流程序語言Java,c#都使用此算法。在Java語言中,GC Roots包括:

1.在VM棧(幀中的本地變量)中的引用。2.方法區中的靜態引用和常量引用的對象。3.JNI(即一般說的Native方法)中的引用。

2)生存還是死亡?

判定一個對象死亡,至少經歷兩次標記過程:如果對象在進行根搜索後,發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,並在稍後執行他的finalize()方法(如果它有的話)。這裡所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這點是必須的,否則一個對象在finalize()方法執行緩慢,甚至有死循環什麼的將會很容易導致整個系統崩潰。 finalize()方法是對象最後一次逃脫死亡命運的機會,稍後GC將進行第二次規模稍小的標記,如果在finalize()中對象成功拯救自己(只要重新建立到GC Roots的連接即可,譬如把自己賦值到某個引用上),那在第二次標記時它將被移除出“即將回收”的集合,如果對象這時候還沒有逃脫,那基本上它就真的離死不遠了。需要關閉外部資源之類的事情,基本上它能做的使用try-finally可以做的更好。

3)回收方法區

方法區即後文提到的永久代,很多人認為永久代是沒有GC的,這區GC的“性價比”一般比較低:在堆中,尤其是在新生代,進行一次GC可以一般可以回收70%~95%的空間,而永久代的GC效率遠小於此。但是目前方法區主要回收兩部分內容:廢棄常量與無用類。需要滿足下面3個條件:1.該類所有的實例都已經被GC,也就是JVM中不存在該Class的任何實例。2.加載該類的ClassLoader已經被GC。3.該類對應的java.lang.Class對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法。

垃圾收集算法

1.標記-清除算法(Mark-Sweep)

算法分成“標記”和“清除”兩個階段,首先標記出所有需要回收的對象,然後回收所有需要回收的對象。主要缺點有兩個,一是效率問題,標記和清理兩個過程效率都不高,二是空間問題,標記清理之後會產生大量不連續的內存碎片,空間碎片太多可能會導致後續使用中無法找到足夠的連續內存而提前觸發另一次的垃圾蒐集動作。

2.複製算法(Copying)

將內存分為一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊survivor,當回收時將eden和 survivor還存活的對象一次過拷貝到另外一塊survivor空間上,然後清理掉eden和用過的survivor。複製收集算法在對象存活率高的時候,效率有所下降。

3.標記-整理(Mark-Compact)算法

標記過程仍然一樣,但後續步驟不是進行直接清理,而是令所有存活的對象一端移動,然後直接清理掉這端邊界以外的內存。

4.分代收集(Generational Collection)算法

此算法只是根據對象不同的存活週期將內存劃分為幾塊。一般是把Java堆分作新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。

垃圾收集器

沒有最好的收集器,也沒有萬能的收集器,只有最合適的收集器。

1.Serial收集器單線程收集器,收集時會暫停所有工作線程(我們將這件事情稱之為Stop The World,下稱STW),使用複製收集算法,虛擬機運行在Client模式時的默認新生代收集器。2.ParNew收集器ParNew收集器就是Serial的多線程版本,除了使用多條收集線程外,其餘行為包括算法、STW、對象分配規則、回收策略等都與Serial收集器一摸一樣。對應的這種收集器是虛擬機運行在Server模式的默認新生代收集器,在單CPU的環境中,ParNew收集器並不會比Serial收集器有更好的效果。3.Parallel Scavenge收集器Parallel Scavenge收集器(下稱PS收集器)也是一個多線程收集器,也是使用複製算法,但它的對象分配規則與回收策略都與ParNew收集器有所不同,它是以吞吐量最大化(即GC時間佔總運行時間最小)為目標的收集器實現,它允許較長時間的STW換取總吞吐量最大化。4.Serial Old收集器Serial Old是單線程收集器,使用標記-整理算法,是老年代的收集器,上面三種都是使用在新生代收集器。5.Parallel Old收集器老年代版本吞吐量優先收集器,使用多線程和標記-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的話,老年代除Serial Old外別無選擇,因為PS無法與CMS收集器配合工作。6.CMS(Concurrent Mark Sweep)收集器CMS是一種以最短停頓時間為目標的收集器,使用CMS並不能達到GC效率最高(總體GC時間最小),但它能儘可能降低GC時服務的停頓時間,這一點對於實時或者高交互性應用(譬如證券交易)來說至關重要,這類應用對於長時間STW一般是不可容忍的。CMS收集器使用的是標記-清除算法,也就是說它在運行期間會產生空間碎片,所以虛擬機提供了參數開啟CMS收集結束後再進行一次內存壓縮。

內存分配與回收策略

分析實驗數據與結果。

總結

GC在很多時候都是系統併發度的決定性因素,虛擬機之所以提供多種不同的收集器,提供大量的調節參數,是因為只有根據實際應用需求、實現方式選擇最優的收集方式才能獲取最好的性能。沒有固定收集器、參數組合,也沒有最優的調優方法,虛擬機也沒有什麼必然的行為。

三虛擬機性能監控與故障處理工具

概述

給一個系統問題定位問題的時候,知識、經驗是關鍵基礎,數據是依據,工具是運用知識處理數據的手段。這裡說的數據包括:運行日誌、異常堆棧、GC日誌、線程快照(threaddump/javacore)、堆轉儲快照(headdump/hprof)等。經常使用適當的虛擬機監控和分析的工具可以加快我們分析數據和定位解決問題的速度。

JDK的命令行工具

jdk的命令行工具都放置在jdk/bin目錄下,其中包括了我們很熟悉的java、javac等工具。這些工具大多都是jdk/lib/tools.jar類庫的一層包裝而已,它們真正的主要功能都是在tools類庫中實現的。下面將介紹幾個常用的虛擬機監控工具。

1.jps: JVM Process Status Tool ,顯示指定系統內所有的HotSpot虛擬機進程

可以列出正在運行的虛擬機進程,並顯示虛擬機執行主類的名稱,以及這些進程的本地虛擬機的唯一ID(LVMID,Local Virtual Machine Identifier)。雖然功能單一,但是它是使用最頻繁的工具。LVMID與系統中的進程ID(PID)是一樣的。如果同時啟動了多個虛擬機進程,無法根據進程名稱定位時,那就只能依賴jps命令顯示主類的功能才能區分。

命令格式:jps [option] [hostid]

執行樣例:C:\Documents andSettings\Administrator>jps –lmv

2.jstat: JVM Statistics Monitoring Tool, 用於收集HotSpot虛擬機各方面的運行數據

用於監視虛擬機各種運行狀態信息的命令行工具。它可以顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據,它是將運行期定位虛擬機性能問題的首選工具。

命令格式:jstat [option vmid [interval [s ms] [count]]]

執行樣例:C:\Documents andSettings\Administrator>jstat -gc 6820 1000 3代表在進程6820,查詢間隔1000毫秒,次數3,查詢參數為-gc。

3.jinfo: Configuration Info for Java , 顯示虛擬機配置信息

jinfo的作用是實時地查看和調整虛擬機的各項參數。使用jps命令的-v參數可以查看虛擬機啟動時顯示指定的參數列表,在JDK1.6之後,jinfo還加入了運行期修改參數的能力,可以使用-flag [+ -] name 或者 -flag name=value。

命令格式:jinfo [option] pid

4.jmap: Memeory Map for Java, 生成虛擬機的內存轉儲快照(headdump文件)

jmap一般用於生成堆轉儲快照。當然jmap的作用也不僅僅是為了獲取dump文件,它還可以查詢finalize執行隊列,Java堆和永久代得詳細信息,如空間使用率、當前使用收集器等。

命令格式:jmap [option] vmid

5.jstack: Stack Trace for Java,顯示虛擬機的線程快照

此命令用於生成虛擬機當前時刻的線程快照。它就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因:線程間死鎖、死循環、請求外部資源導致的長時間等待等。

命令格式:jstack [option] vmid

6.JConsole:Java監視與管理控制檯

JConsole是一個基於JMX的GUI工具,用於連接正在運行的JVM,不過此JVM需要使用可管理的模式啟動。如果要把一個應用以可管理的形式啟動,可以在啟動是設置com.sun.management.jmxremote。除此之外,還可以用JConsole監控tomacat。

JConsole可以以三種方式連接正在運行的JVM:

Local:使用JConsole連接一個正在本地系統運行的JVM,並且執行程序的和運行JConsole的需要是同一個用戶。JConsole使用文件系統的授權通過RMI連接器連接到平臺的MBean服務器上。這種從本地連接的監控能力只有Sun的JDK具有

Remote:使用下面的URL通過RMI連接器連接到一個JMX代理:service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。hostName填入主機名稱,portNum為JMX代理啟動時指定的端口。JConsole為建立連接,需要在環境變量中設置mx.remote.credentials來指定用戶名和密碼從而進行授權。

Advanced:使用一個特殊的URL連接JMX代理。一般情況使用自己定製的連接器而不是RMI提供的連接器來連接JMX代理,或者是一個使用JDK1.4的實現了JMX和JMX Rmote的應用。

當JConsole成功建立連接,它從連接上的JMX代理處獲取信息,並且以下面幾個標籤頁呈現信息。

Summary tab. 監控JVM和一些監控變量的信息。

Memory tab. 內存使用信息

Threads tab. 線程使用信息

Classes tab. 類調用信息

VM tab. JVM的信息

MBeans tab.所有MBeans的信息

深入理解Java虛擬機 (共1篇)

對象, 內存, 收集器, 管理機制,
相關問題答案