深入理解java虛擬機讀后總結(個人總結記錄)


1、jvm布局:
 
jdk1.6版本JVM布局分為:heap(堆),method(方法區),stack(虛擬機棧),native stack(本地方法棧),程序計數器共五大區域。
其中方法區包含運行時常量池。堆和方法區是線程共享的,虛擬機棧和本地方法棧、程序計數器是隨線程而建的。
 
1.1、堆:儲存對象信息和數組。對象信息/數組包括對象頭,實例數據和對齊填充共三個區域;
 
1.1.1、對象頭包括二/三部分內容:
 
一是類型指針,即對象指向它的類元數據的指針,通過這個指針來確定那個類的實例(指向方法區儲存的對象類型數據);
二是用於存儲對象自身的運行時數據,如哈希碼,gc分代年齡,鎖狀態標志,線程持有的鎖,偏向線程id,偏向時間戳等(ps:不了解這些,后續學習);
三是如果存儲的是數組,對象頭除了包含以上倆個內容外還必須得有數組的長度數據。
其中類型指針還有一個知識點是對象的訪問定位,對象的訪問方式主流的有倆種,句柄和直接指針。這倆種區別與特點是:
①、在對象被移動時(垃圾收集時會移動對象即內存整理),句柄只需修改句柄中的對象實例數據的指針,棧中的reference(引用)不需要修改。而直接指針需要修改。
②、直接指針的優勢就是速度更快,因為少了一次指針定位的時間開銷。
-- 具體句柄和直接指針圖解詳見書本第49頁。
 
1.1.2、實例數據包括實例化對象存儲的數據。
 
1.1.3、對齊填充:對齊填充並不是必然存在的,也沒有特殊含義。由於hotspot甲魚的臀部(規定)對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時就需要通過對齊填充來補全。
 
1.1.4、對象創建分配內存有倆種方式:指針碰撞(無內存泄漏)和空間列表(內存不連續,分配時找到一塊足夠大的內存划分給對象實例);選擇哪種方式創建由垃圾收集器是否有壓縮整理功能決定。
 
1.2、方法區:存儲已被jvm加載的類信息(類名,訪問修飾符,字段描述,方法描述等)、常量、靜態變量、即時編譯器編譯后的代碼(不清楚什么是即時編譯器編譯后的代碼)等數據。也就是說 final/static 的“基本數據類型變量”數據和指針放在方法區,沒有final/static 的“基本數據類型變量”數據和指針放在虛擬機棧中。jdk1.6及以前方法區和堆獨立區域,jdk1.7方法區中的字符串常量池放在堆中,jdk1.8刪除方法區改成元空間(還沒太了解)。
 
1.2.1、運行時常量池:存儲編譯期間生成的各種字面量和符號引用。其中有個知識點是string的instern()方法。
詳情見http://blog.csdn.net/hupoling/article/details/62423613的總結。
 
1.3、虛擬機棧:其實嚴格來說虛擬機棧包含局部變量表、操作數棧、動態鏈接、方法出口等信息。其中局部變量表存儲八種基本數據類型(byte,boolean,char,short,int,float,long,double)和對象引用。平常討論的棧都是指的局部變量。
 
1.4、本地方法棧:本地方法棧和虛擬機棧作業是十分相似的,只不過虛擬機棧是為虛擬機執行java方法(也就是字節碼)服務,而本地方法棧是為虛擬機執行native方法服務。sun hotspot虛擬機本地方法棧和虛擬機棧合二為一。
 
1.5、程序計數器:可以看做是當前線程執行字節碼的行號指示器。此區域是jvm規范中唯一沒有規定任何OOM情況的區域。
 
 
2、直接內存:在jdk1.4之后新加入了NIO,可以使用native函數庫直接分配堆外內存(本機內存),然后通過一個存儲在java堆中的directByteBuffer對象作為這塊內存的引用進行操作。避免了io在java堆和native堆來回復制數據提示對鞋性能。直接內存大小默認是java堆xmx(不知道為什么這樣設計 容易誤導);
 
3、OOM
 
3.1、堆溢出:Xmx:最大堆內存,Xms:最小堆內存,當Xmx=Xms時堆不擴展。-XX:+HeapDumpOnOutOfMemoryError ;實例化大量對象可測試堆溢出。
 
3.1.1、OOM解決:增大xmx擴大堆內存
 
3.2、棧溢出:棧溢出有倆種情況:
 
3.2.1、棧的深度超過最大深度限制,拋出StackOverflowError異常
 
3.2.2、棧擴展時內存不足,拋出OOM;
 
Xss:每個棧的大小;Xoss:本地方法棧大小,不過實際上xoss無效,棧容量只由-Xss參數設置。
3.2.3、實測中3.2.1說法有點不全。單線程下只有一個棧,所以只可能出現StackOverflowError,無論是棧的深度太大還是每個棧幀過大到賬內存不足;多線程下會出現OOM,不斷建線程時會出現內存不足;
 
3.2.4、OOM/StackOverflowError解決:
 
3.2.4.1、OOM:由於棧內存=操作系統內存 - 堆內存 - 方法區內存 - 程序計數器內存,所有可以減小堆內存來擴大棧內存大小。棧內存大小影響系統並發線程量(棧內存>=每個棧的大小xss*線程量);具體設置由n台服務器,每台服務器m個cpu,則最大線程量=n*m;每個棧的大小xss<=棧內存/n*m; 注意一下有個問題 ,這個公式沒有直接內存?
 
3.2.4.2、StackOverflowError深度:如果使用jvm默認設置,棧的深度大多數情況下可達到1000~2000,足以在日常開發中使用。注意避免代碼中存在超過1000的方法嵌套。每個方法嵌套對應一個棧幀。
 
3.2.4.3、StackOverflowError棧幀大小:單線程下避免代碼中存在大量基本類型或對象引用。
 
3.2.4.4、多線程下假設每個棧幀特大,jvm是拋出OOM還是StackOverflowError?(待考察研究)
 
3.3、方法區OOM:-XX:PermSize:最小方法區內存大小;-XX:MaxPermSize:最大方法區內存大小。
 
3.3.1、OOM解決:避免大量的string.intern();避免大量的動態java(jsp,java反射)。
 
3.4、本機直接內存溢出:-XX:MaxDirectMemorySize:最大直接內存;默認為Xmx
 
3.4.1、OOM解決:使用NIO分配本機內存多注意是否超過MaxDirectMemorySize;平常開發容易忽略直接內存;
 
 
 

 

第二部分:第三章

1.垃圾回收條件

jvm垃圾回收分為倆點,一是對象的內存回收也就是堆內存回收,二是字面量(運行時常量池)和類信息內存回收也就是方法區內存回收。

1.1 對象內存回收(heap)

如何判斷對象是否可以回收,市場上有引用計數算法和可達性分析算法兩種方法 :

1.1.1 引用計數算法:簡單來說就是,一個對象A被引用一次,對象A的引用次數就加一。當對象A的引用次數為0(也就是沒有其他地方引用此對象)時可以回收。然而這里存在個問題就是 對象之間互相引用 A->B,B->A時 AB無法回收。不過微軟公司的com(componet object model)技術用的就是這個算法,而目前流行的主流的jvm沒有選用此算法的。

1.1.2 可達性分析算法:簡單來說就是,通過一系統成為Gc Roots的對象為起始點,當沖GC Roots到達不了對象A時,則對象A是不可用的,可回收的。

java語言中,GC Roots包括四種對象:

1.1.2.1 虛擬機棧(棧幀中的本地變量表)中引用的對象

1.1.2.2 本地方法棧中(native方法)引用的對象

1.1.2.3 方法區中類靜態屬性引用的對象

1.1.2.4 方法區中常量引用的對象

之所以是上面四種,總結為:GC管理的主要區域是Java堆,一般情況下只針對堆進行垃圾回收。方法區、棧和本地方法區不被GC所管理,因而選擇這些區域內的對象作為GC roots

1.2 方法區內存回收:jvm規范不要求虛擬機在方法區實現垃圾收集(因為性價比低,回收一次回收的空間實際情況很小),但是jvm還是有方法區回收的。

1.2.1 字面量回收:如果一個字面量沒有被引用就回被回收;

1.2.2 類信息回收:類信息回收需要同時滿足3個條件:

1.2.2.1 java堆中不存在此類的實例;

1.2.2.2 加載該類的classloader已經被回收

1.2.2.3 該類對應的java.lang.Class對象沒有任何地方引用,也就是說無法再任何地方通過反射訪問該類的方法。

1.2.3 其他常量池的東西 暫不清楚。

 

1.3 四大類型引用:自jdk1.2之后(jdk1.1引用分的種類不夠明確),jvm中還有幾種類型引用比較特殊;。分別是 強引用、軟引用、弱引用、虛引用。

1.3.1 強引用:Object obj=new Object();只要強引用還存在,垃圾收集器永遠不會回收被引用的對象。假如obj=null或者obj引用的棧幀出棧(不存在)就會被回收。

1.3.2 軟引用:將要發生內存溢出溢出之前,將會把軟引用的對象進行回收。SoftReference類實現軟引用。

1.3.3 弱引用:只能生存到下一次gc之前。WeakReference類實現弱引用。

1.3.4 虛引用:這個需要注意,虛引用無法用來取得一個對象的實例。虛引用不會對對象的生存時間有任何影響,只是能在這個對象被回收時收到一個系統通知。推薦這篇文章更加了解虛引用:http://www.mamicode.com/info-detail-988201.html。

關於四種引用推薦一篇文章里面有詳細例子:http://blog.csdn.net/u011277123/article/details/53908315

 

1.4 finalize() 對象自救

對象在可達性分析之后還需要進行一些邏輯。哪怕對象在可達性分析之后沒有發現與GcRoots的引用鏈,jvm還需要進行下面幾步(需要倆次標記):

1.4.1 沒有發現引用鏈,進行第一次標記

1.4.2 判斷對象是否要執行finalize()方法:當對象沒有覆蓋finalize()方法或者已經被執行過一次finalize()方法;則此對象不執行finalize()方法,否則進行執行finalize()方法(把對象放在F-Quene隊列中);

jvm自動建立的,低優先級的Finalizer線程去執行放在F-Quene的對象里的finalize()方法。

1.4.3 進行第二次標記,判斷對象是否有引用鏈,沒有則回收。

所以如果對象自救(第一次標記沒有引用鏈,第二次標記有引用鏈),只有在覆蓋finalize()方法激活對象(讓對象被引用)。

 

2.垃圾收集算法:jvm垃圾收集可以分為四種(嚴格來說是三種):標記-清除算法;標記-整理算法;復制算法;分代收集算法。

2.1 標記-清除算法:先把可回收內存標記,然后在清除掉;缺點是:會存在內存碎片導致內存泄漏。

2.2 標記-整理算法:先把可回收內存標記,然后讓存活的對象都向一端移動,最后清除點端邊界以外的內存;缺點:比較標記-清除時間增長;

2.3 復制算法:把內存平分為兩份s1,s2;保證只用一個內存區s1,另一個為空s2;把存活的對象復制到為空的那個內存區域s2,然后在清除掉這個區域s1;缺點:內存減半

2.4 分代收集算法:根據jvm特性,年輕代實際每次gc時存活對象較少,故用推薦復制算法;年老代存活對象較多,並且沒有其他內存為年老代分配擔保(分配擔保:舉個栗子:年老代為年輕代進行分配擔保,當年輕代minor gc內存不足時【比如有個大對象obj1】obj1會放到年老代中;而年老代沒有其他空間為其分擔了)所以推薦標記-整理算法。

3.hotspot算法實現:暫時未深入理解

4.垃圾收集器:共七中垃圾收集器,分別是serial、parnew、parallel scavenge和cms、serial old、 parallel old、G1;其中serial、parnew、parallel scavenge用於年輕代,cms、serial old、 parallel old用於年老代,g1用於年輕代和年老代。所有收集器都存在stop the world,不過在java發展中 一直在優化停頓時間。

先總結比較這七個收集器:

年輕代:

4.1.Serial:適用於年輕代垃圾回收,復制算法,jdk1.3之前 推薦用於客戶端模式(Client)下的虛擬機,屬於單線程,無對stop the world優化,屬於最老的年輕代垃圾收集器產品;

4.2 ParNew:適用於年輕代垃圾回收,復制算法,jdk1.3發布,推薦用於服務端模式(Server)下的虛擬機,屬於多線程,無對stop the world優化,其實就是Serial的多線程版本;是服務端開發的首先;

4.3 Parallel Scavenge:適用於年輕代垃圾回收,復制算法,jdk1.4發布,屬於多線程,無對stop the world優化,它是一個可以控制吞吐量的收集器,擁有自適應調節策略;需要注意一點的是,gc停頓時間縮短是犧牲吞吐量和新生代空間來換取的。

stop The World:gc時 需要停止所有線程進行垃圾回收;

吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

自適應調節策略:虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整參數以提高最合適的停頓時間或最大吞吐量。

年老代

4.4 Serial old :適用於年老代垃圾回收,標記-整理算法,jdk1.5之前,主要用於client下的虛擬機,如果用在server模式下主要有倆個作用:一是jdk1.5以及之前用於與Parallel Scavenger搭配使用,二就是為cms提供后備預案,屬於單線程,無stop the world優化。

4.5 Parallel Old:適用於年老代垃圾回收,標記-整理算法,jdk1.6發布,屬於多線程,注重於吞吐量控制,是為了Parallel Scavenge定制的;

4.6 CMS: 適用於年老代垃圾回收,標記-清除算法,jdk1.5發布,推薦server模式下,屬於多線程,對stop the world有優化,CMS是一種以獲取最短回收停頓時間為目標的收集器,也就是優化服務器響應速度。CMS分為4步:其中 初始化標記,重新標記 stop the world

4.6.1 初始標記:僅僅只是標記GcRoots可以直接關聯的對象;

4.6.2 並發標記:進行gcroots tracing(也就是 引用鏈搜索);優化成並發

4.6.3 重新標記:修正並發標記因用戶程序繼續運行而導致標記產生變動那一部分對象的標記記錄。

4.6.4 並發清除:並發清理標記的對象內存。

4.6.5 CMS缺點:

4.6.5.1 CMS對cpu資源非常敏感,默認啟動的回收線程數是(cpu數量+3)/4;垃圾回收線程數量不少於25%的cpu資源,當cpu越大,線程數/cpu總量 越小;但是當cpu小於4個時 顯得就不怎么合適了;

4.6.5.2 無法處理浮動垃圾,需要等下一次gc才能處理;並且還需要預留一部分空間提供並發收集時的用戶程序運作使用,即啟動閾值(-XX:CMSInitiatingOccupancyFraction 閾值百分比);要是CMS運行期間預留的空間不滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這是虛擬機啟動后備預案:臨時啟用Serial Old,這樣一來停頓時間就很長了,所以-XX:CMSInitiatingOccupancyFraction 設置太高容易導致大量“Concurrent Mode Failure”失敗,性能反而降低

4.6.5.3 由於用的是標記-清除算法,所以會出現內存碎片;CMS提供-XX:UseCMSCompactAtFullCollection開關參數(默認為開),用於CMS收集器要進行FullGC時進行內存碎片合並整理,-XX:CMSFullGCsBeforeCompaction 用來設置執行多少次不壓縮Full GC后跟着來一次帶壓縮的(默認為0,表示每次進入Full GC都進行碎片壓縮)。

 

年輕代+年老代

4.7 G1:jdk1.7發布,整體看是“標記-整理算法”,從局部(倆個Region之間)上來看是基金“復制”算法實現,也就是說沒有內存碎片;

如果應用追求低停頓,那G1現在可以作為一個嘗試的選擇,如果應該追求吞吐量,G1並不會帶來什么特別的好處!!!

 

5.內存分配策略:共有 4個策略

5.1 對象優先在Eden分配:major gc時經常會伴隨至少一次的minor gc,但並非絕對,比如說 Parallel Scavenge就是直接major gc沒有伴隨minor gc;

5.2 大對象直接進入老年代 -XX:PretenureSizeThreshold設置最大對象,單位B;PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效;Parallel Scavenge收集器不需要設置;如果遇到必須使用此參數的場景,可以考慮ParNew+CMS組合;

5.3 長期存活的對象放入老年代:虛擬機給每個對象定義了一個對象年齡(Age)計數器;gc一次Age+1,當Age大於一定程度(默認為15歲)就會被晉升到老年代;-XX:MaxTenuringThreshold:最大年齡;

5.4 動態對象年齡判定:如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於改年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡;

5.4空間分配擔保:jdk6 update 24后,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行major gc,否則Full GC;

 

 

第二部分:第四章

1.jdk的命令行工具簡單總結介紹

1.1 jps: 獲取虛擬機進程 vmid;jps -v :顯示虛擬機對應啟動的參數

1.2 jinfo:獲取虛擬機參數值和動態修改部分運行期可改的參數。jinfo [option] pid例如:jinfo -flag PermSize vmid;顯示vid對應的虛擬機的方法區大小;jinfo -flag +/- name:添加或刪除name屬性;

1.3 jmap:java內存影像工具, jmap -heap vmid:顯示堆的詳細信息,如參數配置,分代狀況;jmap -histo vmid:顯示堆中對象統計信息,包括類,實例數量,合計容量。

1.4 jstat:虛擬機監控工具,jstat -gccause vmid:輸出已使用空間占各自總空間的百分比;

1.5jhat:虛擬機堆轉儲快照分析工具;jstack:java堆棧跟蹤工具;

2.jdk的可視化工具:jConsole和visualVm;其中VisualVM有個BTrace插件值得注意。

2.1 BTrace 動態日志跟蹤:可以打印調用堆棧、參數、返回值、性能監視、定位連接泄漏、內存泄漏、解決多線程競爭問題。如生產上遇到問題時需要方法輸入參數和返回輸出參數,但開發時沒有日記記錄,平常做法就是補充上日志,然后在重現上線。而BTrace可以在不停止jvm的情況下動態調試代碼。(ps:BTrace后續深入研究)

推薦一篇文章:http://huanghaifeng1990.iteye.com/blog/2121419

第二部分:第五章

簡單總結:
一、jvm調優思路:
第一步:jps 獲取jvm id;
第二步:獲取jvm垃圾收集器種類
第三步:查看gc次數和時間,分析原因來優化
gc次數頻繁:①、內存回收率低導致短時間內回收次數多;②、內存大小太小;
gc時間長:①、內存過大;②、內存擴展導致時間長(固定內存大小)
選擇適合的收集器也可大幅度優化jvm。
二、優化思路注意點:
1、64位jdk的性能測試結果普遍低於32位jdk;
2、64位jdk由於指針膨脹和數據類型對齊補白導致消耗的內存比32位大;
3、使用nio時,堆外內存不足導致內存溢出。

 

 摘自:https://blog.csdn.net/hupoling/article/details/62887251


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM