一文帶你學會java的jvm精華知識點
前言
本文分為20多個問題,通過問題的方式,來逐漸理解jvm,由淺及深。希望幫助到大家。
1. Java類實例化時,JVM執行順序?
正確的順序如下:
1父類靜態程式碼塊
2父類靜態變數
3子類靜態程式碼塊
3子類靜態變數
4父類成員變數賦值
5父類構造方式開始執行
6子類成員變數賦值
7子類構造方式開始執行
需要注意的地方是靜態變數和靜態程式碼塊誰在前面誰先執行。
看一段程式碼示例:
package com.jdk.learn;
/** * Created by ricky on 2017/8/25. * * 類載入器載入順序考究 *
* */ public class ClassLoaderTest {
public static void main(String[] args) { son sons=new son(); } }
class parent{ private static int a=1; private static int b; private int c=initc(); static { b=1; System.out.println(“1.父類靜態程式碼塊:賦值b成功”); System.out.println(“1.父類靜態程式碼塊:a的值”+a); } int initc(){ System.out.println(“3.父類成員變數賦值:—> c的值”+c); this.c=12; System.out.println(“3.父類成員變數賦值:—> c的值”+c); return c; } public parent(){ System.out.println(“4.父類構造方式開始執行—> a:”+a+”,b:”+b); System.out.println(“4.父類構造方式開始執行—> c:”+c); } }
class son extends parent{ private static int sa=1; private static int sb; private int sc=initc2(); static { sb=1; System.out.println(“2.子類靜態程式碼塊:賦值sb成功”); System.out.println(“2.子類靜態程式碼塊:sa的值”+sa); } int initc2(){ System.out.println(“5.子類成員變數賦值—>:sc的值”+sc); this.sc=12; return sc; } public son(){ System.out.println(“6.子類構造方式開始執行—> sa:”+sa+”,sb:”+sb); System.out.println(“6.子類構造方式開始執行—> sc:”+sc); } } |
執行結果如下:
1.父類靜態程式碼塊:賦值b成功
1.父類靜態程式碼塊:a的值1
2.子類靜態程式碼塊:賦值sb成功
2.子類靜態程式碼塊:sa的值1
3.父類成員變數賦值:—> c的值0
3.父類成員變數賦值:—> c的值12
4.父類構造方式開始執行—> a:1,b:1
4.父類構造方式開始執行—> c:12
5.子類成員變數賦值—>:sc的值0
6.子類構造方式開始執行—> sa:1,sb:1
6.子類構造方式開始執行—> sc:12
對變數的賦值初始值為0,對於對象來說為null。
2. JVM虛擬機何時結束生命周期?
執行了System.exit()方法;
程式正常執行結束;
程式在執行過程中遇到了異常或錯誤而異常終止;
由於作業系統出現錯誤而導致Java虛擬機進程;
3. jvm類的載入機制?
類的載入機制分為如下三個階段:載入,連接,初始化。其中連接又分為三個小階段:驗證,準備,解析。
載入階段:
將類的.class文件中的二進位數據讀入到記憶體中,將其放在運行時數據區的方法區內,然後再堆內創建一個class對象,用來封裝類在方法區內的數據結構。
載入class文件的方式:
從本地系統中直接載入
通過網路下載.class文件
從zip,jar等歸檔文件中載入.class文件
從專有資料庫中提取.class文件
將Java源文件動態編譯為.class文件
類的載入最終產品是位於堆中的class對象。Class對象封裝了類在方法區內的數據結構,並且向java程式設計師提供了訪問方法區內的數據結構和介面。
類載入並不需要等到某個類被主動使用的時候才載入,jvm規範允許類載入器在預料到某個類要被使用的時候就預先載入。如果預先載入過程中報錯,類載入器必須在首次主動使用的時候才會報錯。如果類一直沒有被使用,就不會報錯。
驗證階段:
此階段驗證的內容如下
類文件的結構檢查:
確保類文件遵從java類文件的固定頭格式,就像平時做文件上傳驗證文件頭一樣。還會驗證文件的主次版本號,確保當前class文件的版本號被當前的jvm兼容。驗證類的位元組流是否完整,根據md5碼進行驗證。
語義檢查:
檢查這個類是否存在父類,父類是否合法,是否存在。
檢查該類是不是final的,是否被繼承了。被final修飾的類是不允許被繼承的。
檢查該類的方法重載是否合法。
檢查類方法翻譯後的位元組碼流是否合法。
引用驗證,驗證當前類使用的其他類和方法是否能夠被順利找到。
準備階段:
通過驗證階段之後,開始給類的靜態變數分配記憶體,設置默認的初始值。類變數的記憶體會被分配到方法區中,實例變數會被分配到堆記憶體中。準備階段的變數會賦予初始值,但是final類型的會被賦予它的值,可以理解為編譯的時候,直接編譯成常量賦給。如果是一個int類型的變數會分配給他4個位元組的記憶體空間,並賦予值為0。如果是long會賦予給8個位元組,並賦予0。
解析階段:
解析階段會把類中的符號引用替換成直接引用。比如Worker類的gotoWork方法會引用car類的run方法。
在work類的二進位數據,包含了一個Car類的run的符號引用,由方法的全名和相關描述符組成。解析階段,java虛擬機會把這個符號引用替換成一個指針,該指針指向car類的run方法在方法區中的記憶體位置,這個指針就是直接引用。
初始化階段:
類的初始化階段就是對壘中所有變數賦予正確的值,靜態變數的賦值和成員變數的賦值都在此完成。初始化的順序參考上方的整理。
初始化有幾點需要注意
如果類還沒有被載入和連接,就先進行載入和連接。如果存在直接的父類,父類沒有被初始化,則先初始化父類。
4. Java類的初始化時機?
類分為主動使用和被動使用。主動使用使類進行初始化,被動使用不會初始化。
主動使用有以下六種情形:
1創建類的實例
2訪問某個類或介面的靜態變數,或者對靜態變數進行賦值
3調用類的靜態方法
4反射
5初始化一個類的子類
6具有main方法的java啟動類
需要注意的是:
初始化一個類的時候,要求他的父類都已經被初始化,此條規則不適用於介面。初始化一個類的時候,不會初始化它所實現的介面,在初始化一個介面的時候,並不會初始化他的父介面。
只有到程式訪問的靜態變數或者靜態方法確實在當前類或當前介面中定義的時候,才可以認為是對類或介面的主動使用。
調用classloader類的loadclass方法載入一個類,不是對類的主動使用,因為loadclass調用的一個子方法具有兩個參數,name和resolve,由於resolve是false。在程式碼中並不會調用resolveClass,所以不會對類進行解析。
被動使用的幾種情況:
(1)通過子類引用父類的靜態欄位,為子類的被動使用,不會導致子類初始化。
class Dfather{ static int count = 1; static{ System.out.println(“Initialize class Dfather”); } }
class Dson extends Dfather{ static{ System.out.println(“Initialize class Dson”); } }
public class Test4 { public static void main(String[] args) { int x = Dson.count; } } |
上面這個例子中,雖然是以Dson.count 形式調用的,但是因為count是Dfather的靜態成員變數,所以只初始化Dfather類,而不初始化Dson類。
(2)通過數組定義類引用類,為類的被動使用,不會觸發此類的初始化。
其實數組已經不是E類型了,E的數組jvm在運行期,會動態生成一個新的類型,新類型為:如果是一維數組,則為:[L+元素的類全名;二維數組,則為[[L+元素的類全名
如果是基礎類型(int/float等),則為[I(int類型)、[F(float類型)等。
class E{ static{ System.out.println(“Initialize class E”); } }
public class Test5 { public static void main(String[] args) { E[] e = new E[10]; } } |
(3)常量在編譯階段會存入調用方法所在類的常量池中。再引用就是直接用常量池中的值了。
class F{ static final int count = 1; static{ System.out.println(“Initialize class F”); } }
public class Test6 { public static void main(String[] args) { int x = F.count; } } |
一種特殊的情況如下:
class F{ static final String s = UUID.randomUUID().toString(); static{ System.out.println(“Initialize class F”); } }
public class Test6 { public static void main(String[] args) { String x = F.s; } } |
則語句 “Initialize class F” 會列印出來,因為UUID.randomUUID().toString()這個方法,是運行期確認的,所以,這不是被動使用。
5. 介紹一下Java的類載入器
類載入器就是用來把類載入到java虛擬機中的一種東西。對於任意的一個類,由他的類載入器和他的類本身確立其在jvm中的唯一性。
jvm內置三大類載入器:
根類載入器又叫bootstrap載入器,該類載入器是最頂層的載入器。負責核心類庫的載入。比如java.lang.*等。載入路徑可以通過sun.boot.class.path指定目錄載入。可以通過參數-Xbootclasspath來指定根載入器的路徑。根類載入器實現依賴於底層系統。正常的路徑在於jre/lib下面。
擴展類載入器又叫ext classloader。用於載入javahome的jre/lib/ext子目錄的jar包。或者從java.ext.dirs的指定路徑載入。如果把用戶的jar放在這個目錄下也會載入。擴展類是純java 的,是classloader的子類。
系統類載入器又叫system classloader。也叫應用類載入器。從環境變數的classpath下面載入類。是classloader的子類。可通過系統屬性的java.class.path進行指定,可通過-classpath指定。平時項目都是通過它載入。
用戶自定義類載入器,用戶可以繼承ClassLoader類,實現其中的findClass方法,來實現自定義的類載入器。
6. 如何實現自定義類載入器?
自定義類載入器必須繼承classloader。需要實現裡面的findClass方法。我們可以傳入路徑,通過二進位輸出流,將路徑內容讀取為二進位數組。通過調用defineClass方法定義class。
7. java類的雙親委派機制
當一個類載入器調用loadClass之後,並不會直接載入。先去類載入器的命名空間中查找這個類是否被載入,如果已經被載入,直接返回。如果沒有被載入。先請求父類載入器載入,父類載入器也沒法載入,就再請求父類,直到根節點,如果找到了就代為載入,放到自己的快取中,沒找到就由自己進行載入,載入不了就報錯。
雙親委派機制的優點是能夠提高軟體系統的安全性,在此機制下,用戶自定義的類載入器不可能載入應該由父類載入器載入的可靠類,從而防止惡意程式碼替代父載入器。
8. java破壞雙親委派機制
可以通過重寫類載入器的loadClass 的方式裡面的邏輯來進行破壞,傳統的是先一層層找。但是破壞的話,改變邏輯,先從自己上面找。
參考java高並發書的 168頁。
9. jvm的類的命名空間
每一個類載入器實例都有各自的命名空間,命名空間是由該類載入器及其所有的父類載入器構成的。在同一個命名空間中,不會出現類的完整名字相同的兩個類,在不同命名空間中,可能出現類的完整名字相同的兩個類。
使用同一個類載入器,載入相同類,二者的引用是一直的,class對象相等。
使用不同類載入器或者同一個類載入器的不同實例,去載入一個class,則會產生多個class對象。
參考java高並發書的170頁。
10. jvm的運行時包
由同一類載入器載入的屬於相同包的類組成了運行時包。運行時包是由類載入器的命名空間和類的全限定名稱共同組成的。這樣做的好處是變用戶自定義的類冒充核心類庫的類,比如java.lang.string類的方法getChar只是包訪問許可權。用於此時偽造了一個java.lang.hackString,用自己的類載入器載入,然後嘗試訪問,這樣是不行的。類載入器不同。
11. JVM載入類的快取機制
每一個類在經過載入之後,在虛擬機中就會有對應的class實例。類C被類載入器CL載入,CL就是C的初始化類載入器。JVM為每一個類載入器維護了一個類列表。該列表記錄了將類載入器作為初始化類載入器的所有class。在載入一個類的時候,類載入器先在這裡面尋找。在類的載入過程中,只要是參與過類的載入的,再起類載入器的列表中都會有這個類。因此,在自定義的類中是可以訪問String類型的。
12. jvm類的卸載
類的最後的生命周期就是卸載。滿足以下三個條件類才會被卸載,從方法區中卸載。
1該類的所有實例已經被gc。
2載入該類的classloader實例被回收。
3該類的class實例沒有在其他地方引用。
13. java是解釋語言還是編譯語言?
是解釋型的。雖然java程式碼需要編譯成.class文件。但是編譯後的.class文件不是二進位的機器可以直接運行的程式,需要通過java虛擬機,進行解釋才能正常運行。解釋一句,執行一句。編譯性的定義是編譯過後,機器可以直接執行。也正是因為.class文件,是的jvm實現跨平台,一次編譯,處處運行。
14. jvm的記憶體區域
jvm的記憶體區域主要分為方法區,堆,虛擬機棧,本地方法棧,程式計數器。
程式計數器:
一塊較小的記憶體區域,是當前執行緒執行位元組碼的行號指示器。每個執行緒都有一個獨立的程式計數器。是執行緒私有的。正是因為程式計數器的存在,多個執行緒來回切換的時候,原來的執行緒才能找到上次執行到哪裡。執行java方法的時候,計數器記錄的是虛擬機位元組碼指令的地址,如果是native方法,則為空。這個記憶體區域不會產生out of memorry的情況。
虛擬機棧:
是描述java方法執行的記憶體模型,每個方法在執行的時候都會創建一個棧幀,用於存儲局部變數表,操作數棧,動態連接,方法出口等。每一個方法從調用到執行完成的過程,對應著一個棧幀在虛擬機中從入棧到出棧的過程。
棧幀用來存儲數據和部分過程結果的數據結構。也被用來處理動態連接,方法返回值,和異常分配。棧幀隨著方法調用而創建,隨著方法結束而銷毀。
本地方法棧:
本地方法棧和虛擬機棧本質一樣,不過是只存儲本地方法,為本地方法服務。
堆記憶體:
創建的對象和數組保存在堆記憶體中,是被執行緒共享的一塊記憶體區域。是垃圾回收的重要區域,是記憶體中最大的一塊區域。存了類的靜態變數和字元常量。
方法區:
又名永久代,用於存儲被jvm載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等數據。hotsoptvm用永久代的方式來實現方法區,這樣垃圾回收器就可以像java堆一樣管理這部分記憶體。
運行時常量池是方法區的一部分。class文件中除了有類的版本,欄位,方法和介面描述等資訊外,還有就是常量池,用於存放編譯期生成的各種字面量和符號引用。
15. JVM的直接記憶體?
直接記憶體並不是虛擬機運行時數據區的一部分,也不是Java 虛擬機規範中定義的記憶體區域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可以使用 native 函數庫直接分配堆外記憶體,然後通過一個存儲在Java堆中的 DirectByteBuffer 對象作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回複製數據。
本機直接記憶體的分配不會受到Java 堆大小的限制,受到本機總記憶體大小限制
直接記憶體也可以由 -XX:MaxDirectMemorySize 指定
直接記憶體申請空間耗費更高的性能
直接記憶體IO讀寫的性能要優於普通的堆記憶體
當我們的需要頻繁訪問大的記憶體而不是申請和釋放空間時,通過使用直接記憶體可以提高性能。
16. JVM堆的內部結構
jvm的堆從gc角度可以分為新生代和老年代。
新生代用來存放新生對象,一般佔據堆的三分之一空間。由於頻繁創建對象。所以新生代會頻繁觸發MinorGC 進行垃圾回收。新生代又分為 Eden 區、ServivorFrom、ServivorTo 三個區。
eden區是java新對象的出生地,如果新對象太大,則直接進入老年代。eden區記憶體不夠的時候,觸發一次minorGc,對新生代進行垃圾回收。
servivorfrom區是上一次gc的倖存者,作為這次gc的被掃描者。
servivorto區保留了一次gc的倖存者。
minorgc採用複製演算法。
minorgc觸發過程:
1:eden、servicorFrom 複製到 ServicorTo,年齡+1
首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域(如果有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果 ServicorTo 不夠位置了就放到老年區);
2:清空 eden、servicorFrom
然後,清空 Eden 和 ServicorFrom 中的對象;
3:ServicorTo 和 ServicorFrom 互換
最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時的 ServicorFrom
區。
老年代
主要存放應用程式中生命周期長的記憶體對象。
老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行
了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。
MajorGC 採用標記清除演算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒
有標記的對象。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生記憶體碎片,為了減少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。
永久代
指記憶體的永久保存區域,主要存放 Class 和 Meta(元數據)的資訊,Class 在被載入的時候被放入永久區域,它和和存放實例的區域不同,GC 不會在主程式運行期對永久區域進行清理。所以這也導致了永久代的區域會隨著載入的 Class 的增多而脹滿,最終拋出 OOM 異常。
元數據區
在Java8 中,永久代已經被移除,被一個稱為「元數據區」(元空間)的區域所取代。元空間
的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地記憶體。因此,默認情況下,元空間的大小僅受本地記憶體限制。類的元數據放入 native
memory, 字元串池和類的靜態變數放入 java 堆中,這樣可以載入多少類的元數據就不再由
MaxPermSize 控制, 而由系統的實際可用空間來控制。
MetaSpace 大小默認沒有限制,一般根據系統記憶體的大小。JVM 會動態改變此值。
可以通過 JVM 參數配置
-XX:MetaspaceSize : 分配給類元數據空間(以位元組計)的初始大小(Oracle 邏輯存儲上的初始高水位,the initial high-water-mark)。此值為估計值,MetaspaceSize 的值設置的過大會延長垃圾回收時間。垃圾回收過後,引起下一次垃圾回收的類元數據空間的大小可能會變大。
-XX:MaxMetaspaceSize :分配給類元數據空間的最大值,超過此值就會觸發Full GC 。此值默認沒有限制,但應取決於系統記憶體的大小,JVM 會動態地改變此值。
17. 為什麼移除永久代?
pergenman space,常發生在jsp中。
1、字元串存在永久代中,容易出現性能問題和記憶體溢出。
2、類及方法的資訊等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
3、永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
4、Oracle 可能會將HotSpot 與 JRockit 合二為一。jrockit中沒有永久代概念。
18. JVM觸發full gc的幾種情況?
System.gc()方法的調用
此方法的調用是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。強烈影響系建議能不使用此方法就別使用,讓虛擬機自己去管理它的記憶體,可通過通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。
老年代空間不足
老年代空間只有在新生代對象轉入及創建為大對象、大數組時才會出現不足的現象,當執行Full GC後空間仍然不足,則拋出如下錯誤:
java.lang.OutOfMemoryError: Java heap space
為避免以上兩種狀況引起的Full GC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。
永生區空間不足
JVM規範中運行時數據區域中的方法區,在HotSpot虛擬機中又被習慣稱為永生代或者永生區,Permanet Generation中存放的為一些class的資訊、常量、靜態變數等數據,當系統中要載入的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會拋出如下錯誤資訊:
java.lang.OutOfMemoryError: PermGen space
為避免Perm Gen佔滿造成Full GC現象,可採用的方法為增大Perm Gen空間或轉為使用CMS GC。
CMS GC時出現promotion failed和concurrent mode failure
對於採用CMS進行老年代GC的程式而言,尤其要注意GC日誌中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。
promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入老年代,而此時老年代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入老年代,而此時老年代空間不足造成的(有時候「空間不足」是CMS GC時當前的浮動垃圾過多導致暫時性的空間不足觸發Full GC)。
對措施為:增大survivor space、老年代空間或調低觸發並發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由於JDK的bug29導致CMS在remark完畢後很久才觸發sweeping動作。對於這種狀況,可通過設置-XX: CMSMaxAbortablePrecleanTime=5(單位為ms)來避免。
統計得到的Minor GC晉陞到舊生代的平均大小大於老年代的剩餘空間
這是一個較為複雜的觸發情況,Hotspot為了避免由於新生代對象晉陞到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉陞到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。
例如程式第一次觸發Minor GC後,有6MB的對象晉陞到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。
當新生代採用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。
除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,默認情況下會一小時執行一次Full GC。可通過在啟動時通過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設置Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。
堆中分配很大的對象
所謂大對象,是指需要大量連續記憶體空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來分配給當前對象,此種情況就會觸發JVM進行Full GC。
為了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用於在「享受」完Full GC服務之後額外免費贈送一個碎片整理的過程,記憶體整理的過程無法並發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的。
19. jvm判斷對象是否可被回收?
引用計數法
在 Java 中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,很顯然一個簡單的辦法是通過引用計數來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關聯的引用,即他們的引用計數都不為 0,則說明對象不太可能再被用到,那麼這個對象就是可回收對象。
引用計數法,判斷不出循環引用的情況。所以沒有採用這種方式。例如
objecta.name = objectb objectb.name = objecta
可達性分析
為了解決引用計數法的循環引用問題,Java 使用了可達性分析的方法。通過一系列的「GC roots」對象作為起點搜索。如果在「GC roots」和一個對象之間沒有可達路徑,則稱該對象是不可達的。
要注意的是,不可達對象不等價於可回收對象,不可達對象變為可回收對象至少要經過兩次標記過程。兩次標記後仍然是可回收對象,則將面臨回收。
可作為gc root的對象有
1.Java虛擬機棧(棧幀的本地變數表)中引用的對象
2.本地方法棧 中 JNI引用對象
3.方法區 中常量、類靜態屬性引用的對象。
20. jvm垃圾回收演算法
標記清除演算法
標記-清除(Mark-Sweep)演算法,是現代垃圾回收演算法的思想基礎。
標記-清除演算法將垃圾回收分為兩個階段:標記階段和清除階段。
一種可行的實現是,在標記階段,首先通過根節點,標記所有從根節點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象(好多資料說標記出要回收的對象,其實明白大概意思就可以了)。然後,在清除階段,清除所有未被標記的對象。
缺點:
1、效率問題,標記和清除兩個過程的效率都不高。
2、空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式運行過程中需要分配較大的對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
標記-整理演算法
標記整理演算法,類似與標記清除演算法,不過它標記完對象後,不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉邊界以外的記憶體。
優點:
1、相對標記清除演算法,解決了記憶體碎片問題。
2、沒有記憶體碎片後,對象創建記憶體分配也更快速了(可以使用TLAB進行分配)。
缺點:
1、效率問題,(同標記清除演算法)標記和整理兩個過程的效率都不高。
複製演算法:
複製演算法,可以解決效率問題,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊記憶體用完了,就將還存活著的對象複製到另一塊上面,然後再把已經使用過的記憶體空間一次清理掉,這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指針,按順序分配記憶體即可(還可使用TLAB進行高效分配記憶體)。
圖的上半部分是未回收前的記憶體區域,圖的下半部分是回收後的記憶體區域。通過圖,我們發現不管回收前還是回收後都有一半的空間未被利用。
優點:
1、效率高,沒有記憶體碎片。
缺點:
1、浪費一半的記憶體空間。
2、複製收集演算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。
分代收集演算法:
當前商業虛擬機都是採用分代收集演算法,它根據對象存活周期的不同將記憶體劃分為幾塊,一般是把 Java 堆分為新生代和老年代,然後根據各個年代的特點採用最適當的收集演算法。
在新生代中,每次垃圾收集都發現有大批對象死去,只有少量存活,就選用複製演算法。
而老年代中,因為對象存活率高,沒有額外空間對它進行分配擔保,就必須使用「標記清理」或者「標記整理」演算法來進行回收。
圖的左半部分是未回收前的記憶體區域,右半部分是回收後的記憶體區域。
對象分配策略:
對象優先在 Eden 區域分配,如果對象過大直接分配到 Old 區域。
長時間存活的對象進入到 Old 區域。
改進自複製演算法
現在的商業虛擬機都採用這種收集演算法來回收新生代,IBM 公司的專門研究表明,新生代中的對象 98% 是「朝生夕死」的,所以並不需要按照 1:1 的比例來劃分記憶體空間,而是將記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor 。當回收時,將 Eden 和 Survivor 中還存活著的對象一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。
HotSpot 虛擬機默認 Eden 和 2 塊 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用記憶體空間為整個新生代容量的 90%(80%+10%),只有 10% 的記憶體會被「浪費」。當然,98% 的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於 10% 的對象存活,當 Survivor 空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。
21. java的引用類型?
強引用
平時使用的new就是強引用,把一個對象賦給一個引用變數。它處於可達狀態的時候,是不會被垃圾回收的。強引用是造成記憶體泄漏的主要原因。
軟引用
軟引用配合softreference使用,當系統中有足夠的記憶體的時候,它不會被回收,當系統中記憶體空間不足的時候會被回收,軟引用存在於對記憶體敏感的程式中。
弱引用
弱引用配合weakreference類來實現。比軟引用的生存期更短,對於弱引用對象來說,只要垃圾回收機制一回收,不管記憶體空間是否充足就直接回收掉了。
虛引用
虛引用需要phantomreference來實現,不能單獨使用,必須配合引用隊列。虛引用主要作用是跟蹤對象的被垃圾回收的狀態。
引用隊列
使用軟引用,弱引用和虛引用的時候都可以關聯這個引用隊列。程式通過判斷引用隊列裡面是不是有這個對象來判斷,對象是否已經被回收了。
軟引用,弱引用和虛引用用來解決oom問題,用來保存圖片的路徑。主要用於快取。
22. JVM垃圾收集器
Java 堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收演算法;
年老代主要使用標記-整理垃圾回收演算法,因此 java 虛擬中針對新生代和年老代分別提供了多種不
同的垃圾收集器,JDK1.6 中 Sun HotSpot 虛擬機的垃圾收集器如下:
serial垃圾收集器(單執行緒、複製演算法)
Serial(英文連續)是最基本垃圾收集器,使用複製演算法,曾經是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一個單執行緒的收集器,它不但只會使用一個 CPU 或一條執行緒去完成垃圾收集工作,並且在進行垃圾收集的同時,必須暫停其他所有的工作執行緒,直到垃圾收集結束。
Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作執行緒,但是它簡單高效,對於限定單個 CPU 環境來說,沒有執行緒交互的開銷,可以獲得最高的單執行緒垃圾收集效率,因此 Serial垃圾收集器依然是 java 虛擬機運行在 Client 模式下默認的新生代垃圾收集器。
ParNew 垃圾收集器(Serial+多執行緒)
ParNew 垃圾收集器其實是 Serial 收集器的多執行緒版本,也使用複製演算法,除了使用多執行緒進行垃圾收集之外,其餘的行為和 Serial 收集器完全一樣,ParNew 垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作執行緒。
ParNew 收集器默認開啟和 CPU 數目相同的執行緒數,可以通過-XX:ParallelGCThreads 參數來限制垃圾收集器的執行緒數。【Parallel:平行的】
ParNew雖然是除了多執行緒外和Serial 收集器幾乎完全一樣,但是ParNew垃圾收集器是很多 java虛擬機運行在 Server 模式下新生代的默認垃圾收集器。
Parallel Scavenge 收集器(多執行緒複製演算法、高效)
Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用複製演算法,也是一個多執行緒的垃圾收集器,它重點關注的是程式達到一個可控制的吞吐量(Thoughput,CPU 用於運行用戶程式碼的時間/CPU 總消耗時間,即吞吐量=運行用戶程式碼時間/(運行用戶程式碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用 CPU 時間,儘快地完成程式的運算任務,主要適用於在後台運算而不需要太多交互的任務。自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個重要區別。
下面為年老代的收集器
Serial Old 收集器(單執行緒標記整理演算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單執行緒的收集器,使用標記-整理演算法,這個收集器也主要是運行在 Client 默認的 java 虛擬機默認的年老代垃圾收集器。 在 Server 模式下,主要有兩個用途:
1. 在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用。
2. 作為年老代中使用 CMS 收集器的後備垃圾收集方案。
Parallel Old 收集器(多執行緒標記整理演算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多執行緒的標記-整理演算法,在 JDK1.6才開始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old 正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
CMS收集器
Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間,和其他年老代使用標記-整理演算法不同,它使用多執行緒的標記-清除演算法。
最短的垃圾收集停頓時間可以為交互比較高的程式提高用戶體驗。
CMS 工作機制相比其他的垃圾收集器來說更複雜,整個過程分為以下 4 個階段:
初始標記
只是標記一下 GC Roots 能直接關聯的對象,速度很快,仍然需要暫停所有的工作執行緒。
並發標記
進行 GC Roots 跟蹤的過程,和用戶執行緒一起工作,不需要暫停工作執行緒。
重新標記
為了修正在並發標記期間,因用戶程式繼續運行而導致標記產生變動的那一部分對象的標記
記錄,仍然需要暫停所有的工作執行緒。
並發清除
清除 GC Roots 不可達對象,和用戶執行緒一起工作,不需要暫停工作執行緒。由於耗時最長的並發標記和並發清除過程中,垃圾收集執行緒可以和用戶現在一起並發工作,所以總體上來看
CMS 收集器的記憶體回收和用戶執行緒是一起並發地執行。
G1收集器
Garbage first 垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與 CMS 收集器,G1 收集器兩個最突出的改進是:
1. 基於標記-整理演算法,不產生記憶體碎片。
2. 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。
G1 收集器避免全區域垃圾收集,它把堆記憶體劃分為大小固定的幾個獨立區域,並且跟蹤這些區域的垃圾收集進度,同時在後台維護一個優先順序列表,每次根據所允許的收集時間,優先回收垃圾最多的區域。區域劃分和優先順序區域回收機制,確保 G1 收集器可以在有限時間獲得最高的垃圾收集效率。
23. jdk7、8、9默認垃圾回收器
jdk1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默認垃圾收集器G1
-XX:+PrintCommandLineFlagsjvm參數可查看默認設置收集器類型
-XX:+PrintGCDetails亦可通過列印的GC日誌的新生代、老年代名稱判斷
24. jdk的命令行有哪些可以監控虛擬機
jhat :虛擬機堆轉儲快照分析工具
jstack :Java 堆棧跟蹤工具
JConsole :Java 監視與管理控制台
25. JVM調優的配置
26. Java的osgi是什麼
OSGi(Open Service Gateway Initiative),是面向 Java 的動態模型系統,是 Java 動態化模組化系統的一系列規範。
動態改變構造
OSGi 服務平台提供在多種網路設備上無需重啟的動態改變構造的功能。為了最小化耦合度和促使這些耦合度可管理,OSGi 技術提供一種面向服務的架構,它能使這些組件動態地發現對方。
模組化編程與熱插拔
OSGi 旨在為實現 Java 程式的模組化編程提供基礎條件,基於 OSGi 的程式很可能可以實現模組級的熱插拔功能,當程式升級更新時,可以只停用、重新安裝然後啟動程式的其中一部分,這對企業級程式開發來說是非常具有誘惑力的特性。
OSGi 描繪了一個很美好的模組化開發目標,而且定義了實現這個目標的所需要服務與架構,同時也有成熟的框架進行實現支援。但並非所有的應用都適合採用 OSGi 作為基礎架構,它在提供強大功能同時,也引入了額外的複雜度,因為它不遵守了類載入的雙親委託模型。
熱部署就是典型的osgi。