從HelloWord學習JVM虛擬機

一、爲什麼學習JVM

面試、找工作、OOM、內存調優?

二、什麼是JVM,它做了什麼

  • java虛擬機:執行java代碼的平臺,屏蔽了底層硬件指令的細節,一次編寫到處執行
  • 代碼執行過程:源代碼->字節碼文件class->-->jvm

jvm&jdk&jre 關係  jdk包括jre和jvm

  • jvm做了什麼?
  1. 空間分配回收,而c++需要考慮內存分配和回收。java能讓開發者100%精力投入業務開發
  2. 內存管理
  3. 屏蔽底層硬件區別

三、jvm運行時運行區

1,程序計數器:指向當前線程執行的字節碼指令的地址,行號,如0x3d48

java最小的執行單位是線程,但線程只負責做,做什麼需要指令告知;

程序計數器爲什麼要記錄地址呢?因爲cpu是有調度策略的,當cpu被其他線程搶到了,此時該線程被掛起,此時需要記錄上一次執行到的地址方便線程恢復執行。

2,虛擬機棧(FILO先進後出):存儲當前線程運行方法時所需要的數據,指令和返回地址。

如一個helloword()方法,一個方法一個棧幀,局部變量表存儲局部變量,一個程序運行時就能確定的空間,一個棧層存儲32位的數,即一個int,多個字節要拆開存儲如64位拆爲高位和地位。

操作數棧:比如算sum=i+j  從局部變量表加載i,j,然後做加法得到sum,再將sum存到局部變量表,即對應4條指令。

注意:成員變量不管是不是引用類型都存儲在堆內存中的。

但對於引用類型obj。局部變量表只記錄obj的地址應用,所以結論棧指向堆。

學會看反編譯字節碼文件,javap -v HelloWord.class > aa.txt 參考javap指令集查看https://www.cnblogs.com/JsonShare/p/8798735.html

如methodOne(int i){int j=0; } ,僅一個int j=0 對應兩條指令;  j存在局部變量第二位置裏,地址0:存this 地址一1:存 i  地址2:存j。

 

動態鏈接:java動態特性,如運行時多態,如@Autowired private Service service;

代碼調用serevice.do()。需動態解析,到底是哪個實現類的do方法。爲什麼動態鏈接要存在棧幀裏呢?

方法出口:即return,正常出口和異常出口如throw。

一個方法調用另一個方法,怎麼入棧?先入棧methodOne,再入棧methodTwo

 

3,本地方法棧:native方法棧,

4,方法區:存儲類信息即class文件,常量,靜態變量,JIT,methodOne存儲在方法區。

5,堆內存:分代模型 就分新生代和老年代,跟後面的永久代和元空間沒關係。

注意:jdk1.8後 永久代變成了元空間meta space主要解決永久代溢出的問題,因爲元空間會自擴容,跟arraylist差不多。

方法區就存在永久區。老年代默認是新生代的2倍。創建對象首先在eden區詢問空間,有則放下,沒有則觸發yonggc ,把之前的對象移動到surivor區域的from區域,如果from也放不下,就會觸發擔保機制,直接放到老年代,每新生代gc一次,就觸發from和to的交換一次,對象向下移動年齡是加1的。

Xms  s:starting堆初始的總大小

Xmn  n:new  堆初始時new空間的大小

Xmx   x:max堆最多的空間

 

垃圾回收算法:gc算法:複製回收算法(新生代用的),標記清除算法和標記內存整理算法(後面倆是老年代用的)。

垃圾回收器:不同的垃圾回收器是實現了不同的垃圾回收算法的,如parllen new等實現了

複製回收算法,eden區一定是空的。

 

內存溢出後排查是否是內存泄露。

對象的生命週期,什麼對象能被回收,即是否可達,gc root概念,當一個對象被應用時計數器就加1,當爲0時即無引用代表可回收。堆裏有很多gc root只要有gcroot指向的都是可達的,都不能回收。

gc root有哪些:有4種;強引用、軟引用、弱引用、虛引用

 

FullGc 5分鐘一次怎麼調優? 衡量的標準和維度?5分鐘一次怎麼啦?是要追求吞吐率還是最小執行時間?業務出現性能問題,90%以上是業務調優,並不是有問題就調jvm。

內存泄露,要找出gcroot,即泄露源,因爲它指向了很多對象,或者死循環對象,被回收不了。

性能調優:發揮機器本來的性能。

1,如果追求吞吐率就要算吞吐率,看下回收器用的什麼,如可用cms就是追求吞吐率的。

2,

 

 

總結:程序計數器,虛擬機棧,本地方法棧是線程獨有的,每個線程都有,但堆和方法區是共有的。

 

四:類加載過程

源碼-》字節碼-》裝載-》鏈接-》初始化

裝載:怎麼裝載的,用類加載器

1,通過類的全限定名找到類的二進制流;

2,將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;

3,在java堆中生成一個java.lang.calss對象,作爲方法區中這些數據的入口;

鏈接:驗證,保證類加載的正確性,如文件格式,符號引用等;準備,爲類的靜態變量分配內存,並將其初始化爲默認值如private static int a=10,將a初始化爲0而不是10:解析,把類中的符號引用轉換爲直接引用;

4,初始化:對類的靜態變量或靜態代碼塊執行初始化操作;這時a才初始化爲10;

五:類加載機制&類加載器

bootstrap classloader 負責加載jre/lib/rt.jar

extension classloader 加載javahome中的jre/lib/*.jar

app classloader 負責加載clsspath下的類和jar包

custom classloader 自定義加載類,實現ClassLoadeer

雙親委派機制:所有類加載原則,都是回溯到父類去加載,一直到最頂層加載,沒有再交給孩子節點加載,保證同一個類只有一個層級加載;

六、運行時數據區 共6塊

兩塊:一個是跟隨線程生命週期的如虛擬機棧,本地方法棧,程序計數器;另一個是跟隨jvm生命週期的如堆方法區

方法區:

常量final修飾的,靜態變量static修飾的;方法區是線程非安全的,因爲共享;

堆:所有線程共享,對象和數組

虛擬機棧:

 

七、內存分配模型

1,非堆,就是方法區

2,堆分爲old和young,young又分爲eden和s0和s1塊,eden:s0:s1一般等於8:1:1,爲什麼這個比例,因爲浪費的最少,而且80% eden區域可以存下更多對象,且eden大部分都是朝生夕死的,所以不能太小了。

3,剛分配的對象,首先分配給eden區,大對象直接放old區,如果不夠用則觸發young gc,對象年齡就加一,一般到達15歲就會到old區

4,young gc過程:將死亡的對象垃圾回收,但這導致了空間不連續,有垃圾碎片,爲了空間連續能夠再次多盛對象,就把存活的對象複製到s0,再次young gc時把eden和s0對象放到s1去,這樣s0和eden就可以清空,空間就連續了,所以s0和s1永遠都有一個使用一個未使用。

5,當young gc=minor gc時,若s0放不下,則觸發擔保機制,向old區域借空間存放對象。

fullgc = old gc+young gc

注意:s0和s1總有一個空閒,所以浪費了10%的空間,但目的就是爲了解決空間碎片的問題,浪費了空間

總體流程圖如下:

jvisualvm內存查看工具,

八,jvm垃圾&回收

1,判斷對象是不是垃圾,

a,引用計數法(有引用計數加1,但存在兩個對象循環引用的問題導致永遠無法回收)

b,可達性分析,gc root可達或間接可達都不能算做垃圾,gc root主要有:類加載器,Thead類、本地變量表、static成員,常用引用,本地方法棧中的變量

2,垃圾回收算法

a)標記清除:循環內存所有內存區域,找出不可達的對象,將其標記釋放,但這個導致了空間不連續,有空間碎片產生

b)複製算法:內存再一分爲二,循環內存所有區域,將所有存活的對象複製到空閒區域,複製時連續存放,解決了標記清除碎片問題,但基本空間浪費一半空間。

c)標記整理:綜合了a和b兩種,首先循環所有內存區域進行標記,標記完將存活對象整理成連續存放,最後釋放垃圾對象

3,各個區域和回收機制的配合使用-分代收集算法:不同的代使用不同的回收算法機制

young區:使用複製算法,因爲eden區當gc時大部分對象都死了,所以只需要複製少量存活對象到from區域即可。

old區域:標記清除或標記整理

4,垃圾回收器:垃圾回收算法的實現

主要有串行的,並行的,cms,g1的等,各個回收器使用的範圍如圖:

上述適用於young區域的都是實現了複製算法的稅收器,連線關係是搭配使用的關係;下層都是實現了標記整理的回收器;

serial,串行回收,當需要gc時,停止所有用戶進程,啓動一個進程串行回收垃圾,

parnew,並行,當需要gc時,停止所有用戶進程,啓動多個進程並行回收垃圾,

paralleScavenge:相比parnew更加關注吞吐量,

cms:主要關注停頓時間。比較主流,先單線程初始標記,找到gcroot關聯的對象,這個很快所以不需要花額外開銷開多線程,第二步:再並行標記,防止初始標記不完成,但此刻並行標記線程可以和用戶進程併發執行了,重新標記需要stop the world(STW),最後併發清理,不需要STW,具體圖下圖:

G1回收器;使用於新生代和老年代,主要關注停頓時間,比cms高端的是,用戶可以設置停頓時間,供 4刷選回收使用

分爲4個過過程,1,初始標記;2並行標記,3最終標記  4刷選回收。其中1,3,4步,需要stop the world

 

九、如何選擇回收器,如何開啓

  • 垃圾回收器分類,

1,串行的serial和serial old 只有一個線程回收,需要停用戶線程,適用於內存較小的設備,

2,並行收集器(吞吐量優先),如parallel scanvenge 、parallel old多條垃圾收集器線程共同工作,用戶線程處於等待狀態,適用於後臺運算而不需要太多交互的任務

3,並行收集器(停頓時間優先),如cms和g1,用戶線程和垃圾回收器線程同時執行(但不一定並行,可能是交替執行),不會停用戶線程,適用於對運行時間有要求的場景,如web對響應時間有要求

  • 評判垃圾回收器好壞的指標:吞吐量和停頓時間,調優也主要調這兩個指標

停頓時間:垃圾回收器進行垃圾回收終端應用執行的響應時間,停時間越短越適合與用戶交互

吞吐量:運行用戶代碼的時間/(運行用戶代碼時間+垃圾收集時間),吞吐量越高越有效的利用了cpu時間,cpu利用率越高。

如何選擇收集器

如何開啓

十、jvm參數,命令,工具

1,標準參數,不隨着

2,-X參數,

3,-XX參數,有bool值和參數設置值 如-XX:+:UseG1GC  +/-代表開啓和關閉    -XX:name=value如設置堆內存大小等

4,其他參數,如-Xms100M  <=> 等價於 -XX:InitialHeapSize=100M   再比如 -Xmx100M    -Xss100M等

設置方式:在開發工具中有vm options參數設置或者java命令後設置,或者tomcat啓動.sh中設置

JVM命令:

1,jps:查看當前所有的java進程,主要用來獲取pid

2,jinfo:查看某個java進程的參數設置, 如jinfo -flag UseG1GC  pid      查看參數UseG1GC設置情況   jinfo -flags pid查看該pid的所有參數設置

3,jstat:查看當前java進程統計信息,如:jstat -gc pid 1000 10 =>查看該pid gc運行情況,每隔1000ms打印一次,共打印10次

4,jstack,查看當前進程堆棧信息  如 jstack pid,查看當前進程有那些線程,比如代碼中死鎖,用這個一目瞭然

5,jmap,打印出堆轉存儲快照,如jmap -heap pid  會打印出當前老年代,新生代等存儲信息。

或dump出堆內存相關信息到文件 如:jmap -dump:format=b,file=xxx.hprof PID

也可以設置當堆內存溢出時自動dump,,只要在jvm參數加上-XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=heap.hprof  

 

常用工具

1,jconsole監聽某一個具體的java進程

2,jvisualvm

3,arthas 阿里的火焰圖工具,

4,mat/perfma查看dump出的內存文件

5,gceasy.io網址,直接在線選擇gc日誌文件分析查看即可、   gcviewer本地工具,查看gc的回收

 

 

類加載執行過程,調用本地方法時有本地方法接口可以調用。

十一,性能優化

1,OOM後dump出prof文件,用prof查看工具如mat/perfma,查看時有histogram,可以查看其中的類對象及個數, leak suspects 泄露疑點,打開可以看details詳情。

2,GC優化:通過不斷的調整,觀察GC日誌的吞吐量和停頓時間,尋找最佳值。

jvm參數使用 -Xloggc:/home/admin/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps  -XX:+PrintGCTimeStamps  -XX:+UseConcMarkSweepGC可以打印gc日誌。通過gceasy.io網站,或者gcviewer本地查看,主要關兩個指標,一個是avg pause gc time停頓時間和throughput吞吐量。

調優目標:高吞吐量和低停頓時間。假如我能忍受200ms的停頓時間,就可以調吞吐量了。主要手段就是調整堆內存大小,垃圾回收器選擇,回收的線程數,堆佔用比例是多少時觸發gc,然後觀察吞吐量。

GC優化指南,發現問題->排查問題->解決方案

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章