jvm原理以內存機制

轉自:https://www.cnblogs.com/dreamowneryong/p/6381633.htmlhtml

JVM棧由堆、方法區,棧、本地方法棧、程序計數器等部分組成,結構圖以下所示:java

還有一張以資源共享角度描繪的圖:算法

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. 程序計數器併發

程序計數器是一塊較小的內存區域,做用能夠看作是當前線程執行的字節碼的位置指示器。分支、循環、跳轉、異常處理和線程恢復等基礎功能都須要依賴這個計算器來完成,很少說。xss

 

2.VM Strack工具

先來了解下JAVA指令的構成:spa

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

不支持的組合

一、-XX:+UseParNewGC -XX:+UseParallelOldGC

二、-XX:+UseParNewGC -XX:+UseSerialGC

 

4、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數。並行收集線程數。

相關文章
相關標籤/搜索