java虛擬機運行時數據區分佈圖:java
其中,堆(Heap)和JVM棧是程序運行的關鍵,由於:程序員
那爲何要把堆和棧區分出來呢?棧中不是也能夠存儲數據嗎?面試
Java堆是java虛擬機所管理內存中最大的一塊內存空間,處於物理上不連續的內存空間,只要邏輯連續便可,主要用於存放各類類的實例對象。該區域被全部線程共享,在虛擬機啓動時建立,用來存放對象的實例,幾乎全部的對象以及數組都在這裏分配內存(棧上分配、標量替換優化技術的例外)。
在 Java 中,堆被劃分紅兩個不一樣的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分爲三個區域:Eden、From Survivor(S0)、To Survivor(S1)。如圖所示:算法
堆的內存佈局:數組
這樣劃分的目的是爲了使jvm可以更好的管理內存中的對象,包括內存的分配以及回收。 而新生代按eden和兩個survivor的分法,是爲了:緩存
參數 | 描述 |
---|---|
-Xms | 堆內存初始大小,單位m、g |
-Xmx | 堆內存最大容許大小,通常不要大於物理內存的80% |
-Xmn | 年輕代內存初始大小 |
-Xss | 每一個線程的堆棧大小,即JVM棧的大小 |
-XX:NewRatio | 年輕代(包括Eden和兩個Survivor區)與年老代的比值 |
-XX:NewSzie(-Xns) | 年輕代內存初始大小,能夠縮寫-Xns |
-XX:MaxNewSize(-Xmx) | 年輕代內存最大容許大小,能夠縮寫-Xmx |
-XX:SurvivorRatio | 年輕代中Eden區與Survivor區的容量比例值,默認爲8,即8:1 |
-XX:MinHeapFreeRatio | GC後,若是發現空閒堆內存佔到整個預估堆內存的40%,則放大堆內存的預估最大值,但不超過固定最大值。 |
-XX:MaxHeapFreeRatio | 預估堆內存是堆大小動態調控的重要選項之一。堆內存預估最大值必定小於或等於固定最大值(-Xmx指定的數值)。前者會根據使用狀況動態調大或縮小,以提升GC回收的效率,默認70% |
-XX:MaxTenuringThreshold | 垃圾最大年齡,設置爲0的話,則年輕代對象不通過Survivor區,直接進入年老代。對於年老代比較多的應用,能夠提升效率.若是將此值設置爲一個較大值,則年輕代對象會在Survivor區進行屢次複製,這樣能夠增長對象再年輕代的存活 時間,增長在年輕代即被回收的機率 |
-XX:InitialTenuringThreshold | 能夠設定老年代閥值的初始值 |
-XX:+PrintTenuringDistribution | 查看每次minor GC後新的存活週期的閾值 |
Note: 每次GC 後會調整堆的大小,爲了防止動態調整帶來的性能損耗,通常設置-Xms、-Xmx 相等。
新生代的三個設置參數:-Xmn,-XX:NewSize,-XX:NewRatio的優先級:
(1).最高優先級: -XX:NewSize=1024m和-XX:MaxNewSize=1024m
(2).次高優先級: -Xmn1024m (默認等效效果是:-XX:NewSize==-XX:MaxNewSize==1024m)
(3).最低優先級:-XX:NewRatio=2
推薦使用的是-Xmn參數,緣由是這個參數很簡潔,至關於一次性設定NewSize和MaxNewSIze,並且二者相等。安全
各個方式的實質操做以下:性能優化
方式 | 實質 |
---|---|
使用new關鍵 | 調用無參或有參構造器函數建立 |
使用Class的newInstance方法 | 調用無參或有參構造器函數建立,且須要是publi的構造函數 |
使用Constructor類的newInstance方法 | 調用有參和私有private構造器函數建立,實用性更廣 |
使用Clone方法 | 不調用任何參構造器函數,且對象須要實現Cloneable接口並實現其定義的clone方法,且默認爲淺複製 |
第三方庫Objenesis | 利用了asm字節碼技術,動態生成Constructor對象 |
在虛擬機層面上建立對象的步驟:數據結構
步驟 | 解析 |
---|---|
一、判斷對象對應的類是否加載、連接、初始化 | 虛擬機遇到一條new指令,首先去檢查這個指令的參數可否在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化。若是沒有,那麼必須先執行類的加載、解釋、初始化(類的clinit方法)。 |
二、爲對象分配內存 | 類加載檢查經過後,虛擬機爲新生對象分配內存。對象所需內存大小在類加載完成後即可以徹底肯定,爲對象分配空間無非就是從Java堆中劃分出一塊肯定大小的內存而已。 |
三、處理併發安全問題 | 另一個問題及時保證new對象時候的線程安全性:建立對象是很是頻繁的操做,虛擬機須要解決併發問題。 虛擬機採用了兩種方式解決併發問題:(1)CAS配上失敗重試的方式保證指針更新操做的原子性;(2)TLAB 把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝區,(TLAB ,Thread Local Allocation Buffer)虛擬機是否使用TLAB,能夠經過-XX:+/-UseTLAB參數來設定。 |
四、初始化分配到的空間 | 內存分配結束,虛擬機將分配到的內存空間都初始化爲零值(不包括對象頭)。這一步保證了對象的實例字段在Java代碼中能夠不用賦初始值就能夠直接使用,程序能訪問到這些字段的數據類型所對應的零值 |
五、設置對象的對象頭 | 將對象的所屬類(即類的元數據信息)、對象的HashCode和對象的GC分代年齡等數據存儲在對象的對象頭中 |
六、執行init方法進行初始化 | 在Java程序的視角看來,初始化才正式開始,開始調用方法完成初始賦值和構造函數,全部的字段都爲零值。所以通常來講(由字節碼中是否跟隨有invokespecial指令所決定),new指令以後會接着就是執 行方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底建立出來。 |
分配對象內存,有兩種分配方式,指針碰撞和空閒列表:
(1)若是內存是規整的,那麼虛擬機將採用的是指針碰撞法(Bump The Pointer)來爲對象分配內存。意思是全部用過的內存在一邊,空閒的內存在另一邊,中間放着一個指針做爲分界點的指示器,分配內存就僅僅是把指針向空閒那邊挪動一段與對象大小相等的距離罷了。
若是垃圾收集器選擇的是Serial、ParNew這種基於壓縮算法的,虛擬機採用這種分配方式。
通常使用帶有compact(整理)過程的收集器時,使用指針碰撞。
(2)若是內存不是規整的,已使用的內存和未使用的內存相互交錯,那麼虛擬機將採用的是空閒列表法來爲對象分配內存。意思是虛擬機維護了一個列表,記錄上哪些內存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。這種分配方式成爲「空閒列表(Free List)」。架構
Note: 選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
那什麼樣的對象可以進入老年代(Old)?
狀況 | 解析 |
---|---|
1.對象優先在Eden分配 | 大多數狀況下,對象在新生代Eden區中分配,當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC;虛擬機提供了-XX:PrintGCDetails參數,發生垃圾回收時打印內存回收日誌,而且在進程退出時輸出當前內存各區域的分配狀況。 |
2.大對象直接進入老年代 | 所謂的大對象就是指,須要大量連續內存空間的java對象,最典型的大對象就是那種很長的字符串及數組。虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值得對象直接在老年代中分配(這樣作的目的是避免在Eden區及兩個Survivor之間發生大量的內存拷貝) |
3.長期存活的對象將直接進入老年代 | 對象年齡計數器。-XX:MaxTenuringThreshold |
四、動態對象年齡斷定 | 虛擬機並不老是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。 |
五、空間分配擔保 | 在發生Minor GC時(前),虛擬機會檢測以前每次晉升到老年代的平均大小(由於當次會有多少對象會存活是沒法肯定的,因此取以前的平均值/經驗值)是否大於老年代的剩餘空間大小,若是大於,則改成直接進行一次Full GC。若是小於,則查看HandlePromotionFailure設置是否容許擔保失敗;若是容許,那隻會進行Minor GC;若是不容許,則也要改成進行一次Full GC。取平均值進行比較其實仍然是一種動態機率手段,也就是說若是某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然會致使擔保失敗(Handle Promotion Failure),這樣會觸發Full GC。 |
被譽爲現代垃圾回收算法的思想基礎。
標記-清除算法採用從根集合進行掃描,對存活的對象對象標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收,如上圖所示。標記-清除算法不須要進行對象的移動,而且僅對不存活的對象進行處理,在存活對象比較多的狀況下極爲高效,但因爲標記-清除算法直接回收不存活的對象,所以會形成內存碎片。
該算法的提出是爲了克服句柄的開銷和解決堆碎片的垃圾回收。創建在存活對象少,垃圾對象多的前提下。此算法每次只處理正在使用中的對象,所以複製成本比較小,同時複製過去後還能進行相應的內存整理,不會出現碎片問題。但缺點也是很明顯,就是須要兩倍內存空間。
它開始時把堆分紅 一個對象 面和多個空閒面, 程序從對象面爲對象分配空間,當對象滿了,基於copying算法的垃圾 收集就從根集中掃描活動對象,並將每一個活動對象複製到空閒面(使得活動對象所佔的內存之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。一種典型的基於coping算法的垃圾回收是stop-and-copy算法,它將堆分紅對象面和空閒區域面,在對象面與空閒區域面的切換過程當中,程序暫停執行。
此算法是結合了「標記-清除」和「複製算法」兩個算法的優勢。避免了「標記-清除」的碎片問題,同時也避免了「複製」算法的空間問題。
標記-整理算法採用標記-清除算法同樣的方式進行對象的標記,但在清除時不一樣,在回收不存活的對象佔用的空間後,會將全部的存活對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,所以成本更高,可是卻解決了內存碎片的問題。在基於Compacting算法的收集器的實現中,通常增長句柄和句柄表。
基於這樣的事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的回收算法,以便提升回收效率。
新生代因爲其對象存活時間短,且須要常常gc,所以採用效率較高的複製算法,其將內存區分爲一個eden區和兩個suvivor區,默認eden區和survivor區的比例是8:1,分配內存時先分配eden區,當eden區滿時,使用複製算法進行gc,將存活對象複製到一個survivor區,當一個survivor區滿時,將其存活對象複製到另外一個區中,當對象存活時間大於某一閾值時,將其放入老年代。老年代和永久代由於其存活對象時間長,所以使用標記清除或標記整理算法
總結:
垃圾回收器的任務是識別和回收垃圾對象進行內存清理,不一樣代可以使用不一樣的收集器:
總結:
Java 中的堆(deap) 也是 GC 收集垃圾的主要區域。
因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC有兩種類型:Scavenge GC(Minor GC)和Full GC(Major GC)。
基於大多數新生對象都會在GC中被收回的假設。新生代的GC 使用複製算法,(將年輕代分爲3部分,主要是爲了生命週期短的對象儘可能留在年輕代。老年代主要存放生命週期比較長的對象,好比緩存)。可能經歷過程:
GC日誌相關參數:
案例分析:-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime一塊兒使用
Application time: 0.3440086 seconds Total time for which application threads were stopped: 0.0620105 seconds Application time: 0.2100691 seconds Total time for which application threads were stopped: 0.0890223 seconds
得知應用程序在前344毫秒中是在處理實際工做的,而後將全部線程暫停了62毫秒,緊接着又工做了210ms,而後又暫停了89ms。
2796146K->2049K(1784832K)] 4171400K->2049K(3171840K), [Metaspace: 3134K->3134K(1056768K)], 0.0571841 secs] [Times: user=0.02 sys=0.04, real=0.06 secs] Total time for which application threads were stopped: 0.0572646 seconds, Stopping threads took: 0.0000088 seconds
應用線程被強制暫停了57ms來進行垃圾回收。其中又有8ms是用來等待全部的應用線程都到達安全點。
只要設置-XX:+PrintGCDetails 就會自動帶上-verbose:gc和-XX:+PrintGC
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs] 100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
從代碼上:
從JVM參數上調優上:
調優的最終目的都是爲了令應用程序使用最小的硬件消耗來承載更大的吞吐。jvm的調優也不例外,jvm調優主要是針對垃圾收集器的收集性能優化,令運行在虛擬機上的應用可以使用更少的內存以及延遲獲取更大的吞吐量。
最後
在此我向你們推薦一個架構學習交流圈。點擊加入交流圈 裏面資深架構師會分享一些整理好的錄製視頻錄像和BATJ面試題:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。
做者:Ccww_
連接:http://www.imooc.com/article/...