在閱讀本篇文章時,至少首先對JVM的概念、工作原理、結構組成,有一定的基礎性瞭解,廢話不多說,本文開門見山,直接以工作中比較常見的幾種概念:類、類對象、執行個體對象、靜態方法、非靜態方法、靜態屬性、非靜態屬性等為出發點,直接切入JVM記憶體的使用及分配,通過這些原理認清Java中的靜態方法和靜態屬性的問題,其次講JVM的記憶體堆棧模型,主要描述JVM記憶體堆棧結構的組成,最後是JVM的記憶體參數設定,主要描述記憶體設定,以防止記憶體溢出。
一、從JVM記憶體角度認清靜態訪問、靜態屬性
在JVM中,記憶體分為兩個部分,Stack(棧)和Heap(堆),這裡,我們從JVM的記憶體管理原理的角度來認識Stack和Heap,並通過這些原理認清Java中靜態方法和靜態屬性的問題。
一般JVM的記憶體分為兩部分:Stack和Heap。
Stack(棧)是JVM的記憶體指令區。Stack管理很簡單,push一定長度位元組的資料或者指令,Stack指標壓棧相應的位元組位移;pop一定位元組長度資料或者指令,Stack指標彈棧。Stack的速度很快、管理很簡單,並且每次操作的資料或者指令位元組長度是已知的。所以Java 基礎資料型別 (Elementary Data Type),Java 指令代碼,常量都儲存在Stack中。
Heap(堆)是JVM的記憶體資料區。Heap 的管理很複雜,每次分配不定長的記憶體空間,專門用來儲存對象的執行個體。在Heap 中分配一定的記憶體來儲存對象執行個體,實際上也只是儲存對象執行個體的屬性值,屬性的類型和對象本身的類型標記等,並不儲存對象的方法(方法是指令,儲存在Stack中),在Heap 中分配一定的記憶體儲存對象執行個體和對象的序列化比較類似。而對象執行個體在Heap 中分配好以後,需要在Stack中儲存一個4位元組的Heap 記憶體位址,用來定位該對象執行個體在Heap 中的位置,便於找到該對象執行個體。
由於Stack的記憶體管理是順序分配的,而且定長,不存在記憶體回收問題;而Heap 則是隨機分配記憶體,不定長度,存在記憶體配置和回收的問題;因此在JVM中另有一個GC進程,定期掃描Heap ,它根據Stack中儲存的4位元組對象地址掃描Heap ,定位Heap 中這些對象,進行一些最佳化(例如合并空閑記憶體塊什麼的),並且假設Heap 中沒有掃描到的地區都是閒置,統統refresh(實際上是把Stack中丟失了對象地址的無用對象清除了),這就是垃圾收集的過程;
圖為:JVM的體繫結構
我們首先要搞清楚的是什麼是資料以及什麼是指令。然後要搞清楚對象的方法和對象的屬性分別儲存在哪裡。
1)方法本身是指令的作業碼部分,儲存在Stack中;
2)方法內部變數作為指令的運算元部分,跟在指令的作業碼之後,儲存在Stack中(實際上是簡單類型儲存在Stack中,物件類型在Stack中儲存地址,在Heap 中儲存值);上述的指令作業碼和指令運算元構成了完整的Java 指令。
3)對象執行個體包括其屬性值作為資料,儲存在資料區Heap 中。
非靜態對象屬性作為對象執行個體的一部分儲存在Heap 中,而對象執行個體必須通過Stack中儲存的地址指標才能訪問到。因此能否訪問到對象執行個體以及它的非靜態屬性值完全取決於能否獲得對象執行個體在Stack中的地址指標。
非靜態方法和靜態方法的區別:
非靜態方法有一個和靜態方法很重大的不同:非靜態方法有一個隱含的傳入參數,該參數是JVM給它的,和我們怎麼寫代碼無關,這個隱含的參數就是對象執行個體在Stack中的地址指標。因此非靜態方法(在Stack中的指令代碼)總是可以找到自己的專用資料(在Heap 中的對象屬性值)。當然非靜態方法也必須獲得該隱含參數,因此非靜態方法在調用前,必須先new一個對象執行個體,獲得Stack中的地址指標,否則JVM將無法將隱含參數傳給非靜態方法。
靜態方法無此隱含參數,因此也不需要new對象,只要class檔案被ClassLoader load進入JVM的Stack,該靜態方法即可被調用。當然此時靜態方法是存取不到Heap 中的對象屬性的。
總結一下該過程:
當一個class檔案被ClassLoader load進入JVM後,方法指令儲存在Stack中,此時Heap區沒有資料。然後程式寄存器開始執行指令,如果是靜態方法,直接依次執行指令代碼,當然此時指令代碼是不能訪問Heap 資料區的;如果是非靜態方法,由於隱含參數沒有值,會報錯。因此在非靜態方法執行前,要先new對象,在Heap 中分配資料,並把Stack中的地址指標交給非靜態方法,這樣程式技術器依次執行指令,而指令代碼此時能夠訪問到Heap 資料區了。
靜態屬性和動態屬性:
前面提到對象執行個體以及動態屬性都是儲存在Heap 中的,而Heap 必須通過Stack中的地址指標才能夠被指令(類的方法)訪問到。因此可以推斷出:靜態屬性是儲存在Stack中的,而不同於動態屬性儲存在Heap 中。正因為都是在Stack中,而Stack中指令和資料都是定長的,因此很容易算出位移量,也因此不管什麼指令(類的方法),都可以訪問到類的靜態屬性。也正因為靜態屬性被儲存在Stack中,所以具有了全域屬性。
所以,在JVM中,靜態屬性儲存在Stack指令記憶體區,動態屬性儲存在Heap資料記憶體區。
二、JVM的堆棧記憶體模型
1. Java棧
Java棧是與每一個線程關聯的,JVM在建立每一個線程的時候,會分配一定的棧空間給線程。它主要用來儲存線程執行過程中的局部變數,方法的傳回值,以及方法調用上下文。棧空間隨著線程的終止而釋放。StackOverflowError:如果線上程執行的過程中,棧空間不夠用,那麼JVM就會拋出此異常,這種情況一般是死遞迴造成的。
2. 堆
Java中堆是由所有的線程共用的一塊記憶體地區,堆用來儲存各種JAVA對象,比如數組,線程對象等。
2.1 Generation
JVM堆一般又可以分為以下三部分:
◆ Perm 永久區
Perm代主要儲存class,method,filed對象,這部門的空間一般不會溢出,除非一次性載入了很多的類,不過在涉及到熱部署的應用伺服器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤,造成這個錯誤的很大原因就有可能是每次都重新部署,但是重新部署後,類的class沒有被卸載掉,這樣就造成了大量的class對象儲存在了perm中,這種情況下,一般重新啟動應用伺服器可以解決問題。
◆ Tenured 年老區
Tenured區主要儲存生命週期長的對象,一般是一些老的對象,當一些對象在Young複製轉移一定的次數以後,對象就會被轉移到Tenured區,一般如果系統中用了application層級的緩衝,緩衝中的對象往往會被轉移到這一區間。
◆ Young 年輕區
Young區被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區,其中Survivor區間中,某一時刻只有其中一個是被使用的,另外一個留做垃圾收集時複製對象用,在Young區間變滿的時候,minor GC就會將存活的對象移到閒置Survivor區間中,根據JVM的策略,在經過幾次垃圾收集後,任然存活於Survivor的對象將被移動到Tenured區間。有時候該區經常會遇到java.lang.OutOfMemoryError :Java heap space的錯誤。
2.2 Sizing the Generations
JVM提供了相應的參數來對記憶體大小進行配置。正如上面描述,JVM中堆被分為了3個大的區間,同時JVM也提供了一些選項對Young,Tenured的大小進行控制。
◆ Total Heap
-Xms :指定了JVM初始啟動以後初始化記憶體
-Xmx:指定JVM堆得最大記憶體,在JVM啟動以後,會分配-Xmx參數指定大小的記憶體給JVM,但是不一定全部使用,JVM會根據-Xms參數來調節真正用於JVM的記憶體
-Xmx -Xms之差就是三個Virtual空間的大小
◆ Young Generation
-XX:NewRatio=8意味著tenured 和 young的比值8:1,這樣eden+2*survivor=1/9
堆記憶體
-XX:SurvivorRatio=32意味著eden和一個survivor的比值是32:1,這樣一個Survivor就占Young區的1/34.
-Xmn 參數設定了年輕代的大小
◆ Perm Generation
-XX:PermSize=16M -XX:MaxPermSize=64M
Thread Stack
-XX:Xss=128K
3. 堆棧分離的好處
就來說說物件導向的設計吧,當然除了物件導向的設計帶來的維護性,複用性和擴充性方面的好處外,我們看看物件導向如何巧妙的利用了堆棧分離。如果從JAVA記憶體模型的角度去理解物件導向的設計,我們就會發現對它完美的表示了堆和棧,對象的資料放在堆中,而我們編寫的那些方法一般都是運行在棧中,因此物件導向的設計是一種非常完美的設計方式,它完美的統一了資料存放區和運行。
三、JVM的堆記憶體參數設定
PermGen space:全稱是Permanent Generation space.就是說是永久儲存的地區,用於存放Class和Meta資訊,Class在被Load的時候被放入該地區Heap space:存放Instance。
GC(Garbage Collection)應該不會對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤
Java Heap分為3個區
1.Young
2.Old
3.Permanent
Young儲存剛執行個體化的對象。當該區被填滿時,GC會將對象移到Old區。Permanent區則負責儲存反射對象。
JVM的Heap分配可以使用-X參數設定:
-Xms 初始Heap大小
-Xmx java heap最大值
-Xmn young generation的heap大小
JVM有2個GC線程
第一個線程負責回收Heap的Young區
第二個線程在Heap不足時,遍曆Heap,將Young 區升級為Older區
Older區的大小等於-Xmx減去-Xmn,不能將-Xms的值設的過大,因為第二個線程被迫運行會降低JVM的效能。
為什麼一些程式頻繁發生GC。有如下原因:
1. 程式內調用了System.gc()或Runtime.gc()。
2. 一些中介軟體軟體調用自己的GC方法,此時需要設定參數禁止這些GC。
3. Java的Heap太小,一般預設的Heap值都很小。
4. 頻繁執行個體化對象,Release對象 此時盡量儲存並重用對象,例如使用StringBuffer()和String()。
如果你發現每次GC後,Heap的剩餘空間會是總空間的50%,這表示你的Heap處於健康狀態,許多Server端的Java程式每次GC後最好能有65%的剩餘空間
經驗之談:
1.Server端JVM最好將-Xms和-Xmx設定成相同值。為了最佳化GC,最好讓-Xmn值約等於-Xmx的1/3。
2.一個GUI程式最好是每10到20秒間運行一次GC,每次在半秒之內完成。
注意:
1.增加Heap的大小雖然會降低GC的頻率,但也增加了每次GC的時間。並且GC運行時,所有的使用者線程將暫停,也就是GC期間,Java應用程式不做任何工作。
2.Heap大小並不決定進程的記憶體使用量量。進程的記憶體使用量量要大於-Xmx定義的值,因為Java為其他任務分配記憶體,例如每個線程的Stack等。
Stack的設定
每個線程都有他自己的Stack。
-Xss :每個線程的Stack大小
Stack的大小限制著線程的數量。-Xss參數決定Stack大小,例如-Xss1024K。如果Stack太小,也會導致Stack溢漏。
硬體環境
硬體環境也影響GC的效率,例如機器的種類,記憶體,swap空間,和CPU的數量。如果你的程式需要頻繁建立很多transient對象,會導致JVM頻繁GC。這種情況你可以增加機器的記憶體,來減少Swap空間的使用。
4種GC
1、第一種為單線程GC,也是預設的GC,該GC適用於單CPU機器。
2、第二種為Throughput GC,是多線程的GC,適用於多CPU,使用大量線程的程式。第二種GC與第一種GC相似,不同在於GC在收集Young區是多線程的,但在Old區和第一種一樣,仍然採用單線程。 -XX:+UseParallelGC參數啟動該GC。
3、第三種為Concurrent Low Pause GC,類似於第一種,適用於多CPU,並要求縮短因GC造成程式停滯的時間。這種GC可以在Old區的回收同時,運行應用程式。 -XX:+UseConcMarkSweepGC參數啟動該GC。
4、第四種為Incremental Low Pause GC,適用於要求縮短因GC造成程式停滯的時間。這種GC可以在Young區回收的同時,回收一部分Old區對象。 -Xincgc參數啟動該GC。
單檔案的JVM記憶體進行設定
預設的java虛擬機器的大小比較小,在對大資料進行處理時java就會報錯:java.lang.OutOfMemoryError。設定jvm記憶體的方法,對於單獨的.class,可以用下面的方法對Test運行時的jvm記憶體進行設定:
java -Xms64m -Xmx256m Test
-Xms是設定記憶體初始化的大小
-Xmx是設定最大能夠使用記憶體的大小(最好不要超過實體記憶體大小)
Tomcat啟動jvm記憶體設定
Linux:
在/usr/local/apache-tomcat-5.5.23/bin目錄下的catalina.sh添加:JAVA_OPTS='-Xms512m -Xmx1024m'要加“m”說明是MB,否則就是KB了,在啟動tomcat時會報記憶體不足。
-Xms:初始值
-Xmx:最大值
-Xmn:最小值Windows
在catalina.bat最前面加入
set JAVA_OPTS=-Xms128m -Xmx350m 如果用startup.bat啟動tomcat,OK設定生效.夠成功的分配200M記憶體.但是如果不是執行startup.bat啟動tomcat而是利用windows的系統服務啟動tomcat服務,上面的設定就不生效了,就是說set JAVA_OPTS=-Xms128m -Xmx350m 沒起作用.上面分配200M記憶體就OOM了..windows服務執行的是bin\tomcat.exe.他讀取註冊表中的值,而不是catalina.bat的設定.解決辦法:
修改註冊表HKEY_LOCAL_MACHINE\SOFTWARE\Apache Software Foundation\Tomcat Service Manager\Tomcat5\Parameters\JavaOptions
原值為
-Dcatalina.home="C:\ApacheGroup\Tomcat 5.0"
-Djava.endorsed.dirs="C:\ApacheGroup\Tomcat 5.0\common\endorsed"
-Xrs加入 -Xms300m -Xmx350m
重起tomcat服務,設定生效
weblogic
在weblogic中,可以在startweblogic.cmd中對每個domain虛擬記憶體的大小進行設定,預設的設定是在commEnv.cmd裡面。
JBoss
預設可以使用的記憶體為64MB
$JBOSSDIR$/bin/run.config
JAVA_OPTS = "-server -Xms128 -Xmx512"