JVM棧由堆、方法區,棧、本地方法棧、程序計數器等部分組成,結構圖如下所示:
還有一張以資源共享角度描繪的圖:
Method Area(Non-Heap)(方法區) , Heap(堆) , Program Counter Register(程序計數器) , VM Stack(虛擬機棧,也有翻譯成JAVA 方法棧的),Native Method Stack ( 本地方法棧 );
JVM初始運行的時候都會分配好 Method Area(方法區) 和 Heap(堆) ,而JVM 每遇到一個線程,就為其分配一個 Program Counter Register(程序計數器) , VM Stack(虛擬機棧)和Native Method Stack (本地方法棧),(這也能理解為什么線程會消耗較多資源了,還有遞歸。。。。)
1. 程序計數器
程序計數器是一塊較小的內存區域,作用可以看做是當前線程執行的字節碼的位置指示器。分支、循環、跳轉、異常處理和線程恢復等基礎功能都需要依賴這個計算器來完成,不多說。
2.VM Strack
先來了解下JAVA指令的構成:
JAVA指令由 操作碼 (方法本身)和 操作數 (方法內部變量) 組成。
1)方法本身是指令的 操作碼 部分,保存在Stack中;
2)方法內部變量(局部變量)作為指令的 操作數 部分,跟在指令的操作碼之后,保存在Stack中(實際上是簡單類型(int,byte,short 等)保存在Stack中,對象類型在Stack中保存地址,在Heap 中保存值);
虛擬機 棧也叫棧內存,是在線程創建時創建,它的 生命期是跟隨線程的生命 期 ,線程結束棧內存也就釋放, 對於棧來說不存在垃圾回收問題,只要線程一結束,該棧就 Over,所以不存在垃圾回收 。 也有一些資料翻譯成JAVA方法棧,大概是因為它所描述的是java方法執行的內存模型,每個方法執行的同時創建幀棧(Strack Frame)用於存儲局部變量表(包含了對應的方法參數和局部變量),操作棧(Operand Stack,記錄出棧、入棧的操作),動態鏈接、方法出口等信息,每個方法被調用直到執行完畢的過程,對應這幀棧在虛擬機棧的入棧和出棧的過程。
局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象的引用(reference類型,不等同於對象本身,根據不同的虛擬機實現,可能是一個指向對象起始地址的引用指針,也可能是一個代表對象的句柄或者其他與對象相關的位置)和 returnAdress類型(指向下一條字節碼指令的地址)。局部變量表所需的內存空間在編譯期間完成分配,在方法在運行之前,該局部變量表所需要的內存空間是固定的,運行期間也不會改變。
棧幀是一個內存區塊,是一個數據集, 是 一個有關方法(Method)和運行期數據的數據集 ,當一個方法 A 被調用時就產生了一個棧幀 F1,並 被壓入到棧中,A 方法又調用了 B 方法,於是產生棧幀 F2 也被壓入棧,執行完畢后,先彈出 F2 棧幀,再彈出 F1 棧幀,遵循“ 先進后出 ”原則。 光說比較枯燥,我們看一個圖來理解一下 Java 棧,如下圖所示:
3.Heap
Heap(堆)是JVM的內存數據區。Heap 的管理很復雜,是被所有線程共享的內存區域,在JVM啟動時候創建,專門用來保存對象的實例。在Heap 中分配一定的內存來保存對象實例,實際上也只是保存對象實例的屬性值,屬性的類型和對象本身的類型標記等,並不保存對象的方法(以幀棧的形式保存在Stack中),在Heap 中分配一定的內存保存對象實例。而對象實例在Heap 中分配好以后,需要在Stack中保存一個4字節的Heap 內存地址,用來定位該對象實例在Heap 中的位置,便於找到該對象實例,是垃圾回收的主要場所。java堆處於物理不連續的內存空間中,只要邏輯上連續即可。
4.Method Area
Object Class Data(加載類的類定義數據) 是存儲在方法區的。除此之外, 常量 、 靜態變量、JIT(即時編譯器)編譯后的代碼也都在方法區。正因為方法區所存儲的數據與堆有一種類比關系,所以它還被稱為 Non-Heap。方法區也可以是內存不連續的區域組成的,並且可設置為固定大小,也可以設置為可擴展的,這點與堆一樣。
垃圾回收在這個區域會比較少出現,這個區域內存回收的目的主要針對常量池的回收和類的卸載。
5.運行時常量池(Runtime Constant Pool)
方法區內部有一個非常重要的區域,叫做 運行時常量池(Runtime Constant Pool,簡稱 RCP) 。在字節碼文件(Class文件)中,除了有類的版本、字段、方法、接口等先關信息描述外,還有常量池(Constant Pool Table)信息,用於存儲編譯器產生的字面量和符號引用。這部分內容在類被加載后,都會存儲到方法區中的RCP。值得注意的是,運行時產生的新常量也可以被放入常量池中,比如 String 類中的 intern() 方法產生的常量。
常量池就是這個類型用到的常量的一個有序集合。包括 直接常量(基本類型,String) 和 對其他類型、方法、字段的符號引用.例如:
◆類和接口的全限定名;
◆字段的名稱和描述符;
◆方法和名稱和描述符。
池中的數據和數組一樣通過索引訪問。由於常量池包含了一個類型所有的對其他類型、方法、字段的符號引用,所以常量池在Java的動態鏈接中起了核心作用.
很有用且重要關於常量池的擴展:Java常量池詳解 http://www.cnblogs.com/DreamSea/archive/2011/11/20/2256396.html
6.Native Method Stack
與VM Strack相似,VM Strack為JVM提供執行JAVA方法的服務,Native Method Stack則為JVM提供使用native 方法的服務。
7.直接內存區
直接內存區並不是 JVM 管理的內存區域的一部分,而是其之外的。該區域也會在 Java 開發中使用到,並且存在導致內存溢出的隱患。如果你對 NIO 有所了解,可能會知道 NIO 是可以使用 Native Methods 來使用直接內存區的。
小結:
- 在此,你對JVM的內存區域有了一定的理解,JVM內存區域可以分為線程共享和非線程共享兩部分,線程共享的有堆和方法區,非線程共享的有虛擬機棧,本地方法棧和程序計數器。
小結:
1. 分清什么是實例什么是對象。Class a= new Class();此時a叫實例,而不能說a是對象。實例在棧中,對象在堆中,操作實例實際上是通過實例的指針間接操作對象。多個實例可以指向同一個對象。
2. 棧中的數據和堆中的數據銷毀並不是同步的。方法一旦結束,棧中的局部變量立即銷毀,但是堆中對象不一定銷毀。因為可能有其他變量也指向了這個對象,直到棧中沒有變量指向堆中的對象時,它才銷毀,而且還不是馬上銷毀,要等垃圾回收掃描時才可以被銷毀。
3. 以上的棧、堆、代碼段、數據段等等都是相對於應用程序而言的。每一個應用程序都對應唯一的一個JVM實例,每一個JVM實例都有自己的內存區域,互不影響。並且這些內存區域是所有線程共享的。這里提到的棧和堆都是整體上的概念,這些堆棧還可以細分。
4. 類的成員變量在不同對象中各不相同,都有自己的存儲空間(成員變量在堆中的對象中)。而類的方法卻是該類的所有對象共享的,只有一套,對象使用方法的時候方法才被壓入棧,方法不使用則不占用內存。
這里補充下堆內存的垃圾回收機制:
-
新生代。新建的對象都是用新生代分配內存,Eden空間不足的時候,會把存活的對象轉移到Survivor中,新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例
-
舊生代。用於存放新生代中經過多次垃圾回收仍然存活的對象
-
持久帶(Permanent Space)實現方法區,主要存放所有已加載的類信息,方法信息,常量池等等。可通過-XX:PermSize和-XX:MaxPermSize來指定持久帶初始化值和最大值。Permanent Space並不等同於方法區,只不過是Hotspot JVM用Permanent Space來實現方法區而已,有些虛擬機沒有Permanent Space而用其他機制來實現方法區。
-
-Xmx:最大堆內存,如:-Xmx512m
-
-Xms:初始時堆內存,如:-Xms256m
-
-XX:MaxNewSize:最大年輕區內存
-XX:NewSize:初始時年輕區內存.通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%
-XX:MaxPermSize:最大持久帶內存
-XX:PermSize:初始時持久帶內存
-XX:+PrintGCDetails。打印 GC 信息
-XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代占整個堆空間的1/3,老年代占2/3
-XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。默認值為 8。即 Eden 占新生代空間的 8/10,另外兩個 Survivor 各占 1/10
注意:棧內存設置:-xss:設置每個線程的堆棧大小. JDK1.5+ 每個線程堆棧大小為 1M,一般來說如果棧不是很深的話, 1M 是絕對夠用了的。
垃圾回收按照基本回收策略分
引用計數(Reference Counting):
比較古老的回收算法。原理是此對象有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數為0的對象。此算法最致命的是無法處理循環引用的問題。
標記-清除(Mark-Sweep):
此算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法需要暫停整個應用,同時,會產生內存碎片。
復制(Copying):
此算法把內存空間划為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象復制到另外一個區域中。算法每次只處理正在使用中的對象,因此復制成本比較小,同時復制過去以后還能進行相應的內存整理,不會出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是需要兩倍內存空間。
標記-整理(Mark-Compact):
此算法結合了“標記-清除”和“復制”兩個算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把清除未標記對象並且把存活對象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“標記-清除”的碎片問題,同時也避免了“復制”算法的空間問題。
JVM分別對新生代和舊生代采用不同的垃圾回收機制
新生代的GC:
新生代通常存活時間較短,因此基於Copying算法來進行回收,所謂Copying算法就是掃描出存活的對象,並復制到一塊新的完全未使用的空間中,對應於新生代,就是在Eden和From Space或To Space之間copy。新生代采用空閑指針的方式來控制GC觸發,指針保持最后一個分配的對象在新生代區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC。當連續分配對象時,對象會逐漸從eden到survivor,最后到舊生代。
在執行機制上JVM提供了串行GC(Serial GC)、並行回收GC(Parallel Scavenge)和並行GC(ParNew)
1)串行GC
在整個掃描和復制過程采用單線程的方式來進行,適用於單CPU、新生代空間較小及對暫停時間要求不是非常高的應用上,是client級別默認的GC方式,可以通過-XX:+UseSerialGC來強制指定
2)並行回收GC
在整個掃描和復制過程采用多線程的方式來進行,適用於多CPU、對暫停時間要求較短的應用上,是server級別默認采用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數
3)並行GC
與舊生代的並發GC配合使用
舊生代的GC:
舊生代與新生代不同,對象存活的時間比較長,比較穩定,因此采用標記(Mark)算法來進行回收,所謂標記就是掃描出存活的對象,然后再進行回收未被標記的對象,回收后對用空出的空間要么進行合並,要么標記出來便於下次進行分配,總之就是要減少內存碎片帶來的效率損耗。在執行機制上JVM提供了串行GC(Serial MSC)、並行GC(parallel MSC)和並發GC(CMS),具體算法細節還有待進一步深入研究。
以上各種GC機制是需要組合使用的,指定方式由下表所示:
指定方式 |
新生代GC方式 |
舊生代GC方式 |
-XX:+UseSerialGC |
串行GC |
串行GC |
-XX:+UseParallelGC |
並行回收GC |
並行GC |
-XX:+UseConeMarkSweepGC |
並行GC |
並發GC |
-XX:+UseParNewGC |
並行GC |
串行GC |
-XX:+UseParallelOldGC |
並行回收GC |
並行GC |
-XX:+ UseConeMarkSweepGC -XX:+UseParNewGC |
串行GC |
並發GC |
不支持的組合 |
1、-XX:+UseParNewGC -XX:+UseParallelOldGC 2、-XX:+UseParNewGC -XX:+UseSerialGC |
四、JVM內存調優
首先需要注意的是在對JVM內存調優的時候不能只看操作系統級別Java進程所占用的內存,這個數值不能准確的反應堆內存的真實占用情況,因為GC過后這個值是不會變化的,因此內存調優的時候要更多地使用JDK提供的內存查看工具,比如JConsole和Java VisualVM。
對JVM內存的系統級的調優主要的目的是減少GC的頻率和Full GC的次數,過多的GC和Full GC是會占用很多的系統資源(主要是CPU),影響系統的吞吐量。特別要關注Full GC,因為它會對整個堆進行整理,導致Full GC一般由於以下幾種情況:
舊生代空間不足
調優時盡量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象
Pemanet Generation空間不足
增大Perm Gen空間,避免太多靜態對象
統計得到的GC后晉升到舊生代的平均大小大於舊生代剩余空間
控制好新生代和舊生代的比例
System.gc()被顯示調用
垃圾回收不要手動觸發,盡量依靠JVM自身的機制
調優手段主要是通過控制堆內存的各個部分的比例和GC策略來實現,下面來看看各部分比例不良設置會導致什么后果
1)新生代設置過小
一是新生代GC次數非常頻繁,增大系統消耗;二是導致大對象直接進入舊生代,占據了舊生代剩余空間,誘發Full GC
2)新生代設置過大
一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加
一般說來新生代占整個堆1/3比較合適
3)Survivor設置過小
導致對象從eden直接到達舊生代,降低了在新生代的存活時間
4)Survivor設置過大
導致eden過小,增加了GC頻率
另外,通過-XX:MaxTenuringThreshold=n來控制新生代存活時間,盡量讓對象在新生代被回收
由內存管理和垃圾回收可知新生代和舊生代都有多種GC策略和組合搭配,選擇這些策略對於我們這些開發人員是個難題,JVM提供兩種較為簡單的GC策略的設置方式
1)吞吐量優先
JVM以吞吐量為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設置
2)暫停時間優先
JVM以暫停時間為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,盡量保證每次GC造成的應用停止時間都在指定的數值范圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設置
最后匯總一下JVM常見配置
堆設置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設置年輕代大小
-XX:NewRatio=n:設置年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
-XX:MaxPermSize=n:設置持久代大小
收集器設置
-XX:+UseSerialGC:設置串行收集器
-XX:+UseParallelGC:設置並行收集器
-XX:+UseParalledlOldGC:設置並行年老代收集器
-XX:+UseConcMarkSweepGC:設置並發收集器
垃圾回收統計信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
並行收集器設置
-XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
-XX:GCTimeRatio=n:設置垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)
並發收集器設置
-XX:+CMSIncrementalMode:設置為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設置並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。