日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

java虚拟机学习笔记

發布時間:2023/12/20 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java虚拟机学习笔记 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

?

一、java運行時數據區域

?

?

1、程序計數器

2、虛擬機棧:用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息

3、本地方法棧:與虛擬機棧的區別是虛擬機棧是為虛擬機執行的java方法服務,本地方法棧是為虛擬機使用到的本地(native)方法服務

4、堆

5、元空間:用于存儲被虛擬機加載的類型信息、常量、靜態變量;運行時常量池屬于元空間的一部分,用于存儲各種字面量和符號引用

?

二、java對象存儲布局

在hotspot虛擬機里,對象在堆內存中的存儲布局可以分為三部分:對象頭、實例數據(對象的實例數據就是在java代碼中能看到的屬性和他們的值)、對齊填充(因為JVM要求java的對象占的內存大小應該是8bit的倍數,所以后面有幾個字節用于把對象的大小補齊至8bit的倍數,沒有特別的功能)

對象頭包括兩類信息,第一類是用于存儲對象自身的運行時數據,比如哈希碼、gc分代年齡、鎖狀態標志、線程持有的鎖、偏向鎖ID、偏向時間戳等。

這部分數據稱為mark word

Mark Word記錄了對象和鎖有關的信息,當這個對象被synchronized關鍵字當成同步鎖時,圍繞這個鎖的一系列操作都和Mark Word有關。

Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

Mark Word在不同的鎖狀態下存儲的內容不同,在32位JVM中是這么存的:

?

JVM一般是這樣使用鎖和Mark Word的:

?

1,當沒有被當成鎖時,這就是一個普通的對象,Mark Word記錄對象的HashCode,鎖標志位是01,是否偏向鎖那一位是0。

?

2,當對象被當做同步鎖并有一個線程A搶到了鎖時,鎖標志位還是01,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的線程id,表示進入偏向鎖狀態。

?

3,當線程A再次試圖來獲得鎖時,JVM發現同步鎖對象的標志位是01,是否偏向鎖是1,也就是偏向狀態,Mark Word中記錄的線程id就是線程A自己的id,表示線程A已經獲得了這個偏向鎖,可以執行同步鎖的代碼。

?

4,當線程B試圖獲得這個鎖時,JVM發現同步鎖處于偏向狀態,但是Mark Word中的線程id記錄的不是B,那么線程B會先用CAS操作試圖獲得鎖,這里的獲得鎖操作是有可能成功的,因為線程A一般不會自動釋放偏向鎖。如果搶鎖成功,就把Mark Word里的線程id改為線程B的id,代表線程B獲得了這個偏向鎖,可以執行同步鎖代碼。如果搶鎖失敗,則繼續執行步驟5。

?

5,偏向鎖狀態搶鎖失敗,代表當前鎖有一定的競爭,偏向鎖將升級為輕量級鎖。JVM會在當前線程的線程棧中開辟一塊單獨的空間,里面保存指向對象鎖Mark Word的指針,同時在對象鎖Mark Word中保存指向這片空間的指針。上述兩個保存操作都是CAS操作,如果保存成功,代表線程搶到了同步鎖,就把Mark Word中的鎖標志位改成00,可以執行同步鎖代碼。如果保存失敗,表示搶鎖失敗,競爭太激烈,繼續執行步驟6。

?

6,輕量級鎖搶鎖失敗,JVM會使用自旋鎖,自旋鎖不是一個鎖狀態,只是代表不斷的重試,嘗試搶鎖。從JDK1.7開始,自旋鎖默認啟用,自旋次數由JVM決定。如果搶鎖成功則執行同步鎖代碼,如果失敗則繼續執行步驟7。

?

7,自旋鎖重試之后如果搶鎖依然失敗,同步鎖會升級至重量級鎖,鎖標志位改為10。在這個狀態下,未搶到鎖的線程都會被阻塞。

?

第二類是類型指針,即對象指向它的類型元數據的指針,jvm通過這個指針來確定該對象是哪個類的實例。

?

?

三、jvm判斷對象是否存活的算法

1、引用計數法(無法解決對象之間相互循環引用的問題)

2、可達性分析法

這個算法的基本思路是通過一系列稱為“GC Roots“的根對象作為起始節點集(GC Root Set),從這些節點開始,根據引用關系向下搜索,如果某個對象到GC Roots間沒有任何路徑可達,則該對象將被判定為可回收的對象。

?

在jvm里,可固定作為GC Roots中的對象包括以下幾種:

1、在虛擬機棧中引用的對象,比如當前正在運行的方法所使用的參數、局部變量表、臨時變量等

2、在方法區中類靜態屬性引用的對象,比如java類的引用類型靜態變量

3、在方法區中常量引用的對象,比如字符串常量池里的引用

4、在本地方法棧中JNI(java Native方法)引用的對象

5、jvm內部的引用,比如基本數據類型對應的Class對象、系統類加載器

6、所有被同步鎖(synchronized)持有的對象

?

在可達性分析算法中判定不可達的對象,也不是一定會被清理掉,在被標記為不可達后,會判斷此對象是否有必要執行finalize()方法,假如該對象沒有重寫finalize方法或該對象的finalize方法已經被jvm調用過一次了,則該對象會被回收,否則就執行該對象的finalize方法,如果該對象在finalize方法中重新與引用鏈關聯上,則該對象本次就不會被回收了。(下次不可達時就會被回收,因為已經執行過finalize方法了)

?

從根節點枚舉,是必須要暫停用戶線程的(STW),當然也有OopMap、安全點、安全區域、記憶集與卡表、寫屏障等種種措施來降低枚舉根節點時(可達性分析)的stw時長

?

?

四、垃圾回收算法

1、標記-清除算法

標記出所有需要回收的對象,再統一回收掉所有被標記的對象,標記過程就是判定對象是否屬于垃圾的過程(對象不可達)

該算法會產生大量的內存碎片

?

2、標記-復制算法(新生代使用)

基本思路是將內存劃分為大小相等的兩部分,每次只使用一部分,當這一部分用完了,就將還存活的對象復制到另一塊上面,然后再把已使用的內存空間一次性清理掉。

該算法沒有內存碎片問題,但如果內存中多數對象是存活的,就會產生大量的內存間復制的開銷,并且內存每次只有一半在使用,利用率太低。

?

由于新生代的對象有98%熬不過第一輪垃圾回收,因此并不需要按照1:1的比例來劃分新生代內存空間。

而是劃分為一塊Eden和兩塊survivor空間,每次分配內存只使用eden和一塊survivor(fromSurvivor),垃圾收集時,將eden和survivor仍存活的對象復制到另一個survivor(toSurvivor)上,然后直接清理掉eden和survivor(fromSurvivor)空間,然后fromSurvivor變成toSurvivor, toSurvivor變成fromSurvivor。在hotspot中,eden和survivor默認比例是8:1,

當toSurvivor的空間不足以容納垃圾回收后(minor GC/Young GC)存活的對象時,就要依賴其他內存區域(大多數是老年代)進行分配擔保

?

3、標記-整理算法(老年代使用)

先標記所有存活的對象,然后將所有存活的對象向內存空間的一端移動(移動對象期間需要stw),再清理掉邊界以外的內存。

?

補充:垃圾回收名詞解釋

部分收集(Partial GC):

新生代收集(Minor GC/Young GC)

老年代收集(Major GC/Old GC)

整堆收集(Full GC)

?

五、垃圾回收器

新生代:

1、serial(在jdk9中,已經取消serial與cms的配合工作)

2、parNew(目前只有parNew能和cms配合工作)

3、parallel scavenge(吞吐量優先收集器)

老年代:

1、cms

2、serial old

3、parallel old

同時工作在新生代與老年代:

G1

?

CMS收集器(concurrent Mark Sweep),工作在老年代,使用標記-清除算法,

工作過程分為四個步驟

1、初始標記(需要stw)

2、并發標記

3、重新標記(需要stw)

4、并發清除

?

初始標記僅僅只是標記下gc roots能直接關聯到的對象,速度很快;

并發標記階段就是從gc roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但不需要停頓用戶線程;

重新標記階段是為了修正并發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的

停頓時間會比初始標記稍長一些,但比并發標記時間短的多。

并發清除階段清理掉被判定已死亡的對象。耗時也較長,但沒有停頓用戶線程。

?

Cms缺點:

1、并發階段,雖然不會導致用戶線程停頓,但是會因并發而占用一部分線程(或者說是cpu的計算能力)而導致應用程序變慢,降低總吞吐量。

Cms默認啟動的回收線程數是(cpu核數 + 3)/4,當cpu核數是4或以上時,cms并發回收線程占用不少于25%的cpu資源,如果cpu核數小于4,這個比值

就較高了,cpu壓力變大。

?

2、CMS無法處理浮動垃圾,有可能出現“concurrent mode failure“進而導致一次完全的stw的full GC的產生。

在cps并發標記和并發清理階段,用戶線程繼續運行,自然就可能會有新的垃圾對象不斷產生,但這一部分垃圾是出現在標記過程結束以后,cms無法在本次收集中

處理掉它們,只能等待下一次垃圾收集,這就是所謂的浮動垃圾。同樣由于垃圾收集階段用戶線程還在持續運行,所以必須預留足夠的內存空間提供給用戶線程。

因此cms并不會等到老年代幾乎完全滿了再進行收集。在jdk6之后,cms默認是老年代內存使用比92%(-XX:CMSInitiatingOccupancyFraction)之后開始垃圾回收,如果cms預留的內存(8%)無法滿足用戶線程時,就會出現一次“concurrent mode failure“,這時jvm就會凍結用戶線程執行,臨時啟用serial old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

所以-XX:CMSInitiatingOccupancyFraction設置得太高容易導致大量的并發失敗產生,性能反而降低,設置得太低會導致cms更容易觸發垃圾回收,也不好,生產環境需要根據實際情況把握

?

3、內存碎片問題。Cms采用標記-清除算法,會有內存碎片問題,內存碎片過多時,會給大對象分配帶來很大麻煩,往往會出現老年代還有很多剩余空間,但就是無法找到足夠大的

連續空間來分配當前對象,而不得不提前觸發一次Full GC的情況。為了解決這個問題,cms提供了-XX:+UseCMS-CompactAtFullCollection的開關參數(默認是開啟的,jdk9廢棄),

用于在cms不得不進行full gc時開啟內存碎片的合并整理過程(會將所有存活對象移動到內存一端,需要stw)

?

G1收集器(Garbage First收集器)

保留了新生代和老年代概念,整個堆內存劃分為若干個均等region區域(相當于新生代劃分為若干個region,老年代劃分為若干個region),G1從整體上來看是基于

標記-整理算法,從局部看(兩個region之間)是基于標記-復制算法,不管如何,都不會產生內存碎片問題,有利于程序長時間運行,在程序為大對象分配內存時不容易

出現因無法找到連續內存而提前觸發full gc的情況

?

G1適合工作在堆內存較大的配置下(8G或以上),cms適合堆內存較小(8G以下)。

就內存占用來說,雖然G1和CMS都采用卡表來處理跨代引用問題,但G1的卡表實現更復雜,堆中的每個region都必須有一份卡表,這導致G1的記憶集可能會占用整個堆容量的20%或以上。

?

?

六、內存分配

新建的對象優先在eden區分配

當eden區沒有足夠的空間進行分配時,將觸發一次minor GC(Young GC),

比如針對-Xms20M,-Xmx20M、-Xmn10M的配置,即java堆大小20mb,新生代10mb(eden 8mb,survivor 1mb),老年代10mb,

有三個2mb大小和一個4mb大小的對象,假設先分配了三個2mb大小的對象,當分配4mb的對象時,由于eden區已經占用了6mb,還剩2mb,不足以分配該對象,此時將會發生一次Minor GC,gc期間jvm發現已分配的三個2mb對象仍然存活,無法放入survivor空間內(1mb),只好通過分配擔保機制提前轉移到老年代區,等本次垃圾收集完畢后,

4mb的對象分配在eden區中,老年代占用6mb(有三個2mb的對象)。

?

大對象直接進入老年代

hotspot提供了-XX:PretenureSizeThreshold參數(只針對serial和ParNew這兩款收集器有效,如果是Parallel Scavenge則不支持),指定大于該設置值的對象直接在老年代分配,避免大對象在eden區和survivor區來回復制。

?

長期存活的對象進入老年代

每個對象都有一個對象年齡的計數器,存儲在對象頭中,每發生一次minor GC,新生代中仍存活的對象年齡就會加1,默認達到15后(-XX:MaxTenuringThreshold),會被晉升到老年代里

?

動態對象年齡判定

hotspot并不是永遠要求對象的年齡必須達到-XX:MaxTenuringThreshold后才能晉升到老年代,如果在survivor空間中小于等于某年齡的所有對象大小總和大于survivor空間的一半,則大于等于該年齡的對象就可以直接進入老年代(比如小于等于10歲的對象總大小超過了survivor空間的一半,則10歲或以上年齡的對象就會晉升到老年代)

?

空間分配擔保

在jdk6之前,當發生minor GC前,jvm會先檢查老年代最大可用的連續空間是否大于新生代所有對象的總空間,如果是則進行minor GC,否則,會先查看

-XX:HandlePromotionFailure的參數值是否為true,如果允許則會檢查老年代最大可用的連續空間是否會大于歷次晉升老年代對象的平均大小,如果大于,則嘗試進行

minor GC,如果小于,或-XX:HandlePromotionFailure設置為false,則就進行full GC而不是minor GC。

在jdk6之后,只要老年代的連續空間大于新生代對象總大小或歷次晉升的平均大小,就會進行Minor GC,否則進行Full GC(相當于忽略了-XX:HandlePromotionFailure,默認就是true,不可更改)

?

?

七、虛擬機性能監控、故障處理工具

1、jstat

用于監視jvm各種運行狀態信息的命令行工具。可以顯示本地或遠程jvm進程中的類加載、內存、垃圾收集、即時編譯等運行時數據

jstat [ option pid [interval[s|ms] [count]] ]

interval和count代表查詢間隔和次數,例如每250毫秒查詢一次進程2764的垃圾收集狀況,一共查詢20次,則執行:

jstat –gc 2764 250 20

?

?

2、jmap

用于生成堆轉儲快照(heapdump文件,hprof結尾),也可以查詢java堆和方法區詳細信息,如果空間使用率、當前用的是哪種收集器

?

3、jstack

用于生成jvm當前時刻的線程快照(threaddump),線程快照就是當前jvm內每一條線程正在執行的方法堆棧的集合,線程出現停頓時通過jstack來查看各個線程

的調用堆棧,就可以獲知沒有響應的線程到底在做什么事情或者等待著什么資源

?

?

八、JVM類加載機制

Jvm把描述類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被jvm直接使用的java類型,這個過程被稱為jvm的類加載機制。

類型的加載、連接和初始化都是在程序運行期間完成的

加載-連接-初始化-使用-卸載

?

類的初始化時機:

1、遇到new、getstatic、putstatic或invokestatic這四條字節碼指令時,如果類型沒有進行過初始化,則先觸發初始化。

Java中能生成這四條字節碼指令的場景有:

使用new關鍵字實例化對象時

讀取或設置一個類型的靜態字段時(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)

調用一個類型的靜態方法時

?

2、對類進行反射調用時,如果類沒有初始化,則先進行初始化

?

3、當初始化類時,若父類還未初始化,則先初始化父類

?

4、當jvm啟動時,用戶需要指定一個主類(包含main方法的的類),jvm會先初始化這個類

?

類加載過程:

一、加載

加載是類加載過程中的第一個階段,在這個階段jvm需要完成下面三個事情:

1、通過一個類的全限定名找到定義此類的二進制字節流(可以理解為就是class文件)

2、將這個字節流所代表的的靜態存儲結構轉化為方法區的運行時數據結構(轉換class文件為運行時數據結構)

3、在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口

?

加載階段完成后,class文件就按照jvm所設定的格式存儲在方法區之中了

?

二、驗證

三、準備

為類中定義的靜態變量分配內存并設置類變量初始值的階段(比如int型,設置初始值為0)

四、解析

?

五、初始化

在前面幾個階段里,除了在加載階段用戶可以通過自定義類加載器的方式局部參與外,其余階段都完全由jvm主導,直到初始化階段,

jvm才真正開始執行類中編寫的java程序代碼,將主導權交給應用程序(用戶)。

初始化階段其實就是執行類構造器<clinit>()方法的過程,而<clinit>()方法是javac編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合并生成的,

<clinit>()方法與類的構造函數(即<init>()方法)不同,它不需要顯式地調用父類構造器jvm會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢。如果是接口,

則實現類初始化時不會先執行接口的<clinit>()方法。

(比如,如果父類和子類都有static語句塊,并且都有寫構造器,那么當new一個子類對象時,會先執行父類的static語句塊,再執行子類的static語句塊,再執行父類構造器,最后執行子類構造器)

?

類加載器

在加載階段,實現“通過一個類的全限定名來獲取描述該類的二進制字節流“這個動作即類加載器所要做的

對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確定在jvm中的唯一性。

即使兩個類來源同一個class文件,被同一個jvm加載,只要類加載器不同,則這兩個類必定不同。

1、啟動類加載器(Bootstrap ClassLoader)

2、擴展類加載器(Extension ClassLoader)

3、應用程序類加載器(Application ClassLoader)

?

雙親委派模型

該模型要求除了啟動類加載器外,其余的類加載器都應有自己的父類加載器。如果一個類加載器收到了類加載的請求,它會將該請求委派給父類去完成,只有父類無法完成這個加載請求時(它的搜索范圍中沒有找到這個類),子加載器才會嘗試自己去加載這個類。

?

運行時棧幀結構

1、局部變量表

2、操作數棧

3、動態鏈接

4、方法返回地址

?

?

九、java內存模型

規定所有變量都存儲在主存中,每個線程還有自己的工作內存,線程的工作內存保存了被該線程使用的變量副本,線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,

而不能直接讀寫主存中的數據,不同的線程之間也無法直接訪問對方工作內存中的變量。

?

關于主存與工作內存之間具體是如何交互的,有如下幾個原子操作:

1、lock(使用synchronized時生效):作用于主存中的變量,把一個變量標識為一條線程獨占的狀態

2、unlock(使用synchronized時生效):作用于主存中的變量,把一個處于鎖定狀態的變量釋放出來,釋放變量后才可以被其他線程鎖定

3、read:作用于主存變量,把一個變量值從主存中傳輸到線程的工作內存中

4、load:作用于工作內存的變量,把read操作從主存中得到的變量值放入工作內存的變量副本中

5、use

6、assign

7、store:作用于工作內存的變量,把工作內存中一個變量的值傳送到主存中

8、write:作用于主存的變量,把store操作從工作內存中得到的變量值放入主存的變量中

?

所謂volatile、synchronized等保證可見性、原子性、有序性本質上都是通過上述操作的配合實現的,

另外所謂的可見性、原子性、有序性都是基于多線程并發的場景下,如果是單線程,不必考慮這些。

?

volatile的可見性

當一個變量被定義成volatile之后,它將具備兩項特性:第一是保證可見性,這里的可見性是指當一條線程修改了這個變量的值,會立即同步到主存中,

而別的線程在訪問volatile變量時,必須先從主存中刷新最新的值。

但volatile不保證原子性,體現在當線程A已經從主存中read并load最新的值后,如果此時線程B更改了變量并寫入主存,那線程A中的這個變量值就不是最新的了。

?

?

所以volatile變量的適用場景是運算結果并不依賴變量的當前值。如何理解這句話呢?

比如int a = 0;

如果執行a++操作,那運算結果就依賴于a的當前值,也就是說如果當前a是1,那運算結果就是2,如果當前a是5,那運算結果就是6;

?

而對于boolean b = true;

如果執行set b = false;操作,則運算結果就不依賴與當前b的值,不論b當前是true或false,執行set b=false;的操作后,運算結果就是false

?

同樣對于int c = 0;

如果執行set c = 2; 則運算結果同樣不依賴當前值,那么這種情況也符合volatile的場景

?

volatile的有序性

volatile變量會禁止指令重排序優化,也就是訪問volatile變量之前的代碼不會被排序到volatile變量之后執行,而volatile變量之后的代碼也不會被排到volatile變量之前來執行

原理就是程序在遇到volatile變量時,會自動增加內存屏障,這樣cpu在指令重排序時不能把后面的指令重排序到內存屏障之前的位置(指令重排序無法越過內存屏障)

?

假設T表示一個線程,V和W分別表示兩個volatile變量:

則滿足:

1、在工作內存中,每次使用V前都必須先從主存刷新最新的值,用于保證能看見其他線程對變量V所做的修改

2、在工作內存中,每次修改V后都必須立刻同步會主存中,用于保證其他線程可以看到自己對變量V的修改

3、volatile變量不會被指令重排序優化,從而保證代碼的執行順序和程序的順序相同

?

?

補充:原子性、可見性與有序性

1、原子性

java內存模型提供了lockunlock操作來保證一系列操作集的原子性,這兩個操作對應的字節碼指令是monitorentermonitorexit,這兩個字節碼指令反映到java代碼中就是同步塊-------synchronized關鍵字

?

2、可見性

volatile、synchronized、final可以保證可見性

?

3、有序性

volatile、synchronized保證線程之間操作的有序性

?

?

happens-before原則

時間上的先后順序對線程來說感知上并不一定是有序的。而通過happens-before原則,可以讓線程在感知上也是有序的。

比如針對int value = 0;

public void setValue(int value) {this.value = value;}

?

public int getValue() {return value;}

?

假設線程A先(時間上的先后)調用了setValue(1),然后線程B調用了同一個對象的getValue(),那么線程B的返回值是什么?

答案是不確定,盡管線程A在操作時間上先于線程B,但是無法確定線程B中getValue()方法的返回結果,換句話說,這里面的操作不是線程安全的。

解決方式是加鎖或volatile修飾value

?

綜上所述,那么什么是happens-before原則呢?

先行發生是java內存模型中定義的兩項操作之間的偏序關系,比如操作A先于操作B發生,那么在發生操作B之前,操作A產生的影響能被操作B感知到

這就是所謂的happens-before原則

?

?

?

?

?

?

?

?

?

?

?

?

?

?

總結

以上是生活随笔為你收集整理的java虚拟机学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。