天天看點

JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆

JVM體系結構概述

JVM位置

JVM是運作在作業系統之上的,它與硬體沒有直接的互動      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆

JVM體系結構

JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆

類裝載器ClassLoader

負責加載class檔案,class檔案在檔案開頭有特定的檔案标示,
并且ClassLoader隻負責class檔案的加載,
至于它是否可以運作,則由Execution Engine決定      
在這裡需要區分一下class與Class。小寫的class,
是指編譯 Java 代碼後所生成的以.class為字尾名的位元組碼檔案。
而大寫的Class,是 JDK 提供的java.lang.Class,
可以了解為封裝類的模闆。多用于反射場景,
例如 JDBC 中的加載驅動,Class.forName("com.mysql.jdbc.Driver");

接下來我們來觀察下圖,
Car.class位元組碼檔案被ClassLoader類裝載器加載并初始化,
在方法區中生成了一個Car Class的類模闆,
而我們平時所用到的執行個體化,就是在這個類模闆的基礎上,
形成了一個個執行個體,即car1,car2。反過來講,
我們可以對某個具體的執行個體進行getClass()操作,
就可以得到該執行個體的類模闆,即Car Class。再接着,
我們對這個類模闆進行getClassLoader()操作,
就可以得到這個類模闆是由哪個類裝載器進行加載的。      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
JVM并不僅僅隻是通過檢查檔案字尾名是否是.class來判斷是否加載,
最主要的是通過class檔案中特定的檔案标示,
即下圖main.class檔案中的cafe babe和後面的數字要符合java的規範      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆

類裝載器分類

虛拟機自帶的加載器
1.啟動類加載器(Bootstrap)C++,
  加載%JAVAHOME%/jre/lib/rt.jar。
2.擴充類加載器(Extension)Java
  加載%JAVAHOME%/jre/lib/ext/ *.jar,例如javax.swing包。
3.應用程式類加載器(App)Java
  也叫系統類加載器,加載目前應用的classpath的所有類

使用者自定義加載器  Java.lang.ClassLoader的子類,
使用者可以定制類的加載方式      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
package com.jane;

/**
 * @author jane
 * @create 2021-03-09 13:11
 */
public class MyObject
{
    public static void main(String[] args)
    {
        Object object = new Object();
        System.out.println(object.getClass().getClassLoader());     //null

        MyObject myObject = new MyObject();
        System.out.println(myObject.getClass().getClassLoader());   //sun.misc.Launcher$AppClassLoader@14dad5dc

        System.out.println(myObject.getClass().getClassLoader().getParent().getParent());   //null
        System.out.println(myObject.getClass().getClassLoader().getParent());               //sun.misc.Launcher$ExtClassLoader@1b6d3586
        System.out.println(myObject.getClass().getClassLoader());                           //sun.misc.Launcher$AppClassLoader@14dad5dc

    }
}      
先看自定義的MyObject,
首先通過getClassLoader()擷取到的是AppClassLoader,
然後getParent()得到ExtClassLoader,再getParent()竟然是null?
可能大家會有疑惑,不應該是Bootstrap加載器麼?
這是因為,BootstrapClassLoader是使用C++語言編寫的,
Java在加載的時候就成了null。

我們再來看Java自帶的Object,
通過getClassLoader()擷取到的加載器直接就是BootstrapClassLoader,
如果要想getParent()的話,因為是null值,是以就會報
java.lang.NullPointerException空指針異常。

輸出中,sun.misc.Launcher是JVM相關調用的入口程式。      

雙親委派

當一個類收到了類加載請求,它首先不會嘗試自己去加載這個類,
而是把這個請求委派給父類去完成,
是以所有的加載請求都應該傳送到啟動類加載器中,
隻有當父類加載器回報自己無法完成這個請求的時候
(在它的加載路徑下沒有找到所需加載的Class),
子類加載器才會嘗試自己去加載。

采用雙親委派的一個好處是,比如加載位于rt.jar包中的類
java.lang.Object,不管是哪個加載器加載這個類,
最終都是委派給頂層的啟動類加載器進行加載,
確定哪怕使用了不同的類加載器,最終得到的都是同樣一個Object對象。      

沙箱安全機制

是基于雙親委派機制上采取的一種JVM的自我保護機制,
假設你要寫一個java.lang.String的類,由于雙親委派機制的原理,
此請求會先交給BootStrapClassLoader試圖進行加載,
但是BootStrapClassLoader在加載類時首先通過包和類名查找rt.jar中有沒有該類,
有則優先加載rt.jar包中的類,是以就保證了java的運作機制不會被破壞,
確定你的代碼不會污染到Java的源碼。      
因為雙親委派,是以這裡我們自己寫的java.lang.String就加載不到,
加載的是Bootstrap啟動類加載器的String
package java.lang;

/**
 * @author jane
 * @create 2021-03-09 13:41
 */
public class String
{
    public static void main(String[] args)
    {
        System.out.println("hello");
        /**
         * 錯誤: 在類 java.lang.String 中找不到 main 方法, 請将 main 方法定義為:
         *    public static void main(String[] args)
         * 否則 JavaFX 應用程式類必須擴充javafx.application.Application
         */
    }
}      

類加載器的加載順序

當AppClassLoader加載一個class時,
  它首先不會自己去嘗試加載這個類,
  而是把類加載請求委派給父類加載器ExtClassLoader去完成。
  
當ExtClassLoader加載一個class時,
  它首先也不會自己去嘗試加載這個類,
  而是把類加載請求委派給BootStrapClassLoader去完成。
  
如果BootStrapClassLoader加載失敗
  (例如在$JAVA_HOME/jre/lib裡未查找到該class),
  會使用ExtClassLoader來嘗試加載。
  
若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,
  如果AppClassLoader也加載失敗,
  則會報出異常ClassNotFoundException。      
rt.jar是什麼?做了哪些事?
為什麼可以在idea這些開發工具中可以直接去使用String、ArrayList、甚至一些JDK提供的類和方法?
這些都在rt.jar中定義好了,且直接被啟動類加載器進行加載了。      

本地方法棧 Native Method Stack

Java語言本身不能對作業系統底層進行通路和操作,
但是可以通過JNI接口調用其他語言來實作對底層的通路。

本地方法接口(Native Interface):
  其作用是融合不同的程式設計語言為 Java 所用,
  它的初衷是用來融合 C/C++ 程式的,
  Java 誕生的時候是 C/C++ 流行時期,要想立足,
  就得調用 C/C++ 程式,
  于是 Java就在記憶體中專門開辟了一塊區域處理标記為 native 的代碼。

本地方法棧(Native Method Stack)
  就是在一個 Stack 中登記這些 native 方法,
  然後在執行引擎Execution Engine執行時加載本地方法庫native      
我們通過多線程部分源碼來了解什麼是native方法。
首先我們觀察start()的源碼,發現它其實并沒有做什麼複雜的操作,
隻是單純的調用了start0()這個方法,然後我們去觀察start0()的源碼,
發現它隻是一個使用了native關鍵字修飾的一個方法(private native void start0();),
但隻有聲明卻沒有具體的實作!。
為什麼?我們都知道Thread是Class關鍵字修飾的類(class Thread implements Runnable),
而不是接口。一般來說,類中的方法都要有定義和實作,
接口裡面才有方法的定義聲明。這就是native方法的獨特之處,說白了,
被native關鍵字修飾的方法,基本上和我們,甚至和 Java 都沒啥關系了,
因為它要去調用底層作業系統或者第三方語言的庫函數,
是以我們不需要去考慮它具體是如何實作的。      

程式計數器 Program Counter Register

程式計數器(Program Counter Register),也叫PC寄存器。
每個線程啟動的時候,都會建立一個PC寄存器。
PC寄存器裡儲存目前正在執行的JVM指令的位址。 
每一個線程都有它自己的PC寄存器,也是該線程啟動時建立的。

簡單來說,PC寄存器就是儲存下一條将要執行的指令位址的寄存器,
其内容總是指向下一條将被執行指令的位址,
這裡的位址可以是一個本地指針,
也可以是在方法區中相對應于該方法起始指令的偏移量。

每個線程都有一個程式計數器,是線程私有的,就是一個指針,
指向方法區中的方法位元組碼(用來存儲指向下一條指令的位址,      
如果執行的是一個native方法,那這個計數器是空的。      

方法區 Method Area

方法區(Method Area),是供各線程共享的運作時記憶體區域,
它存儲了每一個類的結構資訊。
例如運作時常量池(Runtime Constant Pool)、字段和方法資料、構造函數和普通方法的位元組碼内容。
比如一個類的普通方法是跟着執行個體對象在堆中,但是類的靜态方法
是在方法區中,因為所有的執行個體對象共享這個靜态方法

上面說的是規範(定義的一種抽象概念),
實際在不同虛拟機裡實作是不一樣的,
最典型的就是永久代(PermGen space)和元空間(Meta space)。
Java7中,方法區 f =new 永久代();
Java8中,方法區 f =new 元空間();      
執行個體變量存在堆記憶體中,和方法區無關。      

棧和堆

棧管運作,堆管存儲      

棧(Stack),也叫棧記憶體,主管Java程式的運作,線上程建立時建立。
其生命期是跟随線程的生命期,是線程私有的,線程結束棧記憶體也就是釋放。

對于棧來說,不存在垃圾回收的問題,隻要線程一結束該棧就Over。      
棧存儲什麼資料?
  棧主要存儲8種基本類型的變量、對象的引用變量、以及執行個體方法。

這裡引出一個名詞,棧幀,什麼是棧幀?
  棧幀對應Java裡面的方法
  每個方法執行的同時都會建立一個棧幀,
  用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊,
  每個方法從調用直至執行完畢的過程,
  就對應着一個棧幀在虛拟機中入棧到出棧的過程。      
簡單來說,棧幀對應一個方法的執行和結束,是方法執行過程的記憶體模型。

其中,棧幀主要保持了3類資料:

  本地變量(Local Variables):輸入參數和輸出參數,以及方法内的變量。
  棧操作(Operand Stack):記錄出棧、入棧的操作。
  棧幀資料(Frame Data):包括類檔案、方法等。


棧的大小是根據JVM有關,一般在256K~756K之間,約等于1Mb左右。      
在圖中一個棧中有兩個棧幀,分别是Stack Frame1和Stack Frame2,
對應方法1和方法2。其中Stack Frame2是最先被調用的方法2,
是以它先入棧。然後方法2又調用了方法1,
是以Stack Frame1處于棧頂位置。執行完畢後,
依次彈出Stack Frame1和Stack Frame2,然後線程結束,棧釋放。
是以,每執行一個方法都會産生一個棧幀,并儲存到棧的頂部,
頂部的棧幀就是目前所執行的方法,該方法執行完畢後會自動出棧。      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
總結如下,棧中的資料都是以棧幀(Stack Frame)的格式存在,
棧幀是一個記憶體區塊,是一個資料集,
是一個有關方法(Method)和運作期資料的資料集,
當一個方法A被調用時就産生了一個棧幀F1,并被壓入到棧中,
方法A中又調用了方法B,于是産生棧幀F2也被壓入棧中,
方法B又調用方法C,于是産生棧幀F3也被壓入棧中······執行完畢後
,遵循“先進後出,後進先出”的原則,先彈出F3棧幀,再彈出F2棧幀,
再彈出F1棧幀。      
java.lang.StackOverflowError是錯誤,不是異常!證明如下 :      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆

棧、堆、方法區的互動關系

JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
比如MyObject myObject = new MyObject();,
等号左邊MyObject myObject的myObject就是引用,在Java棧裡面。
等号右邊的new MyObject(),new出來的MyObject執行個體對象在堆裡面。
簡單來說,就是Java棧中的引用myObject指向了堆中的MyObject執行個體對象。

而方法區中的對象類型資料是Class,是MyObject的模闆      

堆 Heap

堆的體系結構

一個JVM執行個體隻存在一個堆記憶體,堆記憶體的大小是可以調節的。
類加載器讀取了類檔案之後,需要把類、方法、常量變量放到堆記憶體中,
保持是以引用類型的真實資訊,友善執行器執行。

其中,堆記憶體分為3個部分:

  Young Generation Space,新生區、新生代
  Tenure Generation Space,老年區、老年代
  Permanent Space,永久區、元空間
Java7之前,堆結構圖如下,而Java8則隻将永久區變成了元空間。      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
總結一下:
  堆記憶體在邏輯上分為新生+養老+元空間,而堆記憶體在實體上分為新生+養老。      

對象在堆的生命周期

新生區是類的誕生、成長、消亡的區域,一個類在這裡産生,應用,
最後被垃圾回收器收集,結束生命。
新生區又分為兩部分: 
  伊甸區(Eden space)和幸存者區(Survivor pace) 

所有的類都是在伊甸區被new出來的。
幸存區有兩個: 
  0區(Survivor 0 space)和1區(Survivor 1 space)。
  
當伊甸園的空間用完時,程式又需要建立對象,
JVM的垃圾回收器将對伊甸園區進行垃圾回收(Minor GC),
将伊甸園區中的不再被其他對象所引用的對象進行銷毀。
然後将伊甸園中的剩餘對象移動到幸存0區.若幸存0區也滿了,
再對該區進行垃圾回收,然後移動到1區。
那如果1區也滿了呢?再移動到養老區。若養老區也滿了,
那麼這個時候将産生MajorGC(FullGC),進行養老區的記憶體清理。
若養老區執行了Full GC之後發現依然無法進行對象的儲存,
就會産生OOM異常“OutOfMemoryError”。

如果出現java.lang.OutOfMemoryError: Java heap space異常,
說明Java虛拟機的堆記憶體不夠。原因有二:
  (1)Java虛拟機的堆記憶體設定不夠,可以通過參數-Xms、-Xmx來調整。
  (2)代碼中建立了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)。      
然後了解點深層的東西      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
Survivor 0 Space,幸存者0區,也叫from區;
Survivor 1 Space,幸存者1區,也叫to區。

其中,from區和to區的區分不是固定的,是互相交換的,
意思是說,在每次GC之後,兩者會進行交換,誰空誰就是to區。      
(1)Eden Space、from複制到to,年齡+1。
首先,當Eden Space滿時,會觸發第一次GC,
把還活着的對象拷貝到from區。而當Eden Space再次觸發GC時,
會掃描Eden Space和from,對這兩個區進行垃圾回收,
經過此次回收後依舊存活的對象,
則直接複制到to區(如果對象的年齡已經達到老年的标準,
則移動至老年代區),同時把這些對象的年齡+1。

(2)清空Eden Space、from
然後,清空Eden Space和from中的對象,此時的from是空的。

(3)from和to互換
最後,from和to進行互換,原from成為下一次GC時的to,
原to成為下一次GC時的from。部分對象會在from和to中來回進行交換複制,
如果交換15次(由JVM參數MaxTenuringThreshold決定,預設15),
最終依舊存活的對象就會移動至老年代。

總結一句話,GC之後有交換,誰空誰是to。
這樣也是為了保證記憶體中沒有碎片,是以Survivor 0 Space和Survivor 1      

面試真題

package com.jane;

/**
 * @author jane
 * @create 2021-03-09 20:07
 */
public class PoolTest
{
    public static void main(String[] args)
    {
        PoolTest test = new PoolTest();
        String str="abc";
        test.Change(str);
        System.out.println(str);
        //結果是:abc
    }
    private void Change(String str)
    {
        str="def";
    }
}      
是這樣的,對于String類型的資料,有個字元串資料池
在main方法裡面建立abc的str,首先去線程池裡面看看有沒有
值是abc的字元串,發現沒有,就建立,然後線上程池儲存起來了
然後調用Change()方法,傳入的是str的位址,然後想修改,
然後看看線程池裡面有沒有值是def的字元串,發現沒有又重新找個
地方建立一個值是def的字元串,然後将Change()方法裡面的
str的指向位址改成def字元串的位址      

HotSpot虛拟機的記憶體管理

JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
不同對象的生命周期不同,其中98%的對象都是臨時對象,
即這些對象的生命周期大多隻存在于Eden區。      
實際而言,方法區(Method Area)和堆一樣,是各個線程共享的記憶體區域,
它用于存儲虛拟機加載的:類資訊+普通常量+靜态常量+編譯器編譯後的代碼等等。
雖然JVM規範将方法區描述為堆的一個邏輯部分,
但它卻還有一個别名叫做Non-Heap(非堆記憶體),目的就是要和堆區分開。

對于HotSpot虛拟機而言,很多開發者習慣将方法區稱為 “永久代(Permanent Gen)” 。
但嚴格來說兩者是不同的,或者說隻是使用永久代來實作方法區而已,
永久代是方法區(可以了解為一個接口interface)的一個實作,
JDK1.7的版本中,已經将原本放在永久代的字元串常量池移走。
(字元串常量池,JDK1.6在方法區,JDK1.7在堆,JDK1.8在元空間。)      
JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
如果沒有明确指明,Java虛拟機的名字就叫做HotSpot。      
整個堆分為新生區和養老區,新生區占整個堆的1/3,養老區占2/3。
新生區又分為3份:伊甸園區:幸存者0區(from區):幸存者1區(to區)=8:1:1
每次從伊甸園區經過GC幸存的對象,年齡(代數)會+1      

永久區

永久區是一個常駐記憶體區域,
用于存放JDK自身所攜帶的Class,Interface的中繼資料(也就是上面文章提到的rt.jar等),
也就是說它存儲的是運作環境必須的類資訊,
被裝載進此區域的資料是不會被垃圾回收器回收掉的,
關閉JVM才會釋放此區域所占用的記憶體。      

JDK1.7

JVM1:體系結構概述,ClassLoader,Native Method Stack,Program Counter Register,Method Area,棧和堆
堆的參數主要是有兩個: -Xms 和 -Xmx
  -Xms  堆的初始化的大小
  -Xmx  堆的最大的大小

Yong Gen(新生代)有個參數 -Xmn,這個參數可以調新生區和養老區的比例
    但是一般不會調

永久代的兩個參數-XX:PermSize和-XX:MaxPermSize
  分别調永久待的初始值和最大值,
  jdk8之後就沒有這兩個參數了,Java8之後元空間不在虛拟機内,      
在JDK1.8中,永久代已經被移除,被一個稱為元空間的區域所取代。
元空間的本質和永久代類似。

元空間與永久代之間最大的差別在于:
 永久帶使用的JVM的堆記憶體,
 但是java8以後的元空間并不在虛拟機中而是使用本機實體記憶體。

是以,預設情況下,元空間的大小僅受本地記憶體限制。
類的中繼資料放入native memory,字元串池和類的靜态變量放入Java堆中,
這樣可以加載多少類的中繼資料就不再由MaxPermSize控制,