哈爾濱企業(yè)做網(wǎng)站今日新聞頭條10條
1.JVM內(nèi)存區(qū)域
一個運行起來的java進程就是一個Java虛擬機,就需要從操作系統(tǒng)中申請一大塊內(nèi)存。
內(nèi)存中會根據(jù)作用的不同被劃分成不同的區(qū)域:
(1)棧:存儲的內(nèi)容是代碼在執(zhí)行過程中,方法之間的調(diào)用關(guān)系(棧中每一個元素就是一個棧幀,代表一個方法的調(diào)用,包含方法的形參,返回值,局部變量)。
(2)堆:這里存儲的內(nèi)容是代碼中new的對象
(3)方法區(qū)(1.8開始:元數(shù)據(jù)區(qū)):存儲類對象(.class文件加載到內(nèi)存就成為了類對象)
(4)程序計數(shù)器:存放每個線程,下一條指令執(zhí)行的地址??臻g較小主要就是用來存放一個“地址”,表示下一條要執(zhí)行的指令,在內(nèi)存的哪個地方(在方法區(qū)中去找下一條指令,指令是以二進制的形式存儲在類對象中)。
程序計數(shù)器中的值會隨著指令的執(zhí)行,從而自動進行更新,去指向下一條指令。
如果是順序執(zhí)行的代碼,那么下一條執(zhí)行的指令地址就是把指令地址進行遞增。
如果是條件循環(huán)執(zhí)行的代碼,那么下一條執(zhí)行的指令地址就可能會跳到比較遠的地址。
ps:棧和程序計數(shù)器每一個java線程一份,而堆和方法區(qū)是每一個java進程一份。(僅限java,放在c++中就不一定是這樣了)
2.JVM類加載
(1)基本流程:
java代碼經(jīng)過編譯后生成.class文件,java程序要想運行起來就需要將jvm讀取這個.class文件,并且把里面的內(nèi)容構(gòu)造成類對象,保存到內(nèi)存的方法區(qū)中。
官方文檔中把類加載的過程分成了5個步驟:
1)加載:找到.class文件,并且打開文件,讀取文件中的內(nèi)容(通過全限定名稱查找)
2)驗證:檢查當(dāng)前二進制文件是否符合格式的要求,具體格式
3)準備:給類對象分配內(nèi)存空間,并且為不被final修飾的static變量賦初始默認值(被final修飾的變量在此時會被賦值成程序員給定的值)
4)解析:針對類對象中字符串常量的處理,進行了一些初始化的操作。(符號引用到直接引用)
字符串常量在經(jīng)過編譯之后,eg:final String s = "hello",s本來是存儲內(nèi)存地址,但在.class文件中會重新針對該字符串進行創(chuàng)建出一個引用,此時這個引用就不是存儲內(nèi)存地址了,而是存儲文件偏移量(字符串長度),通過這個變量就可以知道目前字符串處于哪個位置。到解析的階段(類加載的時候),會重新將這個引用替換成內(nèi)存地址。
5)初始化:對類對象中的各種屬性進行初始化,還需執(zhí)行靜態(tài)代碼塊和加載一下父類(子類中調(diào)用構(gòu)造方法,會先幫助父類進行構(gòu)造)
(2)雙親委派模型:
是在加載過程中的第一個步驟。
首先需要了解一下類加載器:
是JVM中的一個模塊,JVM內(nèi)置了三個類加載器:
1)BootStrap ClassLoader
2)Extension?ClassLoader
3)Application?ClassLoader
這三個類加載器之間的關(guān)系就好似爺父子之間的關(guān)系,但不是靠繼承構(gòu)成的,而是由類加載中的一個屬性(parent)來指向父類加載器。
加載的具體流程是:
①根據(jù)給定的全限定類名,找到對應(yīng)的.class文件。
②從Application?ClassLoader作為入口,開始執(zhí)行查找的邏輯。
③Application?ClassLoader不會立即掃描自己所負責(zé)的目錄(負責(zé)搜索項目當(dāng)前目錄和第三方庫的對應(yīng)目錄),而是會交給自己的父類加載器Extension?ClassLoader。
④Extension?ClassLoader不會立即掃描自己所負責(zé)的目錄(負責(zé)搜索JDK中一些擴展庫對應(yīng)的目錄,是JDK標準庫之外的一些庫,屬于對JDK標準庫的擴展),而是會交給自己的父類加載器BootStrap?ClassLoader。
⑤BootStrap?ClassLoader,也不會立即掃描自己所負責(zé)的目錄(負責(zé)搜索JDK標準庫對應(yīng)目錄),而是會交給自己的父類加載器,但是卻發(fā)現(xiàn)沒有父類加載器,因此只能區(qū)掃描自己負責(zé)的目錄,一旦在標準庫中查找到對應(yīng)類的.class文件,此時加載的過程就完了,掃描結(jié)束過后如果沒有掃描到,就會交給它的子類加載器(Extension?ClassLoader)掃描。
⑥如果在Extension?ClassLoader掃描到了就結(jié)束加載過程,沒有掃描到就交給它的子類加載器(Application?ClassLoader)掃描。
⑦如果在Application?ClassLoader掃描到了就結(jié)束加載過程,沒有掃描到此時應(yīng)該交給它的子類加載器掃描,但是發(fā)現(xiàn)沒有了,所以此時會拋出一個異常ClassNotFoundException
總結(jié):所謂的雙親委派模型其實就是一個按照優(yōu)先級查找.class文件的一個過程,之所以有這么一套流程,是為了確保JDK標準庫掃描的優(yōu)先級最高,其次是擴展庫,最后才是項目當(dāng)前目錄和引入的第三方庫對應(yīng)目錄。就好比:你在自己的代碼中使用String(導(dǎo)入的包是java.lang.String),在類加載的時候加載的JDK標準庫中的類,而不是自己寫的這個類。
?3.JVM垃圾回收機制(GC垃圾回收)
在java中new對象,是動態(tài)申請內(nèi)存(運行時分配),如果一個資源申請了內(nèi)存空間,長時間不使用但是不釋放,就可能會造成內(nèi)存泄漏。
在java中給出了一個方案,也就是垃圾回收機制,讓JVM,自動判定某個內(nèi)存是否不再使用了,
如果后面這個內(nèi)存確實不用了,JVM就會自動回收把這個內(nèi)存給回收掉了,此時就不需要手動回收了。
GC是垃圾回收,GC回收的目標是內(nèi)存中的對象。對于java來說就是釋放堆上的new出的對象,棧上的對象是隨著棧幀的生命周期(方法執(zhí)行結(jié)束,棧幀自然銷毀,內(nèi)存自然釋放),靜態(tài)變量,生命周期是整個程序,這個始終存在就意味著靜態(tài)變量無需釋放的,真正釋放的就是堆上的對象。
GC回收垃圾的過程主要有兩個步驟:
(1)找到垃圾
有兩種主流的方案:
①引用計數(shù)
new出來的對象,會單獨劃分空間,來保存有一個計數(shù)器,計的是當(dāng)前有多少引用指向該對象。
如果一個對象沒有引用指向了(即引用計數(shù)為0),就可以將該對象視為垃圾了。
但是使用該種方式可能會存在兩種問題:
·比較浪費內(nèi)存:
計數(shù)器怎么說也得有兩個字節(jié),如果對象本身比較小,那么此時這個計數(shù)器所占空間比例就比較大了。
·存在循環(huán)引用問題:
②可達性分析:
有一組線程,周期性掃描代碼中的所有對象,把所有可以訪問到的對象,都給標記成“可達”,反之,如果經(jīng)過掃描后,不可達的對象,就成垃圾了,需要被回收。
eg:
當(dāng)然這里的遍歷不一定是二叉搜索樹,這里可達的實現(xiàn)大概率是靠N叉搜索樹實現(xiàn),這一步就是針對當(dāng)前對象,看對象中有多少不同引用類型的成員,然后再對每一個類型的成員進行進一步的遍歷。
通過上述過程,不難看出可達性分析是比較耗費資源的(開銷較大)。?
(2)回收垃圾
有三種基本的回收思路:
①標記刪除:
掃描到一個不可達的對象,就直接進行釋放,這個方案非常不好,那就是會產(chǎn)生很多的內(nèi)存碎片。釋放代碼,是為了讓其他代碼能夠申請一塊連續(xù)的內(nèi)存空間,隨著時間的推移,內(nèi)存碎片的情況就會愈演愈烈,就會導(dǎo)致后續(xù)申請連續(xù)內(nèi)存空間變得困難。
②復(fù)制算法:
通過復(fù)制的方式,把有效的對象,歸類到一起,在同一釋放剩下的對象。
也就是把內(nèi)存一分為二,一次只用其中一半。但這種方式有兩個明顯的問題:
a.內(nèi)存利用率不高
b.如果有效的對象很多,那么復(fù)制的開銷將會很大
③標記整理:
既能解決內(nèi)存碎片的問題,又能夠解決上述內(nèi)存利用率不高的問題。
eg:
類似于順序表刪除元素的搬運操作,使用該種方式搬運開銷仍然很大。
而JVM中GC垃圾回收主要思想是分代回收(具體實現(xiàn)可能有一些調(diào)整和優(yōu)化),是對上述思路的集合,讓不同的方案,揚長避短。
分代回收有一個很重要機制就是:對象能活過的GC掃描輪次越多,就是越老(代表當(dāng)前對象是暫時不會被釋放的)
eg:下圖是整個分代回收的全部過程: