JVM最完整最深入解析

Java運行時數據區

  1. 程序計數器:指向當前線程正在執行的字節碼指令。線程私有的。
  2. 虛擬機站:虛擬機站是Java執行方法的內存模型。每個方法被執行的時候,都會創建一個棧幀,把棧幀壓入棧,當方法正常返回或者拋出未捕獲的異常時,棧幀就會出棧。

         (1) 棧幀:棧幀存儲方法的相關信息,包含局部變量表、返回值、操作數棧、動態鏈接

         a) 局部變量表:包含了方法執行過程中的所有變量。局部變量數組所需要的空間在編譯期間完成分配,在方法運行期間不  會改變局部變量數組的大小。

         b) 返回值:如果有返回值的話,壓入調用者棧幀的操作數棧中,並且把PC的值指向方法調用指令後面的一條指令地址

         c) 操作數棧:操作變量的內存模型。操作數棧的最大深度在編譯的時候已經確定(寫入方法區code屬性的max_stacks頂中)。操作數棧的元素可以是任意java類型,包括long和double,32位數據佔用棧空間爲1,64位數據佔用2。方法剛開始執行的時候,棧是空的,當方法執行過程中,各種字節碼指令往棧中存取數據。

        d) 動態鏈接:每個棧幀都持有在運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態鏈接。

      (2)線程私有

    3. 本地方法棧

       1)調用本地native的內存模型

       2)線程獨享

     4. 方法區:用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據

    (1)線程共享的
    (2)運行時常量池: a) 是方法區的一部分。b)存放編譯期生成的各種字面量和符號引用。c)Class文件中除了存有類的版本、字段、方法、接口等描述信息,還有一項是常量池,存有這個類的編譯期生成的各種字面量和符號引用,這部分內容在類加載後,存放到方法區的運行時常量池中。

     5. 堆(Heap):Java對象存儲的地方

      1)Java堆是虛擬機管理的內存中最大的一塊

      2)Java堆是所有線程共享的區域

      3)在虛擬機啓動時創建

      4)此內存區域的唯一目的就是存放對象實例,幾乎所有對象實例都在這裏分配內存。存放new生成的對象和數組

      5)Java堆是垃圾收集器管理的內存區域,很多時候成爲GC堆

JMM Java內存模型

1. Java併發採用“共享內存”模型,線程之間通過讀寫內存的公共狀態進行通訊。

2. 主要目的是定義程序中各個變量的訪問規則

3. Java內存模型規定所有變量都存儲在主內存中,每個線程還有自己的工作內存。

  1)線程的工作內存中保存了被該線程使用到的變量的拷貝(從主內存中拷貝過來),線程對變量的所有操作都必須在工作內存中執行,而不能直接訪問主內存中的變量。

  2)不同線程之間無法訪問對方工作內存的變量,線程間變量值的傳遞都要通過主內存來完成的。

  3)主內存主要對應Java堆中實例數據部分。工作內存對英語虛擬機站中部分區域。

  

  3)每個線程有一個私有的本地內存,裏面存儲了讀/謝共享變量的副本。

4. Java線程之間的通信由內存模型JMM(Java Memory Model)控制。

  1)JMM決定一個線程對變量的寫入何時對另一個線程可見

  2)線程之間共享變量存儲在主內存中

  3)每個線程有一個私有的本地內存,裏面存儲了讀/寫共享變量的副本

  4)JMM通過控制每個線程的本地內存之間的交互,來爲程序員提供內存可見性保證

堆的內存劃分

Java堆的內存劃分分別爲年輕代、Old Memory(老年代)、Perm(永久代),在1.8中,永久代被移除,使用MetaSpace代替

1、新生代

  1. 使用複製算法(Coping算法),因爲年輕代每次GC都要回收大部分對象。新生代裏分成一份較大的Eden空間和兩份較小的Survivor空間。每次使用Eden和其中一塊Survivor空間,然後垃圾回收的時候,把存活對象放到未使用的Survivor空間中,清空Eden和剛使用過的Survivor空間
  2. 分爲Eden、Survivor From、Survivor To,比例默認爲8:1:1
  3. 內存不足時發生Minor GC

2、老年代

  1. 採用標記-整理算法(mark-compact),原因是老年代每次GC只會回收少部分對象

3、永久代

    用來存儲類的元數據,也就是方法區

  1. Perm的廢除:在jdk1.8中,Perm被替換成MetaSpace,MetaSpace存放在本地內存中。原因是永久代進場內存不夠用,或者發生內存泄漏
  2. MetaSpace(元空間):元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存

GC垃圾回收

一、判斷對象是否要回收的方法:可達性分析法

1、可達性分析法:

      通過一系列“GC Roots”對象作爲起點進行搜索,如果在“GC Roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。不可達對象不一定會成爲可回收對象。進入DEAD狀態的線程還可以恢復,GC不會回收它的內存。(把一些對象當做root對象,JVM認爲root對象是不可回收的,並且root對象引用的對象也是不可回收的)

2、以下對象會被認爲是root對象:

  1. 虛擬機棧(棧幀中本地變量表)中引用的對象
  2. 方法區中靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中Native方法引用的對象

3、對象被判定可被回收,需要經歷兩個階段:

  1. 第一個階段是可達性分析,分析該對象是否可達
  2. 第二個階段是當對象沒有重寫finalize()方法或者finalize()方法已經被調用過,虛擬機認爲該對象不可以被救活,因此回收該對象。(finalize()方法在垃圾回收中的作用是,給該對象一次救活的機會)

4、方法去中的垃圾回收:

  1. 常量池中一些常量、符號引用沒有被引用,則會被清理出常量池
  2. 無用的類:被判定爲無用的類,會被清理出方法區。判定方法如下:a) 該類的所有實例被回收。b) 加載該類的ClassLoader被回收。c) 該類的class對象沒有被引用。

5、finalize():

  1. GC垃圾回收要回收一個對象的時候,調用該對象的finalize()方法。然後在下一次垃圾回收的時候,纔去回收這個對象的內存,
  2. 可以在該方法裏面,制定一些對象在釋放前必須執行的操作。

二、發現虛擬機頻繁的full GC時應該咋辦:

    full GC指的是清理整個堆空間,包括年輕代和永久代

  1. 首先用命令查看觸發GC的原因是什麼jstat -gccause進程id
  2. 如果是System.gc(),則看下哪裏調用了
  3. 如果是heap inspection(內存檢查),可能是哪裏執行jmap -histo[:live] 命令
  4. 如果是GC locker,可能是程序依賴的JNI庫的原因

三、常見的垃圾回收算法:

1、Mark-Sweep(標記-清除算法):

  1. 思想:標記-清除算法分爲兩個階段,標記階段和清除階段。標記階段任務是標記出所有需要回收的對象,清除階段就是清除被標記對象的空間。
  2. 優缺點:實現簡單,容易產生內存碎片

2、Copying(複製清除算法):

  1. 思想:將可用內存劃分爲大小相等的兩塊,每次只使用其中一塊。當進行垃圾回收的時候了,把其中存活對象全部複製到另外一塊中,然後把已使用的內存空間一次清空掉
  2. 優缺點:不容易產生內存碎片;可用內存空間少;存活對象多的話,效率低

3、Mark-Compact(標記-整理算法):

  1. 思想:先標記存活對象,然後把存活對象向一邊移動,然後清理掉端邊界以外的內存
  2. 優缺點:不容易產生內存碎片;內存利用率高;存活對象多並且分散的時候,移動次數多,效率低下

4、分代手機算法(大部分JVM的垃圾收集器所採用的算法):

  1. 因爲新生代每次垃圾回收都要回收大部分對象,所以新生代採用Copying算法。新生代裏面分成一份較大的Eden空間和兩份較小的Survivor空間。每次只使用Eden和其中一塊Survivor空間,然後垃圾回收的時候,把存活對象放到未使用的Survivor(劃分出from、to)空間中,清空Eden和剛纔使用過的Survivor空間
  2. 由於老年代每次只回收少量的對象,因此採用mark-compact算法
  3. 在堆區外有一個永久代。對永久代的回收主要是無效的類和常量

5、GC使用時對程序的影響?

    垃圾回收會影響程序的性能,Java虛擬機必須要追蹤運行程序中的有用對象,然後釋放沒用對象,這個過程消耗處理器時間

6、幾種不同的垃圾回收類型:

  1. Minor GC:從年輕代(包括Eden、Survivor區)回收內存
  2. Major GC:清理整個老年代,當eden區內存不足時觸發
  3. Full GC:清理整個堆空間,包括年輕代和老年代。當老年代內存不足時觸發

JVM優化

1、一般來說,當survivor區不夠大或者佔用量達到50%,就會把一些對象放到老年區。通過設置合理的eden區,survivor區及使用率,可以將年輕對象保存在年輕代,從而避免full GC,使用-Xmn設置年輕代的大小。

2、對於佔用內存比較多的大對象,一般會選擇在老年代分配內存。如果在年輕代給大對象分配內存,年輕代內存不夠了,就要在eden區移動大量對象到老年代,然後這些移動的對象可能很快消亡,因此導致full GC。通過設置參數:-XX:PetenureSizeThreshold=1000000,單位爲B,標明對象大小超過1M時,在老年代(tenured)分配內存空間。

3、一般情況下,年輕對象放在eden區,當第一次GC後,如果對象還存活,放到survivor區,此後,每GC一次,年齡增加1,當對象的年齡達到閾值,就被放到tenured老年區。這個閾值可以同構-XX:MaxTenuringThreshold設置。如果想讓對象留在年輕代,可以設置比較大的閾值。

4、設置最小堆和最大堆:-Xmx-Xms穩定的堆大小堆垃圾回收是有利的,獲得一個穩定的堆大小的方法是設置-Xms和-Xmx的值一樣,即最大堆和最小堆一樣,如果這樣子設置,系統在運行時堆大小理論上是恆定的,穩定的堆空間可以減少GC次數,因此,很多服務端都會將這兩個參數設置爲一樣的數值。穩定的堆大小雖然減少GC次數,但是增加每次GC的時間,因爲每次GC要把堆的大小維持在一個區間內。

5、一個不穩定的堆並非毫無用處。在系統不需要使用大內存的時候,壓縮堆空間,使得GC每次應對一個較小的堆空間,加快單次GC次數。基於這種考慮,JVM提供兩個參數,用於壓縮和擴展堆空間。

  1. -XX:MinHeapFreeRatio 參數用於設置堆空間的最小空閒比率。默認值是40,當堆空間的空閒內存比率小於40,JVM便會擴展堆空間
  2. -XX:MaxHeapFreeRatio 參數用於設置堆空間的最大空閒比率。默認值是70, 當堆空間的空閒內存比率大於70,JVM便會壓縮堆空間
  3. 當-Xmx和-Xmx相等時,上面兩個參數無效

6、通過增大吞吐量提高系統性能,可以通過設置並行垃圾回收收集器

  1. -XX:+UseParallelGC:年輕代使用並行垃圾回收收集器。這是一個關注吞吐量的收集器,可以儘可能的減少垃圾回收時間
  2. -XX:+UseParallelOldGC:設置老年代使用並行垃圾回收收集器

7、嘗試使用大的內存分頁:使用大的內存分頁增加CPU的內存尋址能力,從而系統的性能。-XX:+LargePageSizeInBytes 設置內存頁的大小

8、使用非佔用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停頓

9、-XXSurvivorRatio=3,表示年輕代中的分配比率:survivor:eden = 2:3

10、JVM性能調優的工具:

  1. jps(Java Process Status):輸出JVM中運行的進程狀態信息(現在一般使用jconsole)
  2. jstack:查看java進程內線程的堆棧信息
  3. jmap:用於生成堆轉存快照
  4. jhat:用於分析jmap生成的堆轉存快照(一般不推薦使用,而是使用Ecplise Memory Analyzer)
  5. jstat是JVM統計監測工具。可以用來顯示垃圾回收信息、類加載信息、新生代統計信息等
  6. VisualVM:故障處理工具

類加載機制:

一、概念

    類加載器把class文件中的二進制數據讀入到內存中,存放在方法區,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類加載的步驟如下:

  1. 加載:查找並加載類的二進制數據(把class文件裏面的信息加載到內存裏面)
  2. 連接:把內存中類的二進制數據合併到虛擬機的運行時環境中

       (1) 驗證:確保被加載的類的正確性,包括:

   A、類文件的結構檢查:檢查是否滿足Java類文件的固定格式
   B、語義檢查:確保類本身符合Java的語法規範
   C、字節碼驗證:確保字節碼流可以被Java虛擬機安全的執行。字節碼流是操作碼組成的序列。每一個操作碼後面都會跟着一個或者多個操作數。字節碼檢查這個步驟會檢查每一個操作碼是否合法。
   D、二進制兼容性驗證:確保相互引用的類之間是協調一致的

      (2) 準備:爲類的靜態變量分配內存,並將其初始化爲默認值

      (3) 解析:把類中的符號引用轉化爲直接引用(比如說方法的符號引用,是有方法名和相關描述符組成,在解析階段,JVM把符號引用替換成一個指針,這個指針就是直接引用,它指向該類的該方法在方法區中的內存位置)

    3. 初始化:爲類的靜態變量賦予正確的初始值。當靜態變量的等號右邊的值是一個常量表達式時,不會調用static代碼塊進行初始化。只有等號右邊的值是一個運行時運算出來的值,纔會調用static初始化。

二、雙親委派模型

1、當一個類加載起收到類加載請求的時候,他首先不會自己去加載這個類的信息,而是把該請求轉發給父類加載器,依次向上。所以所有的類加載請求都會被傳遞到父類加載器中,只有當父類加載器中無法加載到所需的類,子類加載器纔會自己嘗試去加載該類。噹噹前類加載器和所有父類加載器都無法加載該類時,拋出ClassNotFindException異常。

2、意義:

    提高系統的安全性。用戶自定義的類加載器不可能加載應該由父加載器加載的可靠類。(比如用戶定義了一個惡意代碼,自定義的類加載器首先讓系統加載器去加載,系統加載器檢查該代碼不符合規範,於是就不繼續加載了)

三、特點

  1. 全盤負責:當一個類加載器加載一個類時,該類所依賴的其他類也會被這個類加載器加載到內存中。
  2. 緩存機制:所有的Class對象都會被緩存,當程序需要使用某個Class時,類加載器先從緩存中查找,找不到,才從class文件中讀取數據,轉化成Class對象,存入緩存中

四、類加載器

    兩種類型的類加載器:

1、JVM自帶的類加載器(3種):

    (1) 根類加載起(Bootstrap):

        a) C++編寫的,程序員無法在程序中獲取該類

        b) 負責加載虛擬機的核心庫,比如java.lang.Object

        c)沒有繼承ClassLoader類

    (2) 擴展類加載器(Extension):

        a) Java編寫的,從指定目錄中加載類庫

        b) 父加載器是根類加載器

        c) 是ClassLoader的子類

        d) 如果用戶把創建jar文件放到指定目錄中,也會被擴展加載器加載

    (3) 系統加載器(System)或者應用加載器(App):

        a) Java編寫的
        b) 父加載器是擴展類加載器
        c) 從環境變量或者class.path中加載類
        d) 是用戶自定義類加載的默認父加載器
        e) 是ClassLoader的子類

2、用戶自定義的類加載器

(1)Java.lang.ClassLoader類的子類
(2)用戶可以定製類的加載方式
(3)父類加載器是系統加載器
(4)編寫步驟:
A、繼承ClassLoader
B、重寫findClass方法。從特定位置加載class文件,得到字節數組,然後利用defineClass把字節數組轉化爲Class對象
(5)爲什麼要自定義類加載器? 
A、可以從指定位置加載class文件,比如說從數據庫、雲端加載class文件
B、加密:Java代碼可以被輕易的反編譯,因此,如果需要對代碼進行加密,那麼加密以後的代碼,就不能使用Java自帶的ClassLoader來加載這個類了,需要自定義ClassLoader,對這個類進行解密,然後加載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章