1. 程式人生 > 遊戲 >《刺客信條:英靈殿》1/4發光版艾沃爾可換頭雕像 售價7699元

《刺客信條:英靈殿》1/4發光版艾沃爾可換頭雕像 售價7699元

什麼是JVM?

是一個專門執行class位元組碼檔案的作業系統,是用c語言開發的,不同的系統都有對應版本的jvm。專門遮蔽了底層的作業系統、硬體、CPU指令等層面上的細節,讓位元組碼只面對jvm,不用去關注作業系統、硬體的差異。

JVM的組成

jvm由垃圾回收器、類載入器、執行時資料區、執行引擎、本地方法庫組成。

其中執行時資料區是重點,也叫java記憶體部分,其餘還可以關注垃圾回收器和類載入器。

1.類載入器

負責查詢並裝載類的部分稱為類載入子系統,類載入子系統用於定位和載入編譯後的class檔案。

下面是類載入的整個生命週期:

graph LR A[位元組碼] -->B[類載入器]-->C[連結]-->D[初始化]-->E[使用]-->F[解除安裝]

其中連結

這一步又細分為三步:

graph LR A[驗證]-->B[準備]-->C[解析]

類載入器將類載入完畢後會將類的資訊加入到方法區(元空間)

類載入機制

類載入器主要是實現類的載入,將類載入到java記憶體中。

雙親委派機制:

可以用一句話概括:從下而上檢查,至上而下載入。那麼怎麼理解呢?

在瞭解類怎麼載入之前,首先要知道類載入器有很多種:啟動類載入器擴充套件類載入器應用類載入器

啟動類載入器:又叫根載入器/引導類載入器,負責載入JAVA中的一些核心類庫,主要是位於<JAVA_HOME>/lib/rt.jar中。

擴充套件類載入器:主要載入JAVA中的一些拓展類,位於<JAVA_HOME>/lib/ext中,是啟動類載入器的子類。

應用類載入器:又稱為系統類載入器,主要用於載入CLASSPATH路徑下我們自己寫的類,是拓展類載入器的子類。

在知道有多少種類載入器之後,再介紹下類的載入機制,主要有全盤負責父親委託快取機制

在載入類的時候,這裡指的類是自己寫的類,用的是應用類載入器,會先判斷這個類是否被載入過(即判斷該類在快取區是否有此Class),這裡可以看下面這個例子:

public class String {
public static void main(String[] args) {
  System.out.println("1111");
}
}

這裡定義了一個String類,我們都知道String類肯定是存在的,所以這個時候執行的話,類載入器會先判斷這個類是否存在。如果存在,則返回已經存在的類。

那麼類載入器是怎麼判斷這個類是否存在的。當類載入器在載入這個類的時候,會一層一層的往上找(這個過程可以理解為向上委託給父類載入器去完成),就這樣一直到啟動類載入器。如果找到了,比如一直委託到了啟動類載入器,那麼啟動類載入器就會檢查是否能夠載入這個類,如果能載入,則到這一層結束。如果不能,則會通知子類載入器進行載入,一直通知到應用類載入器。----這個就是雙親委派機制(檢查順序從下至上,載入順序從上至下)。

ps:應用類載入器的父親是擴充套件類載入器,擴充套件類載入器的父親是啟動類載入器。

類的裝載是指將.class檔案中的二進位制資料讀到記憶體中,將其放在執行時資料區的方法區內,然後在堆建立一個class物件(也就是常說的物件),這個物件封裝了在方法區內的資料結構,並且向開發者提供了訪問方法區內資料結構的介面。

2.執行時資料區(java記憶體區域)

執行時資料區又叫java的記憶體區域,記憶體區域主要分為兩部分:執行緒私有執行緒公有

其中元空間是執行緒公有的,虛擬機器棧程式計數器本地方法棧是執行緒私有的。

一般而言執行緒公有的區域都會存線上程安全的問題。

程式計數器:

程式計數器主要是用於記錄程式執行的位置和行號,是一塊很小的區域,不會存在記憶體溢位的問題。

虛擬機器棧:

虛擬機器棧又叫方法棧、執行緒棧。虛擬機器棧採用的是棧的資料結構也就是先進後出,後進先出的機制。

在虛擬機器棧中,入口和出口都是同一個口,虛擬機器棧主要是執行方法的。方法的執行也叫壓棧,方法執行結束就叫出棧。

一個方法執行就會建立一個棧幀,棧中可以壓很多的棧幀,一個棧幀對應一個方法。

棧幀分為四個部分:區域性變量表、運算元棧、動態連結、返回地址。

區域性變數

假設現在有一個方法開始執行,也就是開始壓棧,方法中可能會存在區域性變數,那麼這些區域性變數會存在棧幀的區域性變量表中。如果區域性變數是基本資料型別,那麼就會直接存在區域性變量表中,如果是引用資料型別,那麼只會把引用資料型別的地址存在區域性變量表中,具體的引用型別資料則放在堆中,相當於區域性變量表存了引用資料型別的一個指標,指向這個資料的在堆中的位置。

運算元棧

運算元棧指的是在方法中可能存在運算指令,那麼這個運算指令會在運算元棧中進行操作。

動態連結

動態連結指的是在這個方法中可能會存在呼叫其他方法,那麼就需要找到這個被呼叫的方法在哪,方法是放在元空間/方法區中的,方法只有一個,但類可以建立n個物件,所以這個方法可能會被呼叫n次,所以動態連結存的是被呼叫方法的一個記憶體地址,指向的是存在於元空間的方法。

返回地址

返回就比如返回方法的執行情況,比如成功或者失敗或者異常之類的。

虛擬機器棧可能會出現的問題

虛擬機器棧溢位:

棧的深度大於虛擬機器所允許的深度會出現StackOverflowError

常見於遞迴呼叫。

當虛擬機器棧沒有空間且無法申請更多的空間的時候會出現OutOfMemoryError

一般多發生於多執行緒條件下,建立過多的執行緒且每個執行緒執行時間又比較長,這時可能會出現棧的記憶體溢位。

棧可以通過-Xss1M進行大小設定

本地方法棧

本地方法棧是為本地方法服務的,和虛擬機器棧一樣,當執行本地方法的時候,一樣會進行壓棧操作,這時會把棧幀壓到本地方法棧而不是虛擬機器棧,本地方法都是用native關鍵字進行標記。

方法區/元空間

方法區/元空間是執行緒共享的,用於儲存類的資訊、常量、執行時常量池、靜態變數、即時編譯器編譯後的程式碼等。在java虛擬機器規範中描述的方法區是堆的一個邏輯部分,也就是說方法區在邏輯上是屬於堆的一部分,但實際上為了和堆做區分,方法區也被稱為非堆。在HotSpot虛擬機器中,使用永久代來作為方法區的落地實現,但是因為一些問題,後面逐漸使用本地記憶體來作為方法區的落地實現。

ps:在1.7及其之前,方法區在hotspot中都是永久代,直到1.8版本,永久代變成了使用本地記憶體的元空間。在1.7版本的時候,將字串常量池從方法區中去除,放在了堆中。

1.7及其之前的版本可以通過-XX:MaxPermSize來設定最大值。

1.8開始通過-XX:MaxMetaspaceSize=48m來進行設定。

空間不夠分配時會出現OutOfMemoryError的問題。

比如:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:-UseCompressedClassPointers 
-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

第一個引數用於列印GC日誌;

第二個引數用於列印對應的時間戳;

第三個引數-XX:-UseCompressedClassPointer表示在Metaspace中不要開闢出一塊新的空間(Compressed Class Space),如果開闢這塊空間的話,該空間預設大小是1G,所以我們關閉該功能。最後再設定元空間的值為20M,最大值為20m

元空間溢位

元空間有預設的大小,一旦元空間存放class的相關資訊,如類名、訪問修飾符、常量池、欄位描述、方法描述等或者通過反射大量生成動態類填充該區域超出大小範圍即會發生記憶體溢位;

1.7之前的元空間溢位:

OutOfMemoryError:PermGen space;

1.7及1.8的元空間溢位

OutOfMemoryError:Metaspace;

1.8元空間設定大小

-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

堆是執行緒共享的,是java記憶體中最大的區域,只要虛擬機器啟動就會建立堆,用於存放所有的例項物件或者陣列,是垃圾回收器主要管理的區域,堆可劃分為新生代和老年代。

堆可以通過-Xmx-Xms來調節堆的大小。

堆會出現OutOfMemoryError:Java heap space

新生代可細分為:伊甸區、from區和to區

新生代是類誕生,成長甚至死亡的地方。(活的足夠長的類,可能會進入老年區,但是大部分在新生區就玩完了),所有的物件一開始都是在伊甸園區new出來的。

新生區分為三部分:Eden區,新生0區,新生1區或倖存者區(0,1)或者from區和to區。按照8:1:1進行分配。

​ 新生區會引發輕gc(minorgc),養老區會引發重gc(fullgc),垃圾回收主要發生在新生區和養老區,新生區到養老區觸發fullgc

新生代佔整個堆的1/3,老年代佔2/3。其中伊甸區和from區、to區又按照8:1:1的比例分配堆的1/3。

概念:

記憶體溢位:

​ GC Roots到物件之間無可達路徑,可以被收集,但物件還存活著(有可能是垃圾回收器還來不及回收),這個可以通過調節引數來分析物件的生命週期是否過長,物件是否持有狀態時間過長。

記憶體洩漏:

​ GC Roots到物件之間有可達路徑而無法收集

可達路徑:

​ 假設A物件持有B物件,B物件持有C物件。這時C物件和B物件可能已經不用了,但A物件還在用,而又由於A持有B,B持有C,所以A的不能回收導致B和C都不能回收。這就叫可達路徑,其中A可以被稱為GC Roots

3.JVM垃圾回收

垃圾回收器主要關注執行緒共享的部分:堆和方法區/元空間。

哪些需要回收?什麼時候回收?如何回收?

堆建立物件的過程:

當通過 new 建立物件時,首先檢查這個new指令的引數是否能在元空間中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被載入、解析和初始化過,如果沒有,執行相應的類載入;

類載入檢查通過之後,為新物件分配記憶體(記憶體大小在類載入完成後便可確定),在堆的空閒記憶體中劃分一塊區域(‘指標碰撞’或‘空閒列表’的分配方式);

為物件分配記憶體空間相當於把一塊確定大小的記憶體從堆中劃分出來,分配記憶體的方式有兩種,使用哪一種方式取決於堆是否規整,而堆是否規整是由堆採用的垃圾收集器是否帶有壓縮整理的功能。SerialParNew垃圾收集器是帶有壓縮整理功能的,CMS垃圾收集器是不帶有壓縮整理功能的。

帶有壓縮整理功能的收集器的堆是規整的,會把用過的記憶體放在一邊,沒用過的記憶體放在另一邊,中間放一個指標作為分介面的指示器,那麼分配記憶體就是把指標往空間的那一邊移動和物件大小相同的距離,這種分配方式稱為指標碰撞

不帶有壓縮整理功能的收集器的堆是不規整的,已使用的記憶體和使用的記憶體相互交錯,虛擬機器會有一個列表,上面記錄的是哪些記憶體是空間的,在分配的時候會從列表中找到合適的空間劃分給物件,並且更新列表上的記錄,這種分配方式稱為空閒列表

由於堆中分配記憶體非常頻繁,為了避免多個執行緒同時分配堆記憶體時的衝突,虛擬機器採用CAS和失敗重試方式保證操作的執行緒安全,同時虛擬機器還有另一套設計就是把每個執行緒分配堆記憶體的動作隔離開,即每個執行緒預先在堆中分配一塊記憶體,稱為執行緒分配緩衝(TLAB->Thread Local Allocation Buffer),執行緒先在自己的TLAB上分配,分配完了再CAS同步,這樣可以很大程度避免在併發情況下頻繁建立物件造成的執行緒不安全;

記憶體空間分配完成後會初始化為 0(不包括物件頭),接下來就是填充物件頭,把物件是哪個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的 GC 分代年齡等資訊存入物件頭。

執行 new 指令後執行 init 方法後才算一份真正可用的物件建立完成;

物件在記憶體中的佈局:

在虛擬機器中,物件在記憶體中分為 3 塊區域:物件頭(Header)、例項資料(Instance Data) 和 對齊填充(Padding)。

  • 物件頭

包含兩部分,第一部分用於儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等,32 位虛擬機器佔 32 bit,64 位虛擬機器佔 64 bit。官方稱為 ‘Mark Word’;

第二部分是型別指標,即物件指向它的類的元資料指標,虛擬機器通過這個指標確定這個物件是哪個類的例項,另外,如果是 Java 陣列,物件頭中還必須有一塊用於記錄陣列長度的資料,因為普通物件可以通過 Java 物件元資料確定大小,而陣列物件不可以;

  • 例項資料

程式程式碼中所定義的各種成員變數型別的欄位內容(包含父類繼承下來的和子類中定義的);

  • 對齊填充

不是必然需要,主要是佔位,保證物件大小是某個位元組的整數倍;

如何判斷可以回收--可達性路徑分析:

jvm通過可達性分析演算法來判斷是否可以回收,之前說過可達性就相當於持有物件的引用,持有方只要還存在,那被持有方一樣是不能回收的。

機制是通過一個物件(這個物件被稱為GC Root)為根節點,以這個根節點為起始點開始搜尋,看這個物件和其他物件有沒有可達路徑,如果沒有則表示該物件是不可用的,可以被回收。

那麼哪些物件可以作為GC Root:

  1. 虛擬機器棧本地變量表中引用型別所引用的物件;

  2. 方法區/元空間中類的靜態變數所引用的物件;

  3. 方法區/元空間中類的常量所引用的物件;

  4. 本地方法棧中Native方法裡所引用的物件;

jvm主要是通過引用來分析是否有可達路徑的。

主要有四種引用:

  1. 強引用

    強引用是最普遍的引用,比如 User user = new User(); 這是強引用,垃圾收集器不會回收強引用;

  2. 軟引用

    軟引用是表示一些物件還有用,但是也不是必須要用的,比如像快取物件就可以採用軟引用,在系統記憶體不足時,這些軟引用是可以被垃圾回收器回收的,如果我們要使用軟引用的話,編碼的時候,把物件採用jdk中提供的SoftReference型別來包裝;

  3. 弱引用

    弱引用也是表示一些不是必須的物件,但它比軟引用更弱,被弱引用所引用的物件只能生存到下一次垃圾收集之前,當開始垃圾收集時,無論記憶體是否足夠,都會回收弱引用物件;

  4. 虛引用

    虛引用PhantomReference是一種特殊的引用,也是最弱的引用關係,用來實現Object.finalize功能,在開發中很少使用;

方法區/元空間的垃圾回收

在JVM的垃圾回收中,堆記憶體是回收最頻繁也是最多的,方法區/元空間的垃圾收集效率非常低,所以JVM規範中並沒有要求一定要回收方法區/元空間,如果方法區/元空間有無用的類資訊、常量池,JVM不是必須要回收的;

Hotspot 虛擬機器預設會進行類的解除安裝,如果不想要對無用的類進行解除安裝,可以加上引數-Xnoclassgc(不解除安裝),預設情況下Hotspot 虛擬機器會解除安裝類;

類的載入和解除安裝引數:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -verbose:class -XX:+TraceClassLoading -XX:+TraceClassUnloading

方法區/元空間垃圾回收主要兩部分內容:廢棄的常量和無用的類;

判斷廢棄常量:一般是判斷沒有任何物件引用該常量;

判斷無用的類:要滿足以下三個條件

(1)該類所有的例項都已經回收,也就是 Java 堆中不存在該類的任何例項;

(2)載入該類的 ClassLoader 已經被回收;

(3)該類對應的 java.lang.Class 物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法;

JVM回收物件的兩次標記過程

1、第一次標記

如果物件進行可達性分析演算法之後沒有發現與GC Roots相連的引用鏈,那它將會第一次標記並且進行一次篩選;

篩選條件:判斷此物件是否有必要執行finalize()方法;

篩選結果:當物件沒有覆蓋finalize()方法或者finalize()方法已經被JVM執行過,則判定為可回收物件,如果物件有必要執行finalize()方法,則被放入F-Queue佇列中,稍後在JVM自動建立低優先順序的Finalizer執行緒(可能多個執行緒)中觸發這個方法;  

回收之前會呼叫finalize()方法,該方法是用於銷燬的,可以重寫這個方法,如果物件沒有重寫該方法或者已經執行過這個方法,則該物件被標記為可回收物件,並放入F-Queue佇列中。

2、第二次標記

GC對F-Queue佇列中的物件進行二次標記;

如果物件在finalize()方法中重新與引用鏈上的任何一個物件建立了關聯,那麼第二次標記時則會將它移出“即將回收”集合,如果此時物件還沒成功逃脫,那麼只能被回收了;

注:finalize() 方法

finalize()是Object類的一個空方法、該方法是被垃圾收集器所呼叫,一個物件的finalize()方法只會被垃圾收集器自動呼叫一次,經過finalize()方法逃脫死亡的物件,第二次不會再呼叫;

不提倡在程式中呼叫finalize()來進行物件的自救,因為該方法執行的時間不確定,甚至是否被執行也不確定(Java程式的不正常退出),無法保證各個物件的呼叫順序(甚至有不同執行緒中呼叫)。

第二次標記會走一遍第一次的過程,第二次允許退回第一次的操作,也就是如果在F-Queue佇列中的物件被重新引用了,那麼就會被移除“即將回收”的集合。

垃圾回收演算法
  • 複製演算法

    將可用記憶體按容量分為大小相等的兩塊,每次只使用其中一塊,當這一塊的記憶體用完了,就將還存活的物件複製到另外一塊記憶體上,然後再把已使用過的記憶體空間一次清理掉;

    優點:實現簡單,效率高,解決了標記-清除演算法導致的記憶體碎片問題;

    缺點:代價太大,將可分配記憶體縮小了一半,效率隨物件的存活率升高而降低。

    一般虛擬機器都會採用該演算法來回收新生代;

  • 標記-清除演算法

    分為‘ 標記 ’和‘ 清除 ’兩個階段:

    標記

    標記出所有需要回收的物件,這裡面會經歷兩次標記(可達路徑分析),一次標記和二次標記,經過兩次標記的物件就可以判定可以回收了;

    清除

    兩次標記後,對還在“ 即將回收 ”集合的物件進行回收;

    優點:基於最基礎的可達性分析演算法,實現簡單,後續的收集演算法都是基於這種思想實現的;

    缺點:標記和清除效率不高,產生大量不連續的記憶體碎片,導致建立大物件時找不到連續的空間,不得不提前觸發另一次的垃圾回收;

  • 標記-整理演算法

    標記-整理演算法是根據老年代的特點而產生的;

    1 標記

    標記過程與上面的標記-清理演算法一致,也是基於可達性分析演算法,也是兩次標記;

    2 整理

    和標記-清理不同的是,該演算法不是針對可回收物件進行清理,而是根據存活物件進行整理。讓存活物件都向一端移動,然後直接清理掉邊界以外的記憶體;

    優點:不會像複製演算法那樣劃分兩個區域,提高了空間利用率,不會產生不連續的記憶體碎片;

    缺點:效率問題,除了像標記-清除演算法的標記過程外,還多了一步整理過程,效率變低;

  • 分代收集演算法

    現在一般虛擬機器的垃圾收集都是採用“ 分代收集 ”演算法;

    根據物件存活週期的不同將記憶體劃分為幾塊,一般把java堆分為新生代和老年代,JVM根據各個年代的特點採用不同的收集演算法;

    新生代中,每次進行垃圾回收都會發現大量物件死去,只有少量存活,因此採用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集;

    老年代中,因為物件存活率較高,採用標記-清理、標記-整理演算法來進行回收;

垃圾收集器

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

Serial

新生代收集器,最早的收集器,單執行緒的,收集時需暫停使用者執行緒的工作。所以有卡頓現象,效率不高,致使java語言的開發團隊一直在改進垃圾收集器的演算法和實現。但Serial收集器簡單,不會有執行緒互動的開銷,是client模式下預設的垃圾收集器。

-client, -server;

jdk1.8之後就預設是-server模式,在這之前是-client模式,可以通過java -version來檢視使用的是哪種模式。-server比-client效率要高。

引數: -XX:+UseSerialGC

ParNew

多執行緒版的Serial。

它是新生代收集器,就是Serial收集器的多執行緒版本,大部分基本一樣,配置引數也一致,單CPU下,ParNew還需要切換執行緒,可能還不如Serial。

Serial和ParNew收集器可以配合CMS收集器,前者收集新生代,後者CMS收集老年代。

"-XX:+UseConcMarkSweepGC":指定使用CMS後,會預設使用ParNew作為新生代收集器;
"-XX:+UseParNewGC":強制指定使用ParNew;
"-XX:ParallelGCThreads=2":指定垃圾收集的執行緒數量,ParNew預設開啟的收集執行緒與CPU的數量相同;

GC日誌常用引數

-XX:+PrintGC

允許在每個GC上列印訊息。預設情況下,此選項處於禁用狀態。

-XX:+PrintGCApplicationConcurrentTime

啟用列印自上次暫停(例如GC暫停)以來經過的時間。預設情況下,此選項處於禁用狀態。

-XX:+PrintGCApplicationStoppedTime

允許列印暫停(例如GC暫停)持續的時間。預設情況下,此選項處於禁用狀態。

-XX:+PrintGCDateStamps

啟用在每個GC上列印日期戳。預設情況下,此選項處於禁用狀態。

-XX:+PrintGCDetails

允許在每個GC上列印詳細訊息。預設情況下,此選項處於禁用狀態。

-XX:+PrintGCTaskTimeStamps

啟用為每個GC工作執行緒任務列印時間戳。預設情況下,此選項處於禁用狀態。

-XX:+PrintGCTimeStamps

啟用在每個GC上列印時間戳。預設情況下,此選項處於禁用狀態。

-Xloggc:filename

設定要將詳細GC事件資訊重定向到其中進行日誌記錄的檔案。寫入此檔案的資訊類似於-verbose:gc的輸出,其中包含自每個記錄的事件之前的第一個gc事件以來經過的時間。-Xloggc選項重寫-verbose:gc,如果這兩個選項都是用同一個java命令給出的。

-XX:+HeapDumpOnOutOfMemoryError

啟用在引發Java.lang.OutOfMemoryError異常時使用堆探查器(HPROF)將Java堆轉儲到當前目錄中的檔案。可以使用-XX:heap dump path選項顯式設定堆轉儲檔案的路徑和名稱。預設情況下,此選項被禁用,並且在引發OutOfMemoryError異常時不會轉儲堆。

-XX:HeapDumpPath=path

設定在設定-XX:+HeapDumpOnOutOfMemoryError選項時用於寫入堆分析器(HPROF)提供的堆轉儲的路徑和檔名。預設情況下,檔案是在當前工作目錄中建立的,名為java_pidpid.hprof,其中pid是導致錯誤的程序的識別符號。

例子:

-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:d:/jvm/jvmgc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/jvm/heapdump.hprof