JVM詳細介紹

 

JVM工做原理

JVM 主要由 ClassLoader 和 執行引擎 兩子系統組成.html

任何一個Java類的main方法運行都會建立一個JVM實例, 當main函數結束時, JVM實例也就結束了. JVM實例啓動時默認啓動幾個守護線程, 好比: 垃圾回收的線程, 而 main 方法的執行是在一個單獨的非守護線程中執行的.只要母線程結束, 子線程就自動銷燬, 只要非守護main 線程結束JVM實例就銷燬了.java

JVM的工做原理以下:git

  1. 根據系統環境變量, 建立裝載JVM的環境與配置;
  2. 尋找JRE目錄, 尋找jvm.dll, 並裝載jvm.dll;
  3. 根據JVM的參數配置, 如: 內存參數, 初始化jvm實例;
  4. JVM實例產生一個引導類加載器實例(Bootstrap Loader), 加載Java核心庫, 而後引導類加載器自動加載擴展類加載器(Extended Loader),加載Java擴展庫, 最後擴展類加載器自動加載系統類加載器(AppClass Loader), 加載當前的Java類;
  5. 當前Java類加載至內存後, 會通過驗證、準備、解析三步, 將Java類中的類型信息、屬性信息、常量池存放在方法區內存中, 方法指令直接保存到棧內存中, 如: main函數;
  6. 執行引擎開始執行棧內存中指令, 因爲main函數是靜態方法, 因此不須要傳入實例, 在類加載完畢以後, 直接執行main方法指令;
  7. main函數執行主線程結束, 隨之守護線程銷燬, 最後JVM實例被銷燬;

類加載

類生命週期 github

類: 須要由加載它的類加載器和這個類自己共同保證其在JVM中的惟一性算法

加載編程

經過類的全路徑名獲取類的二進制字節流,將類的靜態內容和對象信息加載進方法區,在堆中建立對象,做爲方法區數據的訪問入口.數組

具體是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個java.lang.Class對象,做爲這個類封裝在方法區內的數據結構的入口.安全

類的加載最終是在堆區內的Class對象,Class對象封裝了類在方法區內的數據結構,而且向開發者提供了訪問方法區內的數據結構的接口.數據結構

怎樣加載一個類:多線程

  1. 命令行啓動應用時候由JVM初始化加載
  2. 經過Class.forName()方法動態加載
  3. 經過ClassLoader.loadClass()方式動態加載,如ClassLoader.getSystemClassLoader().loadClass("org.luvx.User")

一個類被加載,當且僅當其某個靜態成員(靜態方法等、構造器)被調用時發生,加載一個類時,其內部類不會同時被加載。

驗證

檢查Class文件數據的正確性,是否符合當前虛擬機的要求 ,是否會危害JVM的安全等,是類加載過程當中最複雜耗時的過程.

細分爲如下過程:

  • 文件格式驗證
  • 元數據驗證
  • 字節碼驗證
  • 符號引用驗證

準備

正式爲類的靜態內容分配內存並設置變量初始值

  1. 進行內存分配的僅是靜態變量,不包括對象變量,對象變量在對象實例化時隨着對象分配在堆內存中.
  2. 設置初始值並非是什麼就是什麼,而是設置對應類型的初始值,如定義static int num = 12,此時設置爲0,12是在上圖初始化階段設置,但static final修飾除外,直接就是12

解析

將常量池中的符號引用替換爲直接引用,主要針對類或接口、字段、類方法、接口方法四類符號引用進行

符號引用不必定要已經加載到內存,而直接引用一定存在於存中 關於符號引用, 查下如下代碼的字節碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 符號引用
public class Test {
   public static void main() {
     String s="adc";
     System.out.println("s=" + s);
   }
}
// 直接�引用
public class Test {
   public static void main() {
     System.out.println("s=" + "abc");
   }
}

初始化

類加載的最後階段,對靜態內容進行初始化操做

Java類初始化順序:

父類靜態變量->父類靜態代碼塊->子類靜態代碼塊->父類非靜態變量->父類非靜態代碼塊->父類構造函數->子類非靜態變量->子類非靜態代碼塊->子類構造函數

不會加載類的情形:

  1. 經過子類使用父類的靜態字段,不會加載子類
  2. 定義對象數組
  3. 使用類名獲取Class對象
  4. 使用Class.forName()加載類時,指定參數initializefalse
  5. 使用ClassLoaderloadClass()方法加載類

類加載

Java中的類都是在程序運行期間加載的,雖然會下降性能,但這種動態加載機制增長了靈活性,如面向接口編程中,只有運行時才能知道具體的類,能夠自定義類加載器,動態加載指定的二進制數據建立對象.

類加載的時機

JVM規範中並無約束類加載時機,但約束了5種狀況需對類進行初始化操做,其以前的操做天然就須要完成.

  1. 遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時(new對象時,調用靜態方法,讀/寫靜態變量,final修飾的除外)
  2. java.lang.reflect包的方法對類進行反射調用
  3. 初始化一個類,其父類仍沒有初始化,就須要初始化父類
  4. jvm啓動,初始化含有main()方法的類
  5. Java7的動態語言支持下,java.lang.invoke.MethodHandle的解析結果對應的類沒有初始化,則須要初始化

本身寫的兩個不一樣的類是被同一個類加載器加載的嗎?爲何

類加載器

類加載器的做用就是從字節碼建立一個類,並負責加載 Java 應用所需的資源.

只有當一個類要使用的時候,類加載器纔會加載這個類並初始化

  • 啓動類加載器:加載Java核心庫(JAVA_HOME/lib),如rt.jar
  • 擴展類加載器:加載Java擴展庫(JAVA_HOME/lib/ext)
  • 應用類加載器:記載當前Java類(java.class.path)
  • 開發者能夠經過繼承java.lang.ClassLoader實現自定義類加載器.

User user = new User()實質就是User user = Class.forName("org.luvx.User", false, this.class.getClassLoader()).newInstance();

雙親委託模型

工做過程爲:一個類收到類加載的請求,首先不會本身去嘗試加載,而是委派爲父加載器去加載,只有當父類反饋沒法加載時,纔會嘗試本身去加載. 若是全部加載器均加載失敗, 則會拋出ClassNotFoundException異常.

意義: 能夠保證java的一些重要類如Object在各類類加載器加載下都是同一個類,由於最終都是由啓動類加載器加載,保證的類的惟一性.

存在的問題:模型自己決定的,例如基礎類要掉回用戶代碼 怎麼解決了:線程上下文類加載器

開發者能夠繼承java.lang.ClassLoader並重寫findClass()方法便可建立自定義類加載器.

一個類的類型是類自己和加載該類的加載器一塊兒肯定的

NoClassDefFoundError

NoSuchMethodError

ClassCastException:同一個類若是被不一樣的加載器加載,那他們就不是同一個類,也沒法將一個類強轉爲另外一個類,會報類轉換異常,這也是ClassLoader隔離問題.

Q&A

java 的對象分配策略 在Eden中, 大對象直接進入老年代, 長期存活的對象進入老年代, 動態年齡分配, 空間分配擔保

內存結構

  • 堆內存
    • 年輕代(8:1:1)
      • Eden空間
      • From Survivor
      • To Survivor
    • 老年代
  • 方法區
    • java虛擬機棧
    • 本地方法棧
區域 做用 共享性 存儲內容
堆內存 存放對象實例,能夠細分爲新生代和老年代 new出來的對象(屬性,方法的地址(指向方法區))
方法區 內有運行時常量池,也有人稱之爲永久代 常量,static變量,類信息(屬性,方法)
運行時常量池 方法區的一部分 運行時常量池(各類字面量和符號引用)
程序計數器 比較小的內存區域,指示當前線程所執行的字節碼的位置 × 正在執行的VM字節碼指令地址(Java方法,native方法時爲空)
VM棧 記錄方法調用 × 局部變量表,對象的引用指針
本地方法棧 執行Native方法時使用 × -

內存結構

程序計數器(Program Counter Register)

一塊較小的內存空間, 屬於線程私有.

字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令, 分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成. 若是線程正在執行一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;

若是是Native方法,則計數器爲空;多線程時, 存在多個程序計數器.

此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域.

VM棧(VM Stacks)

線程私有, 生命週期與線程相同

虛擬機棧描述的是Java方法執行的內存模型:每一個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口等信息.

每個方法被調用直至執行完成的過程, 就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程.

局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時該方法對應的須要在棧幀中分配多大的局部變量空間是徹底肯定的.

對這個區域規定了兩種異常情況: 若是線程請求的棧深度大於虛擬機所容許的深度, 將拋出StackOverflowError異常; 若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展, 只不過Java虛擬機規範中也容許固定長度的虛擬機棧), 當擴展時沒法申請到足夠的內存時會拋出OutOfMemoryError異常.

棧幀: 一個棧幀隨着一個方法的調用開始而建立,這個方法調用完成而銷燬.棧幀內存放着方法中的局部變量,操做數棧等數據

堆內存(Heap)

Java堆是被全部線程共享的一塊內存區域, 在虛擬機啓動時建立.此內存區域的惟一目的就是存放對象實例, 幾乎全部的對象實例都在這裏分配內存.

Java堆是垃圾收集器管理的主要區域, 所以不少時候也被稱作"GC堆" 多采用分代收集策略,因此細分爲新生代和老年代,再細分可分爲Eden空間,From Survivor空間,To Survivor空間,在GC的複製算法中起着重要做用,HotSpot VM默認的Eden和Survivor大小比例爲8:1

堆內存能夠是物理上不連續的內存空間, 邏輯上連續便可,

在堆中沒有內存完成實例分配, 而且堆也沒法再擴展時, 將會拋出OutOfMemoryError異常

方法區(Method Area)

與Java堆同樣, 是各個線程共享的內存區域, 它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,有時也被稱爲永久代(PermGen)

Java虛擬機規範對這個區域的限制很是寬鬆, 除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外, 還能夠選擇不實現垃圾收集

這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載

當方法區沒法知足內存分配需求時, 將拋出OutOfMemoryError異常

運行時常量池

是方法區的一部分,存放編譯期生成的各類字面量和符號引用,當JVM運行的時候會將這些常量池的信息加載進方法區.

當方法區沒法知足內存分配需求時,拋出OutOfMemoryError

本地方法棧(Native Method Stacks)

虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務, 而本地方法棧則是爲虛擬機使用到的Native方法服務

異常拋出類型和JVM棧相同

直接內存

不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,可是這部份內存也被頻繁的使用.

也可能致使OutOfMemoryError異常出現

比較

  • 棧內存用來存儲局部變量和方法調用.
  • 堆內存用來存儲Java中的對象.不管是成員變量,局部變量,仍是類變量,它們指向的對象都存儲在堆內存中.

內存溢出異常

Exception in thread "main": java.lang.OutOfMemoryError: Java heap space

緣由: 對象不能被分配到堆內存中

Exception in thread "main": java.lang.OutOfMemoryError: PermGen space

緣由: 類或者方法不能被加載到永久代.它可能出如今一個程序加載不少類的時候, 好比引用了不少第三方的庫;

Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit

緣由: 建立的數組大於堆內存的空間

Exception in thread "main": java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

緣由: 分配本地分配失敗.JNI、本地庫或者Java虛擬機都會從本地堆中分配內存空間.

Exception in thread "main": java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)

緣由: 一樣是本地方法內存分配失敗, 只不過是JNI或者本地方法或者Java虛擬機發現

java.lang.OutOfMemoryError: unable to create new native thread

緣由: 建立了太多的線程,而能建立的線程數是有限制的,致使了異常的發生

JVM調優工具

工具 做用
jps 進程狀態工具,查看正在運行的JVM進程
jstat 統計信息監視工具,實時顯示JVM進程中類裝載、內存、垃圾收集、JIT編譯等數據
jinfo 配置信息工具,查詢當前運行着的JVM屬性和參數的值
jmap 內存映射工具,生成VM的內存轉儲快照
jhat 堆轉儲快照分析工具,分析使用jmap生成的dump文件
jstack 堆棧跟蹤工具,生成當前JVM的全部線程快照,線程快照是虛擬機每一條線程正在執行的方法,目的是定位線程出現長時間停頓的緣由.
jconsole jvm監視管理控制檯,圖像化顯示堆棧等使用狀況,能夠手動進行GC,很是實用
jcmd  

JVM參數:

工具 做用
-Xmx 最大堆內存
-Xms 最小堆內存, 一般設置成跟最大堆內存同樣,減小GC
-Xmn 設置年輕代大小,官方推薦設置爲堆的3/8
-Xss 指定線程的最大棧空間, 此參數決定了java函數調用的深度, 值越大調用深度越深, 若值過小則容易出棧溢出錯誤(StackOverflowError)
-XX:PermSize 指定方法區(永久區)的初始值,默認是物理內存的1/64, 在Java8永久區移除, 代之的是元數據區, 由-XX:MetaspaceSize指定
-XX:MaxPermSize 指定方法區的最大值, 默認是物理內存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元數據區的大小
-XX:NewRatio=n 年老代與年輕代的比值,-XX:NewRatio=2, 表示年老代與年輕代的比值爲2:1
-XX:SurvivorRatio=n Eden區與一個Survivor區的大小比值,-XX:SurvivorRatio=8表示Eden區與兩個Survivor區的大小比值是8:1:1,由於Survivor區有兩個(from, to)

GC

GC的工做內容就是就是回收內存,包括哪些內存須要回收,何時回收,怎麼回收等3件工做.

堆內存也被稱爲GC堆,是由於GC的主要進行場所就是堆內存,方法區是堆內存的一部分,一樣也能夠GC的對象,

但Java虛擬機規範不要求虛擬機在方法區實現GC,並且在方法區進行GC的"性價比"通常都比較低,這和方法區被稱爲永久代有着相同的緣由.

對象是否可回收

在GC進行前首先要肯定的就是對象是否還活着(是否還在直接或間接的被使用中)

判斷對象的使用常有如下2種策略:

引用計數算法

存儲對特定對象的全部引用數,也就是說,當應用程序建立引用以及引用超出範圍時,JVM必須適當增減引用數.當某對象的引用數爲0時,即可以進行垃圾回收. 優勢: 實現簡單、效率高 缺點: 很難解決對象之間相互引用問題

可達性分析算法

經過一系列的稱爲"GC Roots"的對象做爲起始點,從這些節點開始向下搜索,搜索所走的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的.

可做爲GC Roots的對象

包括:

  • JVM棧(棧中的本地變量表)中引用的對象
  • 方法區中的類靜態屬性,常量引用的對象
  • 本地方法棧中Native方法引用的對象

四種引用

  1. 強引用: 只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象.
  2. 軟引用(SoftReference): 指還有用,可是非必須的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收.回收後仍不足就會拋出內存溢出異常.
  3. 弱引用(WeakReference): 非必須的對象,比軟引用還要弱,只能生存到下一次垃圾回收發生以前.
  4. 虛引用(PhantomReference): 對象是否有虛引用,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得對象實例.關聯虛引用惟一目的就是能在對象被回收器回收時收到系統通知

垃圾回收策略

標記-清除算法

Mark-Sweep,適用於老年代,最基礎的算法,後續的算法都是基於這種思想改進而來,

標記或清除過程的效率都不高,產生大量不連續的內存碎片,在分配大對象時候因沒法找到符合的連續空間而再次進行GC

標記-整理-清除算法

在標記-清除算法的標記的基礎上,將不被回收的對象向同一端移動,而後清理到邊界外的內存,解決了內存碎片

複製算法

爲解決標記清除算法效率和形成的不連續碎片問題而生,適用於對象存活率低的新生代

將內存分爲一塊較大的Eden空間和兩塊較小的Survivor,每次使用Eden和其中一塊Survivor,回收時,將Eden和Survivor中存活的對象一次性地拷貝到另外一塊Survivor空間,最後清理掉Eden和Survivor空間

分代回收策略

不是一種具體的GC算法,是一種不一樣代採起不一樣的回收算法的策略.

新生代的對象生命週期短,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集. 而老年代中由於對象存活率高,沒有額外的空間進行分配擔保,就必須使用「標記-清理」或者「標記-整理」算法來進行回收.

回收方法區(永久代)

此區域的回收主要有兩項內容:廢棄常量和無用的類

垃圾回收器

垃圾回收器一般是做爲一個單獨的低級別的線程運行, 不可預知的狀況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,開發者不能實時的調用垃圾回收器對某個對象或全部對象進行垃圾回收. 常見垃圾回收器有:

  • 串行垃圾回收器(Serial Garbage Collector):Client模式下的默認新生代收集器
  • ParNew 收集器:串行回收器的多線程版本,Server模式下首選的新生代收集器
  • 並行垃圾回收器(Parallel Garbage Collector):複製算法
  • 併發標記掃描垃圾回收器(Concurrent Mask Sweep Garbage Collector)
  • G1垃圾回收器(G1 Garbage Collector):基於標記-整理算法,能夠精確控制停頓.基本不犧牲吞吐量的前提下完成低停頓的內存回收

CMS

應用標記-清除算法,具體過程有初始標記-併發標記-從新標記-併發清理-併發重置

併發收集、低停頓 ;但也有對CPU資源很是敏感、沒法處理浮動垃圾,以及算法自己所具備的會產生內存碎片的缺點

G1收集器

Garbage-First(G1,垃圾優先)收集器是服務類型的收集器,目標是多處理器機器、大內存機器.

應用標記-整理算法,相對於CMS能很是精確地控制停頓,高度符合垃圾收集暫停時間的目標,同時實現高吞吐量.能夠實如今基本不犧牲吞吐量的狀況下完成低停頓的回收

內存劃分方式: 它是將堆內存被劃分爲多個大小相等的 heap 區,每一個heap區都是邏輯上連續的一段內存(virtual memory). 並跟蹤這些區域裏面的垃圾堆積程度,在後臺維護一個優先列表,每次根據容許的收集時間, 優先回收垃圾最多的區域(這也是Garbage First名稱的由來). 總而言之,區域劃分和有優先級的區域回收,保證了G1收集器在有限的時間內能夠得到最高的收集效率.

內存分配

對象主要分配在新生代的Eden區,少數狀況下會直接分配在老年代中.

分配策略:

  • 對象優先在Eden分配
  • 大對象直接進入老年代
  • 長期存活的對象進入老年代

對象年齡斷定

JVM給每一個對象都定義一個age計數器,若對象在Eden出生並通過第一次Minor GC後仍存在並被Survivor容納,對象age爲1,以後在Survivor中每通過一次Minor GC,age加1.當age達到必定數值(默認15)就會成爲老年代,默認值能夠經過-XX:MaxTenuringThreshold修改.

實際上,JVM並非總要求對象的年齡必需達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代.

GC收集器參數

參數 說明
-XX:+UseSerialGC 在新生代和老年代使用串行收集器
-XX:+UseParallelGC 新生代使用並行回收收集器
-XX:+UseParallelOldGC 老年代使用並行回收收集器
-XX:+UseParNewGC 在新生代使用並行收集器
-XX:+UseConcMarkSweepGC 新生代使用並行收集器,老年代使用CMS+串行收集器
-XX:+UseCMSCompactAtFullCollection 設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片的整理
-XX:UseCtpancyOnly 表示只在到達閥值的時候,才進行CMS回收
-XX:SurvivorRatio 設置eden區大小和survivior區大小的比例
-XX:NewRatio 新生代和老年代的比
-XX:ParallelGCThreads 設置用於垃圾回收的線程數
-XX:ParallelCMSThreads 設定CMS的線程數量
-XX:CMSInitiatingOccupancyFraction 設置CMS收集器在老年代空間被使用多少後觸發
-XX:CMSFullGCsBeforeCompaction 設定進行多少次CMS垃圾回收後,進行一次內存壓縮
-XX:+CMSClassUnloadingEnabled 容許對類元數據進行回收
-XX:CMSInitiatingPermOccupancyFraction 當永久區佔用率達到這一百分比時,啓動CMS回收
-XX:+PrintGCDetails 開啓後,GC時打印內存回收日誌,並在線程退出時輸出內存分配狀況

空間分配擔保機制

Q&A

Minor GC與Full GC分別在何時發生?何時觸發Full GC;

類型 GC對象 發生時機
Minor GC 回收年輕代, 包括Eden 和 Survivor 區域 沒法爲一個新的對象分配空間時
Major GC 永久代  
Full GC 整個堆空間  

GC收集器有哪些?CMS收集器與G1收集器的特色。

Java中的大對象如何進行存儲;

爲何新生代內存須要有兩個Survivor區?

G1停頓嗎,CMS回收步驟,CMS爲何會停頓,停頓時間;

每一個算法的優缺點啊, 怎麼簡單的解決啊

增長堆的大小, 增長後臺線程, 提早開始併發週期等

有沒有了解G1收集器這些, G1的流程, 相比CMS有哪些優點.

Minor GC發生的頻繁的緣由?�GC的時間長的緣由是什麼 對象過小, 對象太大

Full GC次數太多了,如何優化;

文章轉載於:https://www.javazhiyin.com/24771.html

相關文章
相關標籤/搜索