一文帶你瞭解JVM堆詳解

JAVA能夠實現跨平臺的一個根本原因,是定義了class文件的格式標準,凡是實現該標準的JVM都能夠加載並解釋該class文件,據此也可以知道,爲啥Java語言的執行速度比C/C++語言執行的速度要慢了,當然原因肯定不止這一個,如在JVM中沒有數據寄存器,指令集使用的是棧來保存中間數據…等,儘管Java的貢獻者們爲執行速度的提高想了各種辦法,如JIT、動態編譯器等,以下是Leetcode中一道題目用不同的語言實現時的執行性能對比圖…

一文帶你瞭解JVM堆詳解
以下是JVM的一個基本架構圖,在這個基本架構圖中,棧有兩部份,Java線程棧以及本地方法棧,棧的概念與C/C++程序基本上都是一個概念,裏面存放的都是棧幀,一個棧幀代表的就是一個函數的調用,在棧幀裏面存放了函數的形參,函數的局部變量, 返回地址等,但是與C/C++的一個重要區別是,C/C++裏面有傳值以及傳址的區別,當傳的是一個對象時( 結構體也可以當成對象,其實就是對象,只不過裏面的方法默認都是public的,不信你可以試試,在結構體中加一個函數,編譯器也不會報錯,程序依舊運行),會將對象復到到棧中,而Java中只有基本類型纔是傳值的,其他類型傳的都是引用,什麼是引用,學過C/C++的就把引用當作指針理解吧~,在這個基本架構圖中,可以看出JVM還定義了一個本地方法棧,本地方法棧是爲Java調用本地方法【這些本地方法是由其他語言編寫的】服務的

一文帶你瞭解JVM堆詳解

上面的圖中看到的是JVM中棧有兩個,但是堆只有一個,每一個線程都有自已的線程棧【線程棧的大小可以通過設置JVM的-xss參數進行配置,32位系統下,一般默認的大小是512K】,線程棧裏面的數據屬於該線程私有,但是所有的線程都共享一個堆空間,堆中存放的是對象數據,什麼是對象數據,排除法,排除基本類型以及引用類型以外的數據都將放在堆空間中,下面來具體分析一下堆空間…

在JVM中堆空間劃分如下圖所示

一文帶你瞭解JVM堆詳解
上圖中,刻畫了Java程序運行時的堆空間,可以簡述成如下2條

1.JVM中堆空間可以分成三個大區,新生代、老年代、永久代

2.新生代可以劃分爲三個區,Eden區,兩個倖存區

在JVM運行時,可以通過配置以下參數改變整個JVM堆的配置比例

1.JVM運行時堆的大小  -Xms堆的最小值  -Xmx堆空間的最大值2.新生代堆空間大小調整  -XX:NewSize新生代的最小值  -XX:MaxNewSize新生代的最大值  -XX:NewRatio設置新生代與老年代在堆空間的大小  -XX:SurvivorRatio新生代中Eden所佔區域的大小3.永久代大小調整  -XX:MaxPermSize4.其他  -XX:MaxTenuringThreshold,設置將新生代對象轉到老年代時需要經過多少次垃圾回收,但是仍然沒有被回收
在上面的配置中,老年代所佔空間的大小是由-XX:SurvivorRatio這個參數進行配置的,看完了上面的JVM堆空間分配圖,可能會奇怪,爲啥新生代空間要劃分爲三個區Eden及兩個Survivor區?有何用意?爲什麼要這麼分?要理解這個問題,就得理解一下JVM的垃圾收集機制(複製算法也叫copy算法),步驟如下:

複製(Copying)算法

將內存平均分成A、B兩塊,算法過程:

  1. 新生對象被分配到A塊中未使用的內存當中。當A塊的內存用完了, 把A塊的存活對象對象複製到B塊。
  2. 清理A塊所有對象。
  3. 新生對象被分配的B塊中未使用的內存當中。當B塊的內存用完了, 把B塊的存活對象對象複製到A塊。
  4. 清理B塊所有對象。
  5. goto 1。

優點:簡單高效。缺點:內存代價高,有效內存爲佔用內存的一半。

圖解說明如下所示:(圖中後觀是一個循環過程)

一文帶你瞭解JVM堆詳解
對複製算法進一步優化:使用Eden/S0/S1三個分區

平均分成A/B塊太浪費內存,採用Eden/S0/S1三個區更合理,空間比例爲Eden:S0:S1==8:1:1,有效內存(即可分配新生對象的內存)是總內存的9/10。

算法過程:

  1. Eden+S0可分配新生對象;
  2. 對Eden+S0進行垃圾收集,存活對象複製到S1。清理Eden+S0。一次新生代GC結束。
  3. Eden+S1可分配新生對象;
  4. 對Eden+S1進行垃圾收集,存活對象複製到S0。清理Eden+S1。二次新生代GC結束。
  5. goto 1。

默認Eden:S0:S1=8:1:1,因此,新生代中可以使用的內存空間大小佔用新生代的9/10,那麼有人就會問,爲什麼不直接分成兩個區,一個區佔9/10,另一個區佔1/10,這樣做的原因大概有以下幾種

1.S0與S1的區間明顯較小,有效新生代空間爲Eden+S0/S1,因此有效空間就大,增加了內存使用率
2.有利於對象代的計算,當一個對象在S0/S1中達到設置的XX:MaxTenuringThreshold值後,會將其分到老年代中,設想一下,如果沒有S0/S1,直接分成兩個區,該如何計算對象經過了多少次GC還沒被釋放,你可能會說,在對象里加一個計數器記錄經過的GC次數,或者存在一張映射表記錄對象和GC次數的關係,是的,可以,但是這樣的話,會掃描整個新生代中的對象, 有了S0/S1我們就可以只掃描S0/S1區了~~~

堆和非堆內存

按照官方的說法:“Java 虛擬機具有一個堆(Heap),堆是運行時數據區域,所有類實例和數組的內存均從此處分配。堆是在 Java 虛擬機啓動時創建的。”“在JVM中堆之外的內存稱爲非堆內存(Non-heap memory)”。

JVM主要管理兩種類型的內存:堆和非堆。

Heap memory Code Cache

Eden Space

Survivor Space

Tenured Gen

non-heap memory Perm Gen

native heap?(I guess)

堆內存

Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。堆是在 Java 虛擬機啓動時創建的。對象的堆內存由稱爲垃圾回收器的自動內存管理系統回收。

堆的大小可以固定,也可以擴大和縮小。堆的內存不需要是連續空間。

非堆內存

Java 虛擬機管理堆之外的內存(稱爲非堆內存)。

Java 虛擬機具有一個由所有線程共享的方法區。方法區屬於非堆內存。它存儲每個類結構,如運行時常數池、字段和方法數據,以及方法和構造方法的代碼。它是在 Java 虛擬機啓動時創建的。

方法區在邏輯上屬於堆,但 Java 虛擬機實現可以選擇不對其進行回收或壓縮。與堆類似,方法區的大小可以固定,也可以擴大和縮小。方法區的內存不需要是連續空間。

除了方法區外,Java 虛擬機實現可能需要用於內部處理或優化的內存,這種內存也是非堆內存。例如,JIT 編譯器需要內存來存儲從 Java 虛擬機代碼轉換而來的本機代碼,從而獲得高性能。

幾個基本概念

PermGen space:全稱是Permanent Generation space,即永久代。就是說是永久保存的區域,用於存放Class和Meta信息,Class在被Load的時候被放入該區域,GC(Garbage Collection)應該不會對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤。

Heap space:存放Instance。

Java Heap分爲3個區,Young即新生代,Old即老生代和Permanent。

Young保存剛實例化的對象。當該區被填滿時,GC會將對象移到Old區。Permanent區則負責保存反射對象。

堆內存分配

JVM初始分配的堆內存由-Xms指定,默認是物理內存的1/64;
JVM最大分配的堆內存由-Xmx指定,默認是物理內存的1/4。
默認空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制;
空餘堆內存大於70%時,JVM會減少堆直到-Xms的最小限制。
因此服務器一般設置-Xms、-Xmx 相等以避免在每次GC 後調整堆的大小。
說明:如果-Xmx 不指定或者指定偏小,應用可能會導致java.lang.OutOfMemory錯誤,此錯誤來自JVM,不是Throwable的,無法用try…catch捕捉。

非堆內存分配

  1. JVM使用-XX:PermSize設置非堆內存初始值,默認是物理內存的1/64;

  2. 由XX:MaxPermSize設置最大非堆內存的大小,默認是物理內存的1/4。

還有一說:MaxPermSize缺省值和-server -client選項相關,-server選項下默認MaxPermSize爲64m,-client選項下默認MaxPermSize爲32m。這個我沒有實驗。

  1. XX:MaxPermSize設置過小會導致java.lang.OutOfMemoryError: PermGen space 就是內存益出。

  2. 爲什麼會內存益出:

這一部分內存用於存放Class和Meta的信息,Class在被 Load的時候被放入PermGen space區域,它和存放Instance的Heap區域不同。
GC(Garbage Collection)不會在主程序運行期對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS 的話,就很可能出現PermGen space錯誤。

  1. 這種錯誤常見在web服務器對JSP進行pre compile的時候。

JVM內存限制(最大值)

  1. 首先JVM內存限制於實際的最大物理內存,假設物理內存無限大的話,JVM內存的最大值跟操作系統有很大的關係。簡單的說就32位處理器雖然可控內存空間有4GB,但是具體的操作系統會給一個限制,這個限制一般是2GB-3GB(一般來說Windows系統下爲1.5G-2G,Linux系統下爲2G-3G),而64bit以上的處理器就不會有限制了。

  2. 爲什麼有的機器我將-Xmx和-XX:MaxPermSize都設置爲512M之後Eclipse可以啓動,而有些機器無法啓動?

通過上面對JVM內存管理的介紹我們已經瞭解到JVM內存包含兩種:堆內存和非堆內存,另外JVM最大內存首先取決於實際的物理內存和操作系統。所以說設置VM參數導致程序無法啓動主要有以下幾種原因:

參數中-Xms的值大於-Xmx,或者-XX:PermSize的值大於-XX:MaxPermSize;
-Xmx的值和-XX:MaxPermSize的總和超過了JVM內存的最大限制,比如當前操作系統最大內存限制,或者實際的物理內存等等。說到實際物理內存這裏需要說明一點的是,如果你的內存是1024MB,但實際系統中用到的並不可能是1024MB,因爲有一部分被硬件佔用了。

  1. 如果你有一個雙核的CPU,也許可以嘗試這個參數: -XX:+UseParallelGC 讓GC可以更快的執行。(只是JDK 5裏對GC新增加的參數)

  2. 如果你的WEB APP下都用了大量的第三方jar,其大小超過了服務器jvm默認的大小,那麼就會產生內存益出問題了。解決方法: 設置MaxPermSize大小。

增加服務器啓動的JVM參數設置: -Xms128m -Xmx256m -XX:PermSize=128M -XX:MaxNewSize=256m -XX:MaxPermSize=256m
如tomcat,修改TOMCAT_HOME/bin/catalina.sh,在echo “Using CATALINA_BASE: $CATALINA_BASE”上面加入以下行:JAVA_OPTS=”-server -XX:PermSize=64M -XX:MaxPermSize=128m

  1. 建議:將相同的第三方jar文件移置到tomcat/shared/lib目錄下,這樣可以減少jar 文檔重複佔用內存

JVM內存設置參數

內存設置參數

一文帶你瞭解JVM堆詳解
說明:
如果-Xmx不指定或者指定偏小,應用可能會導致java.lang.OutOfMemory錯誤,此錯誤來自JVM不是Throwable的,無法用try…catch捕捉。
PermSize和MaxPermSize指明虛擬機爲java永久生成對象(Permanate generation)如,class對象、方法對象這些可反射(reflective)對象分配內存限制,這些內存不包括在Heap(堆內存)區之中。
-XX:MaxPermSize分配過小會導致:java.lang.OutOfMemoryError: PermGen space。
MaxPermSize缺省值和-server -client選項相關:-server選項下默認MaxPermSize爲64m、-client選項下默認MaxPermSize爲32m。

申請一塊內存的過程

JVM會試圖爲相關Java對象在Eden中初始化一塊內存區域
當Eden空間足夠時,內存申請結束。否則到下一步
JVM試圖釋放在Eden中所有不活躍的對象(這屬於1或更高級的垃圾回收);釋放後若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區/OLD區
Survivor區被用來作爲Eden及OLD的中間交換區域,當OLD區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區
當OLD區空間不夠時,JVM會在OLD區進行完全的垃圾收集(0級)
完全垃圾收集後,若Survivor及OLD區仍然無法存放從Eden複製過來的部分對象,導致JVM無法在Eden區爲新對象創建內存區域,則出現”out of memory錯誤”

resin服務器典型的響應時間優先型的jvm配置:

-Xmx2000M -Xms2000M -Xmn500M

-XX:PermSize=250M -XX:MaxPermSize=250M

-Xss256K

-XX:+DisableExplicitGC

-XX:SurvivorRatio=1

-XX:+UseConcMarkSweepGC

-XX:+UseParNewGC

-XX:+CMSParallelRemarkEnabled

-XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction=0

-XX:+CMSClassUnloadingEnabled

-XX:LargePageSizeInBytes=128M

-XX:+UseFastAccessorMethods

-XX:+UseCMSInitiatingOccupancyOnly

-XX:CMSInitiatingOccupancyFraction=60

-XX:SoftRefLRUPolicyMSPerMB=0

-XX:+PrintClassHistogram

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-XX:+PrintHeapAtGC

-Xloggc:log/gc.log

內存回收算法

Java中有四種不同的回收算法,對應的啓動參數爲:

–XX:+UseSerialGC

–XX:+UseParallelGC

–XX:+UseParallelOldGC

–XX:+UseConcMarkSweepGC

Serial Collector

大部分平臺或者強制 java -client 默認會使用這種。

young generation算法 = serial

old generation算法 = serial (mark-sweep-compact)

這種方法的缺點很明顯, stop-the-world, 速度慢。服務器應用不推薦使用。

Parallel Collector

在linux x64上默認是這種,其他平臺要加 java -server 參數纔會默認選用這種。

young = parallel,多個thread同時copy

old = mark-sweep-compact = 1

優點:新生代回收更快。因爲系統大部分時間做的gc都是新生代的,這樣提高了throughput(cpu用於非gc時間)

缺點:當運行在8G/16G server上old generation live object太多時候pause time過長

Parallel Compact Collector (ParallelOld)

young = parallel = 2

old = parallel,分成多個獨立的單元,如果單元中live object少則回收,多則跳過

優點:old old generation上性能較 parallel 方式有提高

缺點:大部分server系統old generation內存佔用會達到60%-80%, 沒有那麼多理想的單元live object很少方便迅速回收,同時compact方面開銷比起parallel並沒明顯減少。

Concurrent Mark-Sweep(CMS) Collector

young generation = parallel collector = 2

old = cms

同時不做 compact 操作。

優點:pause time會降低, pause敏感但CPU有空閒的場景需要建議使用策略4.

缺點:cpu佔用過多,cpu密集型服務器不適合。另外碎片太多,每個object的存儲都要通過鏈表連續跳n個地方,空間浪費問題也會增大。

內存監控方法

jmap -heap 查看java 堆(heap)使用情況

jmap -heap pid

using thread-local object allocation.

Parallel GC with 4 thread(s) #GC 方式

Heap Configuration: #堆內存初始化配置

MinHeapFreeRatio=40 #對應jvm啓動參數-XX:MinHeapFreeRatio設置JVM堆最小空閒比率(default 40)

MaxHeapFreeRatio=70 #對應jvm啓動參數 -XX:MaxHeapFreeRatio設置JVM堆最大空閒比率(default 70)

MaxHeapSize=512.0MB #對應jvm啓動參數-XX:MaxHeapSize=設置JVM堆的最大大小

NewSize = 1.0MB #對應jvm啓動參數-XX:NewSize=設置JVM堆的‘新生代’的默認大小

MaxNewSize =4095MB #對應jvm啓動參數-XX:MaxNewSize=設置JVM堆的‘新生代’的最大大小

OldSize = 4.0MB #對應jvm啓動參數-XX:OldSize=:設置JVM堆的‘老生代’的大小

NewRatio = 8 #對應jvm啓動參數-XX:NewRatio=:‘新生代’和‘老生代’的大小比率

SurvivorRatio = 8 #對應jvm啓動參數-XX:SurvivorRatio=設置年輕代中Eden區與Survivor區的大小比值

PermSize= 16.0MB #對應jvm啓動參數-XX:PermSize=:設置JVM堆的‘永生代’的初始大小

MaxPermSize=64.0MB #對應jvm啓動參數-XX:MaxPermSize=:設置JVM堆的‘永生代’的最大大小

Heap Usage: #堆內存分步

PS Young Generation

Eden Space: #Eden區內存分佈

capacity = 20381696 (19.4375MB) #Eden區總容量

used = 20370032 (19.426376342773438MB) #Eden區已使用

free = 11664 (0.0111236572265625MB) #Eden區剩餘容量

99.94277218147106% used #Eden區使用比率

From Space: #其中一個Survivor區的內存分佈

capacity = 8519680 (8.125MB)

used = 32768 (0.03125MB)

free = 8486912 (8.09375MB)

0.38461538461538464% used

To Space: #另一個Survivor區的內存分佈

capacity = 9306112 (8.875MB)

used = 0 (0.0MB)

free = 9306112 (8.875MB)

0.0% used

PS Old Generation #當前的Old區內存分佈

capacity = 366280704 (349.3125MB)

used = 322179848 (307.25464630126953MB)

free = 44100856 (42.05785369873047MB)

87.95982001825573% used

PS Perm Generation #當前的 “永生代” 內存分佈

capacity = 32243712 (30.75MB)

used = 28918584 (27.57891082763672MB)

free = 3325128 (3.1710891723632812MB)

89.68751488662348% used

JVM內存監控工具

<%@ page import=“java.lang.management.*” %>

<%@ page import=“java.util.*” %>

JVM Memory Monitor

Memory MXBean

Heap Memory Usage<%=ManagementFactory.getMemoryMXBean().getHeapMemoryUsage()%> Non-Heap Memory Usage<%=ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage()%>

Memory Pool MXBeans

<%

Iterator iter = ManagementFactory.getMemoryPoolMXBeans().iterator();

while (iter.hasNext()) {

MemoryPoolMXBean item = (MemoryPoolMXBean) iter.next();

%>

<%= item.getName() %> Type<%= item.getType() %> Usage<%= item.getUsage() %> Peak Usage<%= item.getPeakUsage() %> Collection Usage<%= item.getCollectionUsage() %>

<%} %>

最後,如果覺得本文不錯,別忘了轉發關注一下!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章