一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

第九章

 心本心123 2022-03-25

文章目錄


1.棧、堆、方法區(qū)的交互關(guān)系

從內(nèi)存結(jié)構(gòu)看

image-20201127215445650

從線程共享與否的角度看

ThreadLocal:如何保證多個線程在并發(fā)環(huán)境下的安全性?典型應用就是數(shù)據(jù)庫連接管理,以及獨立會話管理

image-20200708094507624

棧、堆、方法區(qū)的交互關(guān)系

  • Person 類的 .class 信息存放在方法區(qū)中
  • person 變量存放在 Java 棧的局部變量表中
  • 真正的 person 對象存放在 Java 堆中

image-20201127220645705

  • 在 person 對象中,有個指針指向方法區(qū)中的 person 類型數(shù)據(jù),表明這個 person 對象是用方法區(qū)中的 Person 類 new 出來的

第10章_方式2:使用直接指針訪問

2.方法區(qū)的理解

官方文檔

點此進入官方文檔

image-20201127221039734

2.1 方法區(qū)的位置

  • 《Java虛擬機規(guī)范》中明確說明:盡管所有的方法區(qū)在邏輯上是屬于堆的一部分,但一些簡單的實現(xiàn)可能不會選擇去進行垃圾收集或者進行壓縮。
  • 但對于HotSpotJVM而言,方法區(qū)還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。
  • 所以,方法區(qū)可以看作是一塊獨立于Java堆的內(nèi)存空間

image-20201127221429305

2.2 方法區(qū)的理解

方法區(qū)主要存放的是 Class,而堆中主要存放的是實例化的對象

  • 方法區(qū)(Method Area)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域
  • 多個線程同時加載統(tǒng)一個類時,只能有一個線程能加載該類,其他線程只能等等待該線程加載完畢,然后直接使用該類,即類只能加載一次。
  • 方法區(qū)在JVM啟動的時候被創(chuàng)建,并且它的實際物理內(nèi)存空間和Java堆區(qū)一樣都可以是不連續(xù)的。
  • 方法區(qū)的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
  • 方法區(qū)是接口,元空間或者永久代是方法區(qū)的實現(xiàn)
  • 方法區(qū)的大小決定了系統(tǒng)可以保存多少個類,如果系統(tǒng)定義了太多的類,導致方法區(qū)溢出,虛擬機同樣會拋出內(nèi)存溢出錯誤:
    • java.lang.OutofMemoryError:PermGen space(JDK7之前)
    • 或者
    • java.lang.OutOfMemoryError:Metaspace(JDK8之后)
  • 舉例說明方法區(qū) OOM
    • 加載大量的第三方的jar包
    • Tomcat部署的工程過多(30~50個)
    • 大量動態(tài)的生成反射類
  • 關(guān)閉JVM就會釋放這個區(qū)域的內(nèi)存。

代碼示例

/**
 * -Xms600m -Xmx600m
 */
public class EdenSurvivorTest {
    public static void main(String[] args) {
        System.out.println('我只是來打個醬油~');
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 簡單的程序,加載了好多類

image-20201127222759387

  • 可以看到設置堆內(nèi)存為600M后,年輕代+老年代=600M,所以說方法區(qū)是不存在堆中的

image-20201127222235659

2.3 Hotspot中方法區(qū)的演進過程

  • 在 JDK7 及以前,習慣上把方法區(qū),稱為永久代。JDK8開始,使用元空間取代了永久代。JDK 1.8之后,元空間存放在堆外內(nèi)存中
  • 我們可以將方法區(qū)類比為Java中的接口,將永久代或元空間類比為Java中具體的實現(xiàn)類
  • 本質(zhì)上,方法區(qū)和永久代并不等價。僅是對Hotspot而言的可以看作等價。《Java虛擬機規(guī)范》對如何實現(xiàn)方法區(qū),不做統(tǒng)一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
  • 現(xiàn)在來看,當年使用永久代,不是好的idea。導致Java程序更容易OOm(超過-XX:MaxPermsize上限)
  • 而到了JDK8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實現(xiàn)的元空間(Metaspace)來代替
  • 元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn)。不過元空間與永久代最大的區(qū)別在于:元空間不在虛擬機設置的內(nèi)存中,而是使用本地內(nèi)存
  • 永久代、元空間二者并不只是名字變了,內(nèi)部結(jié)構(gòu)也調(diào)整了
  • 根據(jù)《Java虛擬機規(guī)范》的規(guī)定,如果方法區(qū)無法滿足新的內(nèi)存分配需求時,將拋出OOM異常

第08章_堆和方法區(qū)圖

3.設置方法區(qū)大小與OOM

方法區(qū)的大小不必是固定的,JVM可以根據(jù)應用的需要動態(tài)調(diào)整

3.1 JDK7 永久代

  • 通過-XX:Permsize來設置永久代初始分配空間。默認值是20.75M
  • -XX:MaxPermsize來設定永久代最大可分配空間。32位機器默認是64M,64位機器模式是82M
  • 當JVM加載的類信息容量超過了這個值,會報異常OutofMemoryError:PermGen space。

image-20200708111756800

3.2 JDK8 元空間

  • 元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MetaspaceSize-XX:MaxMetaspaceSize 指定
  • 默認值依賴于平臺,Windows下,-XX:MetaspaceSize 約為21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制
  • 與永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統(tǒng)內(nèi)存。如果元數(shù)據(jù)區(qū)發(fā)生溢出,虛擬機一樣會拋出異常OutOfMemoryError:Metaspace
  • -XX:MetaspaceSize:設置初始的元空間大小。對于一個 64位 的服務器端 JVM 來說,其默認的 -XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,F(xiàn)ull GC將會被觸發(fā)并卸載沒用的類(即這些類對應的類加載器不再存活),然后這個高水位線將會重置。新的高水位線的值取決于GC后釋放了多少元空間。
    • 如果釋放的空間不足,那么在不超過MaxMetaspaceSize時,適當提高該值。
    • 如果釋放空間過多,則適當降低該值。
  • 如果初始化的高水位線設置過低,上述高水位線調(diào)整情況會發(fā)生很多次。通過垃圾回收器的日志可以觀察到Full GC多次調(diào)用。為了避免頻繁地GC,建議將-XX:MetaspaceSize設置為一個相對較高的值。

配置元空間大小示例

/**
 * 測試設置方法區(qū)大小參數(shù)的默認值
 *
 * jdk7及以前:
 * -XX:PermSize=100m -XX:MaxPermSize=100m
 *
 * jdk8及以后:
 * -XX:MetaspaceSize=100m  -XX:MaxMetaspaceSize=100m
 */
public class MethodAreaDemo {
    public static void main(String[] args) {
        System.out.println('start...');
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println('end...');
    }
}
  • JVM參數(shù)
-XX:MetaspaceSize=100m  -XX:MaxMetaspaceSize=100m
  • 終端命令查看設置的元空間大小
xiexu@Macintosh ~ % jps
xiexu@Macintosh ~ % jinfo -flag MetaspaceSize 47064
xiexu@Macintosh ~ % jinfo -flag MaxMetaspaceSize 47064

image-20201127224948624

3.3 方法區(qū)OOM

  • OOMTest 類繼承 ClassLoader 類,獲得 defineClass() 方法,可自己進行類的加載
/**
 * jdk6/7中:
 * -XX:PermSize=10m -XX:MaxPermSize=10m
 *
 * jdk8中:
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class OOMTest extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            OOMTest test = new OOMTest();
            for (int i = 0; i < 10000; i++) {
                //創(chuàng)建ClassWriter對象,用于生成類的二進制字節(jié)碼
                ClassWriter classWriter = new ClassWriter(0);
                //指明版本號,修飾符,類名,包名,父類,接口
                classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, 'Class' + i, null, 'java/lang/Object', null);
                //返回byte[]
                byte[] code = classWriter.toByteArray();
                //類的加載
                test.defineClass('Class' + i, code, 0, code.length); //Class對象
                j++;
            }
        } finally {
            System.out.println(j);
        }
    }
}

不設置元空間的上限

  • 使用默認的 JVM 參數(shù),元空間不設置上限
10000

設置元空間的上限

  • JVM 參數(shù)
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
  • 元空間出現(xiàn) OOM
3331
Exception in thread 'main' java.lang.OutOfMemoryError: Compressed class space
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at cn.sxt.java.OOMTest.main(OOMTest.java:26)

3.4 如何解決OOM?

  • 要解決OOM異?;騢eap space的異常,一般的手段是首先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉(zhuǎn)儲快照進行分析,重點是確認內(nèi)存中的對象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)
  • 內(nèi)存泄漏就是有大量的引用指向某些對象,但是這些對象以后不會使用了,但是因為它們還和GC ROOT有關(guān)聯(lián),所以導致以后這些對象也不會被回收,這就是內(nèi)存泄漏的問題
  • 如果是內(nèi)存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。
  • 如果不存在內(nèi)存泄漏,換句話說就是內(nèi)存中的對象確實都還必須存活著,那就應當檢查虛擬機的堆參數(shù)(-Xmx與-Xms),與機器物理內(nèi)存對比看是否還可以調(diào)大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時間過長的情況,嘗試減少程序運行期的內(nèi)存消耗。

4.方法區(qū)的內(nèi)部結(jié)構(gòu)

4.1 方法區(qū)結(jié)構(gòu)

image-20200708161728320

《深入理解Java虛擬機》書中對方法區(qū)(Method Area)存儲內(nèi)容描述如下:它用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等。

image-20200708161856504

4.1.1 類型信息

對每個加載的類型(類class、接口interface、枚舉enum、注解annotation),JVM必須在方法區(qū)中存儲以下類型信息:

  • 這個類型的完整有效名稱(全類名=包名.類名)
  • 這個類型直接父類的完整有效名(對于interface或是java.lang.Object,都沒有父類)
  • 這個類型的修飾符(public,abstract,final的某個子集)
  • 這個類型直接接口的一個有序列表

4.1.2 域(Field)信息

  • JVM必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序。
  • 域信息通俗來講是類的成員變量
  • 域的相關(guān)信息包括:
    • 域名稱
    • 域類型
    • 域修飾符(public,private,protected,static,final,volatile,transient的某個子集)

4.1.3 方法(Method)信息

JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:

  • 方法名稱
  • 方法的返回類型(包括 void 返回類型),void 在 Java 中對應的類為 void.class
  • 方法參數(shù)的數(shù)量和類型(按順序)
  • 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
  • 方法的字節(jié)碼(bytecodes)、操作數(shù)棧、局部變量表及大?。╝bstract和native方法除外)
  • 異常表(abstract和native方法除外),異常表記錄每個異常處理的開始位置、結(jié)束位置、代碼處理在程序計數(shù)器中的偏移地址、被捕獲的異常類的常量池索引

代碼示例

/**
 * 測試方法區(qū)的內(nèi)部構(gòu)成
 */
public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable {
    //屬性
    public int num = 10;
    private static String str = '測試方法的內(nèi)部結(jié)構(gòu)';

    //構(gòu)造器沒寫

    //方法
    public void test1() {
        int count = 20;
        System.out.println('count = ' + count);
    }

    public static int test2(int cal) {
        int result = 0;
        try {
            int value = 30;
            result = value / cal;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public int compareTo(String o) {
        return 0;
    }
}
  • 反編譯字節(jié)碼文件,并輸出到文本文件中,便于查看
  • 參數(shù) -p 確保能查看 private 權(quán)限類型的字段或方法
javap -v -p MethodInnerStrucTest.class > Text.txt

類型信息

  • 在運行時方法區(qū)中,類信息中記錄了哪個加載器加載了該類,同時類加載器也記錄了它加載了哪些類
  • 從反編譯文件可以看出,字節(jié)碼文件記錄了 MethodInnerStrucTest 繼承了哪些類,實現(xiàn)了哪些方法
//類型信息
public class cn.sxt.java.MethodInnerStrucTest extends java.lang.Object
implements java.lang.Comparable<java.lang.String>, java.io.Serializable

域信息

  • descriptor: I 表示字段類型為 Integer
  • flags: ACC_PUBLIC 表示字段權(quán)限修飾符為 public
//域信息
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

  private static java.lang.String str;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC

方法信息

  • descriptor: ( )V 表示方法返回值類型為 void
  • flags: ACC_PUBLIC 表示方法權(quán)限修飾符為 public
  • stack=3 表示操作數(shù)棧深度為 3
  • locals=2 表示局部變量個數(shù)為 2 個(實力方法包含 this)
  • test1( ) 方法雖然沒有參數(shù),但是其 args_size=1 ,這是因為將 this 作為了參數(shù)
public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: bipush        20
         2: istore_1
         3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: new           #4                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #5                  // Method java/lang/StringBuilder.'<init>':()V
        13: ldc           #6                  // String count =
        15: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: iload_1
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 17: 0
        line 18: 3
        line 19: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcn/sxt/java/MethodInnerStrucTest;
            3      26     1 count   I

4.2 域信息特殊情況

non-final 類型的類變量

  • 靜態(tài)變量和類關(guān)聯(lián)在一起,隨著類的加載而加載,他們成為類數(shù)據(jù)在邏輯上的一部分
  • 類變量被類的所有實例共享,即使沒有類實例時,你也可以訪問它

代碼示例

  • 如下代碼所示,即使我們把order設置為null,也不會出現(xiàn)空指針異常
  • 這更加表明了 static 類型的字段和方法隨著類的加載而加載,并不屬于特定的類實例
/**
 * non-final的類變量
 */
public class MethodAreaTest {
    public static void main(String[] args) {
        Order order = null;
        order.hello();
        System.out.println(order.count);
    }
}

class Order {
    public static int count = 1;
    public static final int number = 2;


    public static void hello() {
        System.out.println('hello!');
    }
}

程序運行結(jié)果

hello!
1

全局常量:static final

  • 全局常量就是使用 static final 進行修飾
  • 被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就會被分配了。

代碼示例

class Order {
    public static int count = 1;
    public static final int number = 2;


    public static void hello() {
        System.out.println('hello!');
    }
}
  • 反編譯,查看字節(jié)碼指令,可以發(fā)現(xiàn) number 的值已經(jīng)寫死在字節(jié)碼文件中了
  public static int count;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public static final int number;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2

4.3 運行時常量池

點此進入官方文檔

運行時常量池 VS 常量池

  • 方法區(qū),內(nèi)部包含了運行時常量池
  • 字節(jié)碼文件,內(nèi)部包含了常量池
  • 要弄清楚方法區(qū),需要理解清楚ClassFile,因為加載類的信息都在方法區(qū)。
  • 要弄清楚方法區(qū)的運行時常量池,需要理解清楚ClassFile中的常量池。

image-20200708171151384

常量池

  • 一個有效的字節(jié)碼文件中除了包含類的版本信息、字段、方法以及接口等描述符信息外
  • 還包含一項信息就是常量池表Constant Pool Table),包括各種字面量和對類型、域和方法的符號引用

image-20200708172357052

為什么需要常量池?

  • 一個java源文件中的類、接口,編譯后產(chǎn)生一個字節(jié)碼文件。而Java中的字節(jié)碼需要數(shù)據(jù)支持,通常這種數(shù)據(jù)會很大以至于不能直接存到字節(jié)碼里,換另一種方式,可以存到常量池
  • 這個字節(jié)碼包含了指向常量池的引用。在動態(tài)鏈接的時候會用到運行時常量池,之前有介紹
  • 比如:如下的代碼:
public class SimpleClass {
    public void sayHello() {
        System.out.println('hello');
    }
}
  • 雖然上述代碼只有194字節(jié),但是里面卻使用了String、System、PrintStream及Object等結(jié)構(gòu)。
  • 如果不使用常量池,就需要將用到的類信息、方法信息等記錄在當前的字節(jié)碼文件中,造成文件臃腫
  • 所以我們將所需用到的結(jié)構(gòu)信息記錄在常量池中,并通過引用的方式,來加載、調(diào)用所需的結(jié)構(gòu)
  • 這里的代碼量其實很少了,如果代碼多的話,引用的結(jié)構(gòu)將會更多,這里就需要用到常量池了。

image-20200729145031614

常量池中有什么?

  • 數(shù)量值
  • 字符串值
  • 類引用
  • 字段引用
  • 方法引用

常量池代碼舉例

/**
 * 測試方法區(qū)的內(nèi)部構(gòu)成
 */
public class MethodInnerStrucTest extends Object implements Comparable<String>, Serializable {
    //屬性
    public int num = 10;
    private static String str = '測試方法的內(nèi)部結(jié)構(gòu)';

    //構(gòu)造器沒寫

    //方法
    public void test1() {
        int count = 20;
        System.out.println('count = ' + count);
    }

    public static int test2(int cal) {
        int result = 0;
        try {
            int value = 30;
            result = value / cal;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    public int compareTo(String o) {
        return 0;
    }
}
  • 來看下最簡單的 test1( ) 方法,帶 # 的字節(jié)碼指令,就使用到了常量池的引用
  • 通過字節(jié)碼指令可以看出,拼接字符串時,編譯器幫我們造了個 StringBuilder 對象,然后調(diào)用其 append( ) 方法完成了字符串的拼接
public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: bipush        20
         2: istore_1
         3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: new           #4                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #5                  // Method java/lang/StringBuilder.'<init>':()V
        13: ldc           #6                  // String count =
        15: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: iload_1
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 17: 0
        line 18: 3
        line 19: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcn/sxt/java/MethodInnerStrucTest;
            3      26     1 count   I
  • 常量池
Constant pool:
   #1 = Methodref          #18.#52        // java/lang/Object.'<init>':()V
   #2 = Fieldref           #17.#53        // cn/sxt/java/MethodInnerStrucTest.num:I
   #3 = Fieldref           #54.#55        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Class              #56            // java/lang/StringBuilder
   #5 = Methodref          #4.#52         // java/lang/StringBuilder.'<init>':()V
   #6 = String             #57            // count =
   #7 = Methodref          #4.#58         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #4.#59         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #4.#60         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #61.#62        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #63            // java/lang/Exception
  #12 = Methodref          #11.#64        // java/lang/Exception.printStackTrace:()V
  #13 = Class              #65            // java/lang/String
  #14 = Methodref          #17.#66        // cn/sxt/java/MethodInnerStrucTest.compareTo:(Ljava/lang/String;)I
  #15 = String             #67            // 測試方法的內(nèi)部結(jié)構(gòu)
  #16 = Fieldref           #17.#68        // cn/sxt/java/MethodInnerStrucTest.str:Ljava/lang/String;
  #17 = Class              #69            // cn/sxt/java/MethodInnerStrucTest
  #18 = Class              #70            // java/lang/Object
  #19 = Class              #71            // java/lang/Comparable
  #20 = Class              #72            // java/io/Serializable
  #21 = Utf8               num
  #22 = Utf8               I
  #23 = Utf8               str
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               <init>
  #26 = Utf8               ()V
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               LocalVariableTable
  #30 = Utf8               this
  #31 = Utf8               Lcn/sxt/java/MethodInnerStrucTest;
  #32 = Utf8               test1
  #33 = Utf8               count
  #34 = Utf8               test2
  #35 = Utf8               (I)I
  #36 = Utf8               value
  #37 = Utf8               e
  #38 = Utf8               Ljava/lang/Exception;
  #39 = Utf8               cal
  #40 = Utf8               result
  #41 = Utf8               StackMapTable
  #42 = Class              #63            // java/lang/Exception
  #43 = Utf8               compareTo
  #44 = Utf8               (Ljava/lang/String;)I
  #45 = Utf8               o
  #46 = Utf8               (Ljava/lang/Object;)I
  #47 = Utf8               <clinit>
  #48 = Utf8               Signature
  #49 = Utf8               Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
  #50 = Utf8               SourceFile
  #51 = Utf8               MethodInnerStrucTest.java
  #52 = NameAndType        #25:#26        // '<init>':()V
  #53 = NameAndType        #21:#22        // num:I
  #54 = Class              #73            // java/lang/System
  #55 = NameAndType        #74:#75        // out:Ljava/io/PrintStream;
  #56 = Utf8               java/lang/StringBuilder
  #57 = Utf8               count =
  #58 = NameAndType        #76:#77        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #59 = NameAndType        #76:#78        // append:(I)Ljava/lang/StringBuilder;
  #60 = NameAndType        #79:#80        // toString:()Ljava/lang/String;
  #61 = Class              #81            // java/io/PrintStream
  #62 = NameAndType        #82:#83        // println:(Ljava/lang/String;)V
  #63 = Utf8               java/lang/Exception
  #64 = NameAndType        #84:#26        // printStackTrace:()V
  #65 = Utf8               java/lang/String
  #66 = NameAndType        #43:#44        // compareTo:(Ljava/lang/String;)I
  #67 = Utf8               測試方法的內(nèi)部結(jié)構(gòu)
  #68 = NameAndType        #23:#24        // str:Ljava/lang/String;
  #69 = Utf8               cn/sxt/java/MethodInnerStrucTest
  #70 = Utf8               java/lang/Object
  #71 = Utf8               java/lang/Comparable
  #72 = Utf8               java/io/Serializable
  #73 = Utf8               java/lang/System
  #74 = Utf8               out
  #75 = Utf8               Ljava/io/PrintStream;
  #76 = Utf8               append
  #77 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #78 = Utf8               (I)Ljava/lang/StringBuilder;
  #79 = Utf8               toString
  #80 = Utf8               ()Ljava/lang/String;
  #81 = Utf8               java/io/PrintStream
  #82 = Utf8               println
  #83 = Utf8               (Ljava/lang/String;)V
  #84 = Utf8               printStackTrace

常量池總結(jié)

常量池,可以看做是一張表,虛擬機指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量等信息

運行時常量池

  • 運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。
  • 常量池表(Constant Pool Table)是Class字節(jié)碼文件的一部分,用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。
  • 運行時常量池,在加載類和接口到虛擬機后,就會創(chuàng)建對應的運行時常量池。
  • JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的數(shù)據(jù)項像數(shù)組項一樣,是通過索引訪問的。
  • 運行時常量池中包含多種不同的常量,包括編譯期就已經(jīng)明確的數(shù)值字面量,也包括到運行期解析后才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址
  • 運行時常量池,相對于Class文件常量池的另一重要特征是:具備動態(tài)性。
  • 運行時常量池類似于傳統(tǒng)編程語言中的符號表(symbol table),但是它所包含的數(shù)據(jù)卻比符號表要更加豐富一些。
  • 當創(chuàng)建類或接口的運行時常量池時,如果構(gòu)造運行時常量池所需的內(nèi)存空間超過了方法區(qū)所能提供的最大值,則JVM會拋OutOfMemoryError異常。

5.方法區(qū)使用舉例

public class MethodAreaDemo {
    public static void main(String[] args) {
        int x = 500;
        int y = 100;
        int a = x / y;
        int b = 50;
        System.out.println(a + b);
    }
}

圖解字節(jié)碼指令執(zhí)行流程

  • 字節(jié)碼執(zhí)行過程展示:初始狀態(tài)

image-20200708204750374

  • 首先將操作數(shù)500壓入操作數(shù)棧中

image-20200708204953552

  • 然后將操作數(shù) 500 從操作數(shù)棧中取出,存儲到局部變量表中索引為 1 的位置

image-20200708205029376

  • 將操作數(shù)100壓入操作數(shù)棧中

image-20200729152950703

  • 然后操作數(shù) 100 從操作數(shù)棧中取出,存儲到局部變量表中索引為 2 的位置

image-20200729153015225

  • 讀取本地變量 1 ,壓入操作數(shù)棧

image-20200729153119471

  • 讀取本地變量 2 ,壓入操作數(shù)棧

image-20200729153138678

  • 兩數(shù)相除,計算結(jié)果放在操作數(shù)棧頂,之后執(zhí)行 istore_3 指令,將計算結(jié)果從操作數(shù)棧中彈出,存入本地變量表 3 中

image-20200729153203519

  • 將操作數(shù) 50 壓入操作數(shù)棧

image-20200729153239763

  • 將操作數(shù) 50 從棧頂彈出,保存在局部變量表 4 中

image-20200729153439077

  • 獲取 System.out 輸出流的引用

image-20200729153532555

  • 將本地變量表 3 的值取出,壓入操作數(shù)棧中,準備進行加法運算

image-20200729153919328

  • 將本地變量表 4 的值取出,壓入操作數(shù)棧中,準備進行加法運算

image-20201129113818906

  • 執(zhí)行加法運算后,將計算結(jié)果放在操作數(shù)棧頂

image-20200729154017838

  • 調(diào)用靜態(tài)方法 println( ) ,輸出加法結(jié)果

image-20200729154117269

  • main( ) 方法執(zhí)行結(jié)束

image-20200729154203288

關(guān)于【符號引用 --> 直接引用】的理解

  1. 上面代碼調(diào)用 System.out.println( ) 方法時,首先需要看 System 類有沒有加載,再看看 PrintStream 類有沒有加載
  2. 如果沒有加載,則執(zhí)行加載,執(zhí)行時,將常量池中的符號引用(字面量)轉(zhuǎn)換為直接引用(真正的地址值)

關(guān)于程序計數(shù)器的說明

程序計數(shù)器始終存儲的都是當前字節(jié)碼指令的索引地址,目的是為了方便記錄方法調(diào)用后能夠正常返回,或者是進行了CPU切換后,也能回到原來的代碼繼續(xù)執(zhí)行。

6.方法區(qū)的演進細節(jié)

6.1 永久代演進過程

  • 首先明確:只有Hotspot才有永久代。
  • BEA JRockit、IBMJ9等來說,是不存在永久代的概念的。原則上如何實現(xiàn)方法區(qū)屬于虛擬機實現(xiàn)細節(jié),不受《Java虛擬機規(guī)范》管束,并不要求統(tǒng)一
  • Hotspot中方法區(qū)的變化:
JDK 版本演變細節(jié)
JDK1.6及以前有永久代(permanent generation),靜態(tài)變量存儲在永久代上
JDK1.7有永久代,但已經(jīng)逐步 “去永久代”,字符串常量池、靜態(tài)變量從永久代中移除,保存在堆中
JDK1.8無永久代,類型信息,字段,方法,常量保存在本地內(nèi)存的元空間,但字符串常量池、靜態(tài)變量仍然在堆中。

image-20201129160320158

JDK6

  • 方法區(qū)由永久代實現(xiàn),使用 JVM 虛擬機內(nèi)存

image-20201129121051748

JDK7

  • 方法區(qū)由永久代實現(xiàn),使用 JVM 虛擬機內(nèi)存

image-20201129121111687

JDK8及以后

  • 方法區(qū)由元空間實現(xiàn),使用物理機本地內(nèi)存

image-20201129121131455

6.2 永久代為什么要被元空間替代?

點此進入官方文檔

  • 官方的牽強解釋:JRockit是和HotSpot融合后的結(jié)果,因為JRockit沒有永久代,所以他們不需要配置永久代,HotSpot也就取消了永久代
  • 隨著Java8的到來,HotSpot VM中再也見不到永久代了。但是這并不意味著類的元數(shù)據(jù)信息也消失了。這些數(shù)據(jù)被移到了一個與堆不相連的本地內(nèi)存區(qū)域,這個區(qū)域叫做元空間(Metaspace)。

由于類的元數(shù)據(jù)分配在本地內(nèi)存中,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間,這項改動是很有必要的,原因有:

  • 為永久代設置空間大小是很難確定的。
    • 在某些場景下,如果動態(tài)加載類過多,容易產(chǎn)生Perm區(qū)的OOM。比如某個實際Web工程中,因為功能點比較多,在運行過程中,要不斷動態(tài)地加載很多類,經(jīng)常出現(xiàn)致命錯誤。Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space
    • 而元空間和永久代之間最大的區(qū)別在于:元空間并不在虛擬機中,而是使用本地內(nèi)存。因此,默認情況下,元空間的大小僅受本地內(nèi)存限制
  • 對永久代進行調(diào)優(yōu)是很困難的。
    • 方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再用的類型,方法區(qū)的調(diào)優(yōu)主要是為了降低Full GC
    • 有些人認為方法區(qū)(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機在方法區(qū)中實現(xiàn)垃圾收集。事實上也確實有未實現(xiàn)或未能完整實現(xiàn)方法區(qū)類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
    • 一般來說這個區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區(qū)域的回收有時又確實是必要的。以前Sun公司的Bug列表中,曾出現(xiàn)過的若干個嚴重的Bug就是由于低版本的HotSpot虛擬機對此區(qū)域未完全回收而導致內(nèi)存泄漏

6.3 字符串常量池

字符串常量池 StringTable 為什么要調(diào)整位置?

  • JDK7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在Full GC的時候才會執(zhí)行永久代的垃圾回收,而Full GC是老年代的空間不足、永久代不足時才會觸發(fā)。
  • 這就導致StringTable回收效率不高,而我們開發(fā)中會有大量的字符串被創(chuàng)建,回收效率低,導致永久代內(nèi)存不足。放到堆里,能及時回收內(nèi)存。

6.4 靜態(tài)變量位置

靜態(tài)變量存放在哪里?

/**
 * 結(jié)論:
 *  靜態(tài)變量在jdk6/7存在與永久代中,在jdk8存在于堆中 //private static byte[] arr
 *  靜態(tài)引用對應的對象實體始終都存在堆空間 //new byte[1024 * 1024 * 100];
 *
 * jdk7:
 * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
 * jdk 8:
 * -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
 */
public class StaticFieldTest {
    private static byte[] arr = new byte[1024 * 1024 * 100]; //100MB

    public static void main(String[] args) {
        System.out.println(StaticFieldTest.arr);
    }
}
  • 設置JVM參數(shù)
-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
  • 通過 GC 日志可以看出:靜態(tài)變量引用對應的對象實體始終都在堆空間中(arr 數(shù)組對象直接懟到老年區(qū)去了)

image-20201129161627002

代碼示例2

/**
 * 《深入理解Java虛擬機》中的案例:
 * staticObj、instanceObj、localObj存放在哪里?
 */
public class StaticObjTest {
    static class Test {
        //靜態(tài)屬性
        static ObjectHolder staticObj = new ObjectHolder();
        //非靜態(tài)屬性
        ObjectHolder instanceObj = new ObjectHolder();

        void foo() {
            //局部變量
            ObjectHolder localObj = new ObjectHolder();
            System.out.println('done');
        }
    }

    private static class ObjectHolder {
    }

    public static void main(String[] args) {
        Test test = new StaticObjTest.Test();
        test.foo();
    }
}
  • 可以使用 JHSDB.exe,在JDK9的時候才引入的
  • 分析:staticObj隨著Test的類型信息存放在方法區(qū),instanceObj隨著Test的對象實例存放在Java堆,localObject則是存放在foo( )方法棧幀的局部變量表中。
  • 測試發(fā)現(xiàn):三個對象的數(shù)據(jù)在內(nèi)存中的地址都落在Eden區(qū)范圍內(nèi),所以結(jié)論:只要是對象實例必然會在Java堆中分配。

image-20200708215025527

  • 接著,找到了一個引用該staticObj對象的地方,是在一個java.lang.Class的實例里,并且給出了這個實例的地址,通過Inspector查看該對象實例,可以清楚看到這確實是一個java.lang.Class類型的對象實例,里面有一個名為staticobj的實例字段:

image-20200708215218078

  • 從《Java虛擬機規(guī)范》所定義的概念模型來看,所有Class相關(guān)的信息都應該存放在方法區(qū)之中,但方法區(qū)該如何實現(xiàn),《Java虛擬機規(guī)范》并未做出規(guī)定,這就成了一件允許不同虛擬機自己靈活把握的事情。JDK7及其以后版本的HotSpot虛擬機選擇把靜態(tài)變量與類型在Java語言一端的映射Class對象存放在一起,存儲于Java堆之中,從我們的實驗中也明確驗證了這一點

7.方法區(qū)的垃圾回收

方法區(qū)的垃圾收集

  • 有些人認為方法區(qū)(如Hotspot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。
  • 《Java虛擬機規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機在方法區(qū)中實現(xiàn)垃圾收集。事實上也確實有未實現(xiàn)或未能完整實現(xiàn)方法區(qū)類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
  • 一般來說這個區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區(qū)域的回收有時又確實是必要的。以前sun公司的Bug列表中,曾出現(xiàn)過的若干個嚴重的Bug就是由于低版本的HotSpot虛擬機對此區(qū)域未完全回收而導致內(nèi)存泄漏。
  • 方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再使用的類型

7.1 方法區(qū)常量的回收

  • 先來說說方法區(qū)內(nèi)常量池之中主要存放的兩大類常量:字面量和符號引用
    • 字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等
    • 而符號引用則屬于編譯原理方面的概念,包括下面三類常量:
      • 類和接口的全限定名
      • 字段的名稱和描述符
      • 方法的名稱和描述符
  • HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收
  • 回收廢棄常量與回收Java堆中的對象非常類似。(關(guān)于常量的回收比較簡單,重點是類的回收)

7.2 方法區(qū)類的回收

  • 判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
    • 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
    • 加載該類的類加載器已經(jīng)被回收,這個條件除非是經(jīng)過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
    • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  • Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關(guān)于是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數(shù)進行控制,還可以使用-verbose:class 以及 -XX:+TraceClass-Loading-XX:+TraceClassUnLoading查看類加載和卸載信息
  • 在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架,動態(tài)生成JSP以及 OSGi 這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區(qū)造成過大的內(nèi)存壓力。

8.運行時數(shù)據(jù)區(qū)總結(jié)

  • 線程私有結(jié)構(gòu):程序計數(shù)器、虛擬機棧、本地方法棧
  • 每個虛擬機棧由具體的棧幀組成,在棧幀的動態(tài)鏈接中,保存至對方法的引用
  • 方法區(qū)在 JDK7 之前,使用永久代實現(xiàn),在 JDK8 之后,使用元空間實現(xiàn)
  • Minor GC 針對于新生區(qū),Major GC 針對于老年區(qū),F(xiàn)ull GC 針對于整個堆空間和方法區(qū)

第09章_方法區(qū)與棧的關(guān)聯(lián)結(jié)構(gòu)

9.大廠面試題

百度

  1. 三面:說一下JVM內(nèi)存模型吧,有哪些區(qū)?分別干什么的?

字節(jié)跳動

  1. 二面:Java的內(nèi)存分區(qū)
  2. 二面:講講vm運行時數(shù)據(jù)庫區(qū)
  3. 什么時候?qū)ο髸M入老年代?

螞蟻金服

  1. Java8的內(nèi)存分代改進
  2. JVM內(nèi)存分哪幾個區(qū),每個區(qū)的作用是什么?
  3. 一面:JVM內(nèi)存分布/內(nèi)存結(jié)構(gòu)?棧和堆的區(qū)別?堆的結(jié)構(gòu)?為什么兩個survivor區(qū)?
  4. 二面:Eden和survior的比例分配

小米

  1. jvm內(nèi)存分區(qū),為什么要有新生代和老年代

京東

  1. JVM的內(nèi)存結(jié)構(gòu),Eden和Survivor比例。
  2. JVM內(nèi)存為什么要分成新生代,老年代,持久代。新生代中為什么要分為Eden和survivor。

天貓

  1. 一面:Jvm內(nèi)存模型以及分區(qū),需要詳細到每個區(qū)放什么。
  2. 一面:JVM的內(nèi)存模型,Java8做了什么改

拼多多

  1. JVM內(nèi)存分哪幾個區(qū),每個區(qū)的作用是什么?

美團

  1. java內(nèi)存分配
  2. jvm的永久代中會發(fā)生垃圾回收嗎?
  3. 一面:jvm內(nèi)存分區(qū),為什么要有新生代和老年代?

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    国产成人亚洲精品青草天美| 国产原创激情一区二区三区| 欧美小黄片在线一级观看| 91欧美亚洲精品在线观看| 欧美精品一区二区三区白虎| 久久天堂夜夜一本婷婷| 偷拍美女洗澡免费视频| 少妇人妻精品一区二区三区| 国产一区二区不卡在线播放| 色播五月激情五月婷婷| 亚洲av首页免费在线观看| 欧美激情一区二区亚洲专区| 人妻精品一区二区三区视频免精 | 午夜精品在线观看视频午夜| 国产精品久久男人的天堂| 色无极东京热男人的天堂| 国产精品美女午夜视频| 日韩欧美国产精品自拍| 国产精品美女午夜福利| 国产高清视频一区不卡| 欧美整片精品日韩综合| 日韩一区二区三区高清在| 国产精品自拍杆香蕉视频| 日韩特级黄片免费在线观看| 91日韩欧美中文字幕| 久久精品伊人一区二区| 高清一区二区三区不卡免费| 日本欧美三级中文字幕| 色婷婷国产熟妇人妻露脸| 国产又大又硬又粗又湿| 神马午夜福利免费视频| 中文字幕熟女人妻视频| 麻豆视传媒短视频免费观看| 国产传媒欧美日韩成人精品| 日本午夜免费啪视频在线| 欧洲日韩精品一区二区三区| 91精品国产品国语在线不卡| 中日韩美一级特黄大片| 日韩成人动作片在线观看| 欧美精品日韩精品一区| 五月天丁香婷婷一区二区|