欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

浅谈JVM - 内存结构(六)- 方法区

程序员文章站 2022-03-12 23:19:48
...

 

注:本文虽然讨论的是方法区,但是为了解答某些问题也涉及到了Class文件结构和类加载的相关知识
java7及以前,方法区的实现是永久代,java8以后,方法区的实现是元空间

6.1 定义

  • 方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。
  • 方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。

6.2 组成

浅谈JVM - 内存结构(六)- 方法区

6.3 方法区内存溢出

java7及以前,使用-XX:MaxPermSize限制永久代的最大内存,当加载大量的类时,会抛出java.lang.OutOfMemoryError:PermGen space异常,永久代内存不足时会触发FullGC

java8以后,使用-XX:MaxMetaspaceSize限制元空间的最大内存,当加载大量的类时,会抛出java.lang.OutOfMemoryError:Metaspace异常,元空间内存不足时会触发FullGC

示例代码(采用jdk1.8,增加限制-XX:MaxMetaspaceSize=100m

public class MetaspaceDemo extends ClassLoader{

    public static void main(String[] args) {
        // 类持有
        List<Class<?>> classes = new ArrayList<Class<?>>();
        // 循环1000w次生成1000w个不同的类。
        for (int i = 0; i < 10000000; ++i) {
            ClassWriter cw = new ClassWriter(0);
            // 定义一个类名称为Class{i},它的访问域为public,父类为java.lang.Object,不实现任何接口
            cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
                    "java/lang/Object", null);
            // 定义构造函数<init>方法
            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
                    "()V", null, null);
            // 第一个指令为加载this
            mw.visitVarInsn(Opcodes.ALOAD, 0);
            // 第二个指令为调用父类Object的构造函数
            mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
                    "<init>", "()V", false);
            // 第三条指令为return
            mw.visitInsn(Opcodes.RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();

            MetaspaceDemo test = new MetaspaceDemo();
            byte[] code = cw.toByteArray();
            // 定义类
            Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
            classes.add(exampleClass);
        }
    }

}

输出

Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at com.loksail.learndemo.MetaspaceDemo.main(Metaspace.java:26)

和我们预想的Metaspace异常不一致,这是因为在jdk8中默认开启了类指针压缩,也就是-XX:+useCompressedClassPointers,其会压缩对象头中_klass指针,同时会在元空间中开启一片空间(Compressed Class Space)用来存储类的元信息和虚方法表等,默认大小是1G,可以通过-XX:CompressedClassSpaceSize=100m调节,在这里我们选择关闭掉这个功能,运行参数中加入-XX:-useCompressedClassPointers

运行结果

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at com.loksail.learndemo.memory.MetaspaceDemo.main(MetaspaceDemo.java:43)

6.4 运行时常量池

通过javap反编译class二进制字节码文件,我们可以看到里面的组成部分有

public class Demo {

    private final static int count = 10;

    public static void main(String[] args) {
        String test = "Hell World";
        int days = 20;
        System.out.println(days + test + count);
    }

}
  • 类的基本信息

    Classfile /D:/JWF/Gitee/learn-demo/src/main/java/com/loksail/learndemo/Demo.class
    Last modified 2019-12-20; size 689 bytes
    MD5 checksum e4cd8355bc1aa4e1e9d2b2c65a5ede87
    Compiled from "Demo.java"
    public class com.loksail.learndemo.Demo
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    
  • class常量池(The Constant Pool)

    Constant pool:
     #1 = Methodref          #11.#24        // java/lang/Object."<init>":()V
     #2 = String             #25            // Hell World
     #3 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
     #4 = Class              #28            // java/lang/StringBuilder
     #5 = Methodref          #4.#24         // java/lang/StringBuilder."<init>":()V
     #6 = Methodref          #4.#29         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
     #7 = Methodref          #4.#30         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     #8 = Class              #31            // com/loksail/learndemo/Demo
     #9 = Methodref          #4.#32         // java/lang/StringBuilder.toString:()Ljava/lang/String;
    #10 = Methodref          #33.#34        // java/io/PrintStream.println:(Ljava/lang/String;)V
    #11 = Class              #35            // java/lang/Object
    #12 = Utf8               count
    #13 = Utf8               I
    #14 = Utf8               ConstantValue
    #15 = Integer            10
    #16 = Utf8               <init>
    #17 = Utf8               ()V
    #18 = Utf8               Code
    #19 = Utf8               LineNumberTable
    #20 = Utf8               main
    #21 = Utf8               ([Ljava/lang/String;)V
    #22 = Utf8               SourceFile
    #23 = Utf8               Demo.java
    #24 = NameAndType        #16:#17        // "<init>":()V
    #25 = Utf8               Hell World
    #26 = Class              #36            // java/lang/System
    #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
    #28 = Utf8               java/lang/StringBuilder
    #29 = NameAndType        #39:#40        // append:(I)Ljava/lang/StringBuilder;
    #30 = NameAndType        #39:#41        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    #31 = Utf8               com/loksail/learndemo/Demo
    #32 = NameAndType        #42:#43        // toString:()Ljava/lang/String;
    #33 = Class              #44            // java/io/PrintStream
    #34 = NameAndType        #45:#46        // println:(Ljava/lang/String;)V
    #35 = Utf8               java/lang/Object
    #36 = Utf8               java/lang/System
    #37 = Utf8               out
    #38 = Utf8               Ljava/io/PrintStream;
    #39 = Utf8               append
    #40 = Utf8               (I)Ljava/lang/StringBuilder;
    #41 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
    #42 = Utf8               toString
    #43 = Utf8               ()Ljava/lang/String;
    #44 = Utf8               java/io/PrintStream
    #45 = Utf8               println
    #46 = Utf8               (Ljava/lang/String;)V
    
  • 类方法定义(包含了jvm指令)

    {
    public com.loksail.learndemo.Demo();
      descriptor: ()V
      flags: ACC_PUBLIC
      Code:
        stack=1, locals=1, args_size=1
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
        LineNumberTable:
          line 6: 0
    
    public static void main(java.lang.String[]);
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=3, locals=3, args_size=1
           0: ldc           #2                  // String Hell World
           2: astore_1
           3: bipush        20
           5: istore_2
           6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
           9: new           #4                  // class java/lang/StringBuilder
          12: dup
          13: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
          16: iload_2
          17: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
          20: aload_1
          21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          24: bipush        10
          26: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
          29: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
          32: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          35: return
        LineNumberTable:
          line 11: 0
          line 12: 3
          line 13: 6
          line 14: 35
    }
    

6.4.1 Class文件常量池

Class 文件常量池指的是编译生成的 class 字节码文件,其结构中有一项是常量池(Constant Pool),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

6.4.1.1 字面量和符号引用

案例

public class Demo01 {

    private int field1;
    private Instan field2;

    private float m1 = 10.1f;
    private double m2 = 10.2;
    private long m3 = 103;
    private int a = 10;
    private static int b = 20;
    private static final long c = 10000L;
    private static final double d = 5.5;
    private static final int e = 5;
    private static final short f = 6;
    private static final boolean g = false;
    private static final boolean h = true;
    private static final char i = 'k';
    private static final byte j = 7;
    private static final float k = 6.6f;
    private static final Instan instan = new Instan();
    private String str = "Hello World";

    public void method1(String m1) {
        method3();
    }

    public static int method2(int m2) {
        method4();
        return 0;
    }

    public String method3() {
        return null;
    }

    public static final Long method4() {
        return null;
    }


    public static void main(String[] args) throws IOException {
        String string = "Hello Method";
        System.in.read();
    }
}

class Instan{

}

字节码文件

Classfile /D:/JWF/Gitee/learn-demo/src/main/java/com/loksail/learndemo/demo/Demo01.class
  Last modified 2019-12-26; size 1531 bytes
  MD5 checksum 06e8b620afef54675919d0162c48ec90
  Compiled from "Demo01.java"
public class com.loksail.learndemo.demo.Demo01
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
    #1 = Methodref          #23.#83       // java/lang/Object."<init>":()V
    #2 = Float              10.1f
    #3 = Fieldref           #22.#84       // com/loksail/learndemo/demo/Demo01.m1:F
    #4 = Double             10.2d
    #6 = Fieldref           #22.#85       // com/loksail/learndemo/demo/Demo01.m2:D
    #7 = Long               103l
    #9 = Fieldref           #22.#86       // com/loksail/learndemo/demo/Demo01.m3:J
   #10 = Fieldref           #22.#87       // com/loksail/learndemo/demo/Demo01.a:I
   #11 = String             #88           // Hello World
   #12 = Fieldref           #22.#89       // com/loksail/learndemo/demo/Demo01.str:Ljava/lang/String;
   #13 = Methodref          #22.#90       // com/loksail/learndemo/demo/Demo01.method3:()Ljava/lang/String;
   #14 = Methodref          #22.#91       // com/loksail/learndemo/demo/Demo01.method4:()Ljava/lang/Long;
   #15 = String             #92           // Hello Method
   #16 = Fieldref           #93.#94       // java/lang/System.in:Ljava/io/InputStream;
   #17 = Methodref          #95.#96       // java/io/InputStream.read:()I
   #18 = Fieldref           #22.#97       // com/loksail/learndemo/demo/Demo01.b:I
   #19 = Class              #98           // com/loksail/learndemo/demo/Instan
   #20 = Methodref          #19.#83       // com/loksail/learndemo/demo/Instan."<init>":()V
   #21 = Fieldref           #22.#99       // com/loksail/learndemo/demo/Demo01.instan:Lcom/loksail/learndemo/demo/Instan;
   #22 = Class              #100          // com/loksail/learndemo/demo/Demo01
   #23 = Class              #101          // java/lang/Object
   #24 = Utf8               field1
   #25 = Utf8               I
   #26 = Utf8               field2
   #27 = Utf8               Lcom/loksail/learndemo/demo/Instan;
   #28 = Utf8               m1
   #29 = Utf8               F
   #30 = Utf8               m2
   #31 = Utf8               D
   #32 = Utf8               m3
   #33 = Utf8               J
   #34 = Utf8               a
   #35 = Utf8               b
   #36 = Utf8               c
   #37 = Utf8               ConstantValue
   #38 = Long               10000l
   #40 = Utf8               d
   #41 = Double             5.5d
   #43 = Utf8               e
   #44 = Integer            5
   #45 = Utf8               f
   #46 = Utf8               S
   #47 = Integer            6
   #48 = Utf8               g
   #49 = Utf8               Z
   #50 = Integer            0
   #51 = Utf8               h
   #52 = Integer            1
   #53 = Utf8               i
   #54 = Utf8               C
   #55 = Integer            107
   #56 = Utf8               j
   #57 = Utf8               B
   #58 = Integer            7
   #59 = Utf8               k
   #60 = Float              6.6f
   #61 = Utf8               instan
   #62 = Utf8               str
   #63 = Utf8               Ljava/lang/String;
   #64 = Utf8               <init>
   #65 = Utf8               ()V
   #66 = Utf8               Code
   #67 = Utf8               LineNumberTable
   #68 = Utf8               method1
   #69 = Utf8               (Ljava/lang/String;)V
   #70 = Utf8               method2
   #71 = Utf8               (I)I
   #72 = Utf8               method3
   #73 = Utf8               ()Ljava/lang/String;
   #74 = Utf8               method4
   #75 = Utf8               ()Ljava/lang/Long;
   #76 = Utf8               main
   #77 = Utf8               ([Ljava/lang/String;)V
   #78 = Utf8               Exceptions
   #79 = Class              #102          // java/io/IOException
   #80 = Utf8               <clinit>
   #81 = Utf8               SourceFile
   #82 = Utf8               Demo01.java
   #83 = NameAndType        #64:#65       // "<init>":()V
   #84 = NameAndType        #28:#29       // m1:F
   #85 = NameAndType        #30:#31       // m2:D
   #86 = NameAndType        #32:#33       // m3:J
   #87 = NameAndType        #34:#25       // a:I
   #88 = Utf8               Hello World
   #89 = NameAndType        #62:#63       // str:Ljava/lang/String;
   #90 = NameAndType        #72:#73       // method3:()Ljava/lang/String;
   #91 = NameAndType        #74:#75       // method4:()Ljava/lang/Long;
   #92 = Utf8               Hello Method
   #93 = Class              #103          // java/lang/System
   #94 = NameAndType        #104:#105     // in:Ljava/io/InputStream;
   #95 = Class              #106          // java/io/InputStream
   #96 = NameAndType        #107:#108     // read:()I
   #97 = NameAndType        #35:#25       // b:I
   #98 = Utf8               com/loksail/learndemo/demo/Instan
   #99 = NameAndType        #61:#27       // instan:Lcom/loksail/learndemo/demo/Instan;
  #100 = Utf8               com/loksail/learndemo/demo/Demo01
  #101 = Utf8               java/lang/Object
  #102 = Utf8               java/io/IOException
  #103 = Utf8               java/lang/System
  #104 = Utf8               in
  #105 = Utf8               Ljava/io/InputStream;
  #106 = Utf8               java/io/InputStream
  #107 = Utf8               read
  #108 = Utf8               ()I
{
  public com.loksail.learndemo.demo.Demo01();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // float 10.1f
         7: putfield      #3                  // Field m1:F
        10: aload_0
        11: ldc2_w        #4                  // double 10.2d
        14: putfield      #6                  // Field m2:D
        17: aload_0
        18: ldc2_w        #7                  // long 103l
        21: putfield      #9                  // Field m3:J
        24: aload_0
        25: bipush        10
        27: putfield      #10                 // Field a:I
        30: aload_0
        31: ldc           #11                 // String Hello World
        33: putfield      #12                 // Field str:Ljava/lang/String;
        36: return
      LineNumberTable:
        line 12: 0
        line 17: 4
        line 18: 10
        line 19: 17
        line 20: 24
        line 32: 30

  public void method1(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_0
         1: invokevirtual #13                 // Method method3:()Ljava/lang/String;
         4: pop
         5: return
      LineNumberTable:
        line 35: 0
        line 36: 5

  public static int method2(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: invokestatic  #14                 // Method method4:()Ljava/lang/Long;
         3: pop
         4: iconst_0
         5: ireturn
      LineNumberTable:
        line 39: 0
        line 40: 4

  public java.lang.String method3();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 44: 0

  public static final java.lang.Long method4();
    descriptor: ()Ljava/lang/Long;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=1, locals=0, args_size=0
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 48: 0

  public static void main(java.lang.String[]) throws java.io.IOException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #15                 // String Hello Method
         2: astore_1
         3: getstatic     #16                 // Field java/lang/System.in:Ljava/io/InputStream;
         6: invokevirtual #17                 // Method java/io/InputStream.read:()I
         9: pop
        10: return
      LineNumberTable:
        line 53: 0
        line 54: 3
        line 55: 10
    Exceptions:
      throws java.io.IOException

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        20
         2: putstatic     #18                 // Field b:I
         5: new           #19                 // class com/loksail/learndemo/demo/Instan
         8: dup
         9: invokespecial #20                 // Method com/loksail/learndemo/demo/Instan."<init>":()V
        12: putstatic     #21                 // Field instan:Lcom/loksail/learndemo/demo/Instan;
        15: return
      LineNumberTable:
        line 21: 0
        line 31: 5
}
SourceFile: "Demo01.java"
  • 字面量

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。

这里的字面量是指字符串字面量声明为 final 的八大基本数据类型的值非final的float,double,long数据类型的值等,基于上面的常量池

  • 字符串字面量对应的有

    private String str = "Hello World"对应的字面量是#88的Hello World

    String string = "Hello Method"对应的字面量是#92的Hello Method

  • 声明为 final 的八大基本数据类型值

    private static final long c = 10000L 对应的字面量是#38的10000l

    private static final double d = 5.5 对应的字面量是#41的5.5d

    private static final int e = 5 对应的字面量是#44的5

    private static final short f = 6 对应的字面量是#47的6

    private static final boolean g = false 对应的字面量是#50的0

    private static final boolean h = true 对应的字面量是#52的1

    private static final char i = 'k' 对应的字面量是#55的107

    private static final byte j = 7 对应的字面量是#58的7

    private static final float k = 6.6f对应的字面量是#60的6.6f

  • 非final的float,double,long数据类型的值

    private float m1 = 10.1f 对应的字面量是#2的10.1f

    private double m2 = 10.2 对应的字面量是#4的10.2d

    private long m3 = 103 对应的字面量是#7的103l

    上面标红的值就是字面量,可以看出byte、char、 short、boolean类型的值在常量池中均是以Integer类型存储,同时字符串字面量包括成员变量中和局部变量中的字面量,而基本数据类型的字面量仅是成员变量中字面量

  • 符号引用:符号引用属于编译原理方面的概念,是相对于直接引用来说的,主要包括了下面三类常量:

  1. 类和接口的全限定名(Fully Qualified Name)

    • 接口的全限定名对应#101的java/lang/Object
    • 类的全限定名对应#100的com/loksail/learndemo/demo/Demo01
  2. 方法的名称和描述符(Descriptor)

    <init>方法是instance实例构造器,对非静态变量解析初始化(非静态成员变量初始化、构造方法、非静态代码块这三者会在编译期按顺序合并到<init>方法中),而<clinit>方法是class类构造器对静态变量进行初始化(非final静态成员变量初始化、静态代码块这两者会在编译期按顺序合并到<clinit>方法中)

    • 方法的名称和描述符分别对应的符号引用是#64的<init>和#65的()V

    • 方法的名称对应的符号引用是#76的<clinit>

    • main方法的名称对应的符号引用是#76的<clinit>和#77的([Ljava/lang/String;)V

    • method1方法的名称和描述符分别对应的符号引用是#68的method1和#69的(Ljava/lang/String;)V

    • method2方法的名称和描述符分别对应的符号引用是#70的method2和#71的(I)I

    • method3方法的名称和描述符分别对应的符号引用是#72的method3和#73的()Ljava/lang/String;

    • method4方法的名称和描述符分别对应的符号引用是#74的method4和#75的 ()Ljava/lang/Long;

    • main方法的名称和描述符分别对应的符号引用是#76的main和#77的([Ljava/lang/String;)V

  3. 字段的名称和描述符

    • field1字段的名称和描述符分别对应的符号引用是#24的field1和#25的I

    • field2字段的名称和描述符分别对应的符号引用是#26的field2和#27的Lcom/loksail/learndemo/demo/Instan;

    • m1字段的名称和描述符分别对应的符号引用是#28的m1和#29的F

    • m2字段的名称和描述符分别对应的符号引用是#30的m2和#31的D

    • m3字段的名称和描述符分别对应的符号引用是#32的m3和#33的J

    • a字段的名称和描述符分别对应的符号引用是#34的a和#25的I

    • b字段的名称和描述符分别对应的符号引用是#35的b和#25的I

    • c字段的名称和描述符分别对应的符号引用是#36的c和#33的J

    • d字段的名称和描述符分别对应的符号引用是#40的d和#31的D

    • e字段的名称和描述符分别对应的符号引用是#43的e和#25的I

    • f字段的名称和描述符分别对应的符号引用是#45的f和#46的S

    • g字段的名称和描述符分别对应的符号引用是#48的g和#49的Z

    • h字段的名称和描述符分别对应的符号引用是#51的h和#49的Z

    • i字段的名称和描述符分别对应的符号引用是#53的i和#57的B

    • j字段的名称和描述符分别对应的符号引用是#56的j和#25的I

    • k字段的名称和描述符分别对应的符号引用是#59的k和#29的F

    • instan字段的名称和描述符分别对应的符号引用是#61的instan和#27的Lcom/loksail/learndemo/demo/Instan;

    • str字段的名称和描述符分别对应的符号引用是#62的str和#63的Ljava/lang/String;

    上面标红的值就是符号引用

6.4.1.2 符号引用的作用

  • 类和接口的全限定名以及字段的名称和描述的符号引用,主要用于为成员变量的初始化自定义值。

    例如字节码文件中216行中的2: putstatic #18 // Field b:I,通过常量项关联关系可以获得 #2 -> #22.#84 -> #100. #28:#29 -> com/loksail/learndemo/demo/Demo01.m1:F,通过类的全限定名获取到类的ClassClass对象(类加载的解析阶段会将一个类的符号引用变为指向InstanceKlass对象的直接指针),再根据字段名称和描述符,到ClassClass对象上记录的字段列表找到匹配的字段指针(其位置是相对偏移量)写回到常量池项#2里,将215行的0: bipush 20压入操作数栈栈顶的数值20 弹栈,并为找到的字段指针b初始化自定义值20

    1. 静态成员变量的初始化是在类加载到内存时,在类加载的准备阶段时,会先为静态成员变量初始化默认值,如private static int a = 1;会在此时为a初始化默认值0,然后在类加载的初始化阶段,会为静态成员变量初始化自定义值,如private static int a = 1;会在此时为a初始化自定义值1

    2. 实例成员变量的初始化是在创建对象时,先会执行JVM的new指令,从方法区内对实例变量的定义拷贝一份到堆区,为成员变量初始化默认值,如private int a = 1;会在此时为a初始化默认值0;然后再调用实例构造器的<init>为成员变量初始化自定义值,如private int a = 1;会在此时为a初始化自定义值1

    2. 对于final static的基本数据类型的值,其仅在类加载的准备阶段,就直接为成员变量初始化自定义的值,在类加载的初始化阶段不可以再赋值了。

    3. 静态成员变量的初始化自定义值在类加载的初始化阶段的<clinit>方法中,在字节码中对应

    static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: bipush        20
             2: putstatic     #18                 // Field b:I
             5: new           #19                 // class com/loksail/learndemo/demo/Instan
             8: dup
             9: invokespecial #20                 // Method com/loksail/learndemo/demo/Instan."<init>":()V
            12: putstatic     #21                 // Field instan:Lcom/loksail/learndemo/demo/Instan;
            15: return
          LineNumberTable:
            line 21: 0
          line 31: 5
    

    4. 实例成员变量的初始化自定义值在类加载的初始化阶段的<init>方法中,在字节码文件中对应

      public com.loksail.learndemo.demo.Demo01();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: ldc           #2                  // float 10.1f
             7: putfield      #3                  // Field m1:F
            10: aload_0
            11: ldc2_w        #4                  // double 10.2d
            14: putfield      #6                  // Field m2:D
            17: aload_0
            18: ldc2_w        #7                  // long 103l
            21: putfield      #9                  // Field m3:J
            24: aload_0
            25: bipush        10
            27: putfield      #10                 // Field a:I
            30: aload_0
            31: ldc           #11                 // String Hello World
            33: putfield      #12                 // Field str:Ljava/lang/String;
          36: return
    
  • 类和接口的全限定名以及方法的名称和描述的符号引用,主要用于为方法调用提供连接(静态解析和动态连接)。

    例如字节码文件中152行中的1: invokevirtual #13 // Method method3:()Ljava/lang/String;,通过常量项关联关系可以获得 #13 -> #22.#90 -> #100. #72:#73 -> com/loksail/learndemo/demo/Demo01.method3:()Ljava/lang/String;,通过类的全限定名可以获取到类的ClassClass对象(类加载的解析阶段会将一个类的符号引用变为指向InstanceKlass对象的直接指针),再根据方法名称和描述符到ClassClass对象上记录的方法列表找到匹配的方法内存地址,最终把找到的方法内存地址写回到常量池项#13里,这就是将符号引用转换成直接引用,再进行方法调用。

    字节码中的方法调用就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化成直接引用,这种转换称为静态解析。另外一部分将在每一次运行期间转换为直接引用,这部分称为动态连接。

6.4.1.3 符号引用转换成直接引用

Class文件在编译阶段并不能确定类或接口、字段和方法在内存中的位置,所以类或接口、字段和方法调用在Class文件里面存储的都只是符号引用,有一部分符号引用在类加载的解析阶段就会把符号引用转换成直接引用(如当前加载的类/接口符号引用、字段符号引用、符合静态解析方法的符号引用),有一部分则是在运行阶段才会将符号引用转换成直接引用(如当前类定义的其他类/接口符号引用,符合动态连接的方法的符号引用)。

类加载的解析阶段是将符号引用替换为直接引用,解析动作针对类或接口,字段,类或接口的方法进行解析。首先是用类加载器加载这个类。在加载的过程中逐步解析类中的字段和方法。

符号引用是以字面量的形式明确定义在常量池中,直接引用是指向目标的指针,或者相对偏移量。

  • 类的解析: 类的解析是将一个类的符号引用变为指向InstanceKlass对象的直接指针。当创建对象的时候,这个指针会赋值给对象头中_kclass指针。 这样就定位到了该类的元数据信息。访问类的元数据信息,是通过描述该类的InstanceKlass对象实现的,当然每个类只对应一个InstanceKlass对象。这就是类本身如何被描述的内存形态。因为对象内部的数据在内存中的连续堆放的,当你访问一个类的某字段,是需要通过元数据InstanceKlass对象来记录这个字段的与对象头的偏移量来获取。 当然调用对象的方法是定位到InstanceKlass对象的方法表而不是定位到对象的内存区域。创建对象其实就是仅仅向一块内存区域写入与类元数据对应的各种字段的值。访问一个对象的字段的值, 是通过定位这个字段在这个对象起始地址的相对偏移量。确定相对偏移量就是在字段解析阶段完成的。

  • 字段的解析: 字段的解析是确定一个对象的字段的访问地址,是计算相对对象起始地址的偏移量。

    • 要解析一个字段符号引用,首先要解析其所述的类引用符号CONSTANT_CLASS_INFO,即类元信息地址,然后再解析类中的字段。

    • 由于字段解析得到的相对偏移量加上额外元数据比原本的constant pool index要宽,没办法放在原本的constant pool里,所以HotSpot VM有另外一个叫做constant pool cache的东西来存放它们。

      getfield cp#12(cp表示constant pool)

      就会被解析成

      fast_bgetfield cpc#5 // (offset: +40, type: boolean, ...)(这里用cpc#5来表示constant pool cache的第5项的意思) 于是解析后偏移量信息就记录在了constant pool cache里,getfield根据解析出来的constant pool cache entry里记录的类型信息被改写为对应类型的版本的字节码fast_bgetfield来避免以后每次都去解析一次,然后fast_bgetfield就可以根据偏移量信息以正确的类型来访问字段了。

    • 静态变量字段存储在Class对象中,其偏移量是相对Class对象而言的,而实例成员变量字段是存储在实例对象中,其偏移量是相对实例对象对象头的。

  • 方法的解析:

    • 一部分方法调用的符号引用在类加载的解析阶段就被转换成直接引用,其是根据ClassClass对象(类的元数据信息)中的Array<Method*>*类型的_methods数组,从数组中获取与方法名称和描述符匹配的 methodblock指针,并将此方法指针替换常量表中对应的methodref类型的项的符号引用(若当前类不存在匹配的方法,则继续从父类查找,这里跟字段解析一样,解析得到的方法指针会被存在constant pool cache中,以后的调用都会直接从cpc中获取直接引用),invokestatic或invokespecial指令调用这些方法时,根据直接引用找到对应的方法指针,并执行方法,这部分就是静态解析(非虚方法,包括静态方法、私有方法、实例构造方法、super调用的父类方法,final方法)。

    • 另一部分方法调用的符号引用则是在每一次运行期间转换成直接引用。

      • 当执行invokevirtual指令调用方法时,指令参数为该方法的符号引用,如invokevitual #2,此时JVM会发现该指令尚未被解析(resolve),所以会先去解析一下,通过其操作数所记录的常量池下标0x0002,找到常量池项#2,通过常量表中的methodref类型的项的引用关系而取到对应的值。解析methodref类型的项的过程通过该项的class_index项找到类信息,通过name_and_type_index项找到方法名和方法描述符,然后在ClassClass对象中找到虚方法表(Array<int>*类型的default_vtable_indices),根据方法名称和描述符找到对应指向匹配方法的下标,该下标指向methodblock*指针,也就是对应的方法内存地址入口,然后用虚方法表的下标和参数个数来取代之前的符号引用。也就是说符号引用变成了虚方法表的下标。这个下标就是一种直接引用的体现。 类的符号引用--> instanceKlass --> vtable_index --> methodblock指针

      • 解析好常量池项#2之后回到invokevirtual指令的解析,这时候invokevirtual指令会变成invokevirtual_quick, 该指令的参数为虚方法表的下标(vtable index)和 方法的参数个数,如invokevirtual_quick vtable_index=6, args_size=1。 所以调用方法并不是直接调用方法块,而是先找到虚方法表,再去根据下标调用对应的方法块。

      这部分就是动态连接

6.4.2 运行时常量池

运行时常量池是方法区的一部分,是一块内存区域。

  • 运行时常量池其实就是将编译后的类信息放入运行时的一个区域中,用来动态获取类信息。

  • Class 文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到 JVM 中后对应一个运行时常量池,运行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。

  • 运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引和查找字段和方法名称和描述符的。给定任意一个方法或字段的索引,通过这个索引最终可得到该方法或字段所属的类型信息和名称及描述符信息,这涉及到方法的调用和字段获取。

6.4.3 字符串常量池

  • 字符串常量池是全局的,JVM 中独此一份,因此也称为全局字符串常量池。
  • 运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池。
  • 其实,“使用常量池”对应的字节码是一个 ldc 指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的 intern() 方法还可在运行期间把字符串放到字符串常量池中。
  • JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。
  • java7之后,字符串常量池不再存储在方法区,而是在堆中。

6.5 字符串常量池

定义

  • StringTable又称为String Pool,字符串常量池,其存在于堆中(jdk1.7之后改的)。
  • StringTable的存储结构类似于HashTable,但是不会触发rehash,也就是不能扩容。

特性

  • class常量池和运行时常量池中的字符串仅仅是字面量(符号),第一次使用时才变为字符串对象,加入字符串常量池

    (注:只有编译期class常量池存储的字符串字面量在运行时才会加入字符串常量池,程序中的字符串变量只会存在堆中)

  • 利用字符串常量池,可以避免重复创建字符串对象

  • 字符串变量的拼接原理是StringBuilder(1.8)

  • 字符串常量拼接的原理是编译器优化

  • 可以使用intern方法,主动将字符串常量池中还没有的字符串对象放入串池

6.5.1 通过字节码指令分析字符串存储位置

案例:

public class StringDemo03 {


    public static void main(String[] args) throws Exception {
        String str1 = "a";
        String str2 = "b";
        String str3 = "ab";
        String str4  = str1 + str2;
        String str5  = "a" + "b";

        System.out.println(str3 == str4);
        System.out.println(str3 == str5);
    }
}

字节码指令

Classfile /D:/JWF/Gitee/learn-demo/src/main/java/com/loksail/learndemo/stringp/StringDemo03.class
  Last modified 2019-12-24; size 887 bytes
  MD5 checksum dd4ccbd827ec00728aa1f85c60b7a2fa
  Compiled from "StringDemo03.java"
public class com.loksail.learndemo.stringp.StringDemo03
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#27        // java/lang/Object."<init>":()V
   #2 = String             #28            // a
   #3 = String             #29            // b
   #4 = String             #30            // ab
   #5 = Class              #31            // java/lang/StringBuilder
   #6 = Methodref          #5.#27         // java/lang/StringBuilder."<init>":()V
   #7 = Methodref          #5.#32         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #5.#33         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #36.#37        // java/io/PrintStream.println:(Z)V
  #11 = Class              #38            // com/loksail/learndemo/stringp/StringDemo03
  #12 = Class              #39            // java/lang/Object
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               StackMapTable
  #20 = Class              #40            // "[Ljava/lang/String;"
  #21 = Class              #41            // java/lang/String
  #22 = Class              #42            // java/io/PrintStream
  #23 = Utf8               Exceptions
  #24 = Class              #43            // java/lang/Exception
  #25 = Utf8               SourceFile
  #26 = Utf8               StringDemo03.java
  #27 = NameAndType        #13:#14        // "<init>":()V
  #28 = Utf8               a
  #29 = Utf8               b
  #30 = Utf8               ab
  #31 = Utf8               java/lang/StringBuilder
  #32 = NameAndType        #44:#45        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #33 = NameAndType        #46:#47        // toString:()Ljava/lang/String;
  #34 = Class              #48            // java/lang/System
  #35 = NameAndType        #49:#50        // out:Ljava/io/PrintStream;
  #36 = Class              #42            // java/io/PrintStream
  #37 = NameAndType        #51:#52        // println:(Z)V
  #38 = Utf8               com/loksail/learndemo/stringp/StringDemo03
  #39 = Utf8               java/lang/Object
  #40 = Utf8               [Ljava/lang/String;
  #41 = Utf8               java/lang/String
  #42 = Utf8               java/io/PrintStream
  #43 = Utf8               java/lang/Exception
  #44 = Utf8               append
  #45 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #46 = Utf8               toString
  #47 = Utf8               ()Ljava/lang/String;
  #48 = Utf8               java/lang/System
  #49 = Utf8               out
  #50 = Utf8               Ljava/io/PrintStream;
  #51 = Utf8               println
  #52 = Utf8               (Z)V
{
  public com.loksail.learndemo.stringp.StringDemo03();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore        4
        29: ldc           #4                  // String ab
        31: astore        5
        33: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        36: aload_3
        37: aload         4
        39: if_acmpne     46
        42: iconst_1
        43: goto          47
        46: iconst_0
        47: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
        50: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        53: aload_3
        54: aload         5
        56: if_acmpne     63
        59: iconst_1
        60: goto          64
        63: iconst_0
        64: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
        67: return
      LineNumberTable:
        line 12: 0
        line 13: 3
        line 14: 6
        line 15: 9
        line 16: 29
        line 18: 33
        line 19: 50
        line 20: 67
      StackMapTable: number_of_entries = 4
        frame_type = 255 /* full_frame */
          offset_delta = 46
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
    Exceptions:
      throws java.lang.Exception
}
SourceFile: "StringDemo03.java"
  • 通过上面的字节码文件可知,class文件常量池中存储了程序中定义的a #28,b #29,ab #30字符串字面量,当加载到运行时常量池时,a,b,ab也依旧只是常量池中的字面量(符号),还没有成为String对象被放入StringTable中。只有当程序运行到对应的字节码指令时,才会将运行时常量池中的字符串字面量变为字符串对象

    String str1 = "a";
    String str2 = "b";
    String str3 = "ab";
    
  • ldc #2 将运行时常量池中a字符串字面量变为字符串对象,并放入StringTable中,并将字符串对象引用压入操作数栈

  • ldc #3 将运行时常量池中b字符串字面量变为字符串对象,并放入StringTable中,并将字符串对象引用压入操作数栈

  • ldc #4 将运行时常量池中ab字符串字面量变为字符串对象,并放入StringTable中,并将字符串对象引用压入操作数栈

  • String str4 = str1 + str2;对应的字节码指令是

    9: new           #5                  // class java/lang/StringBuilder
    12: dup
    13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
    16: aload_1
    17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    20: aload_2
    21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    27: astore        4
    
  • 首先new #5 创建并默认初始化一个StringBuilder类型的对象并将其引用放入操作数栈;

    dup将操作数栈的此对象引用复制一份再压入栈顶;

    invokespecial #6从操作数栈弹栈此对象引用,并调用其无参构造方法;

    aload_1将局部变量表中1位置的变量(a字符串对象)压入操作数栈栈顶;

    invokevirtual #7操作数栈弹栈, 调用StringBuilder对象的append方法添加a字符串对象

    aload_2将局部变量表中2位置的变量(b字符串对象)压入操作数栈栈顶;

    invokevirtual #7操作数栈弹栈, 调用StringBuilder对象的append方法添加b字符串对象

    invokevirtual #8调用StringBuilder对象的toString方法

    astore 4将StringBuilder对象的引用存储到局部变量表的4位置

  • 也就是String str4 = str1 + str2;在字节码指令运行时等价于new StringBuilder().append("a").append("b").toString()

  • StringBuilder的toString()

    char[] value;
    int count;
    @Override
    public String toString() {
      // Create a copy, don't share the array
      return new String(value, 0, count);
    }
    
  • String str5 = "a" + "b";对应的字节码指令是

    29: ldc           #4                  // String ab
    31: astore        5
    
  • ldc #4 将#4对应的字符串字面量ab转换成字符串对象,并将其引用压入操作栈顶

  • astore 操作数栈弹栈,将ab的对象引用放入局部变量表的5位置

  • 也就是String str5 = "a" + "b"等价于String str5 = "ab",这是JVM在编译期的优化效果,不需要在字节码指令中重新拼接。

综合上面的所有结果,可知String str4 = str1 + str2;创建的字符串对象是在堆中的,String str5 = "a" + "b";创建的字符串是在字符串常量池中,而String str5 = "a" + "b";等价于String str3 = "ab";

所以最终的运行结果为

false
true

6.5.2 验证字符串对象的延迟加载

验证上文讲到的,类加载时从class常量池加载到运行时常量池中的字符串字面量仅仅是符号,而非字符串对象,仅在运行对应的字节码指令时,才转换成字符串对象

案例

public class StringDemo03 {

    public static void main(String[] args) throws Exception {
        System.out.println();

        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("1");
        System.out.println("2");
        System.out.println("3");

        System.out.println();
    }
}
  • 在第6行打断点,使用IDEA debug模式运行,查看控制台的memory信息,此时字符串对象数量为2304

    浅谈JVM - 内存结构(六)- 方法区
  • 运行完第六行后,此时字符串对象个数为2305,也就说明了。运行时常量池中对应“1”字符串字面量仅仅是符号,直到运行对应的字节码指令才转换成字符串对象

    浅谈JVM - 内存结构(六)- 方法区
  • 运行完第7行后,此时字符串对象个数为2306,运行时常量池中对应“2”字符串字面量转换成字符串对象

    浅谈JVM - 内存结构(六)- 方法区
  • 运行完第8行后,此时字符串对象个数为2307,运行时常量池中对应“3”字符串字面量转换成字符串对象

    浅谈JVM - 内存结构(六)- 方法区
  • 运行完第9行或者之后的代码,字符串对象个数仍然是2307,这是因为,"1","2","3"字符串对象已经放入字符串常量池,再次使用会引用同一个地址,并不会创建新的字符串对象。

6.5.3 String的intern方法

  • 在jdk1.6及之前,intern的处理是 先判断字符串对象字面量是否在字符串常量池中,如果存在直接返回字符串常量池中该字符串对象引用,如果没有找到,则在字符串常量池中创建字面量相同的字符串对象,并返回新的字符串对象的引用。
  • 在jdk1.7及之后,intern的处理是 先判断字符串对象字面量是否在字符串常量池中,如果存在直接返回字符串常量池中该字符串对象引用,如果没有找到,则将当前调用的字符串对象的引用加入到字符串常量池中,并返回该字符串对象的引用。

案例(基于1.8)

public class StringDemo03 {


    public static void main(String[] args) throws Exception {

        String str1 = new String("a") + new String("b");

        String str2 = str1.intern();

        System.out.println(str2 == str1);

        System.out.println(str2 == "ab");

    }
}
  • String str1 = new String("a") + new String("b");执行时,会将"a""b"放入字符串常量池中,然后通过StringBuilderappendtoString方法在堆中创建字面量为"ab"的字符串对象,并返回其对象引用,也就是此时,堆中有字面量为"ab"的字符串对象,字符串常量池中有字面量为"a""b"的字符串对象

  • String str2 = str1.intern();执行时,会将str1对应的堆中的"ab"字符串对象尝试加入字符串常量池中,因为此时字符串常量池中并没有字面量为"ab"的字符串对象,所以会将str1对应的堆中的"ab"字符串对象的引用加入到字符串常量池中,并返回此对象引用。也就是此时,堆中有字面量为"ab"的字符串对象,字符串常量池中有字面量为"a""b"的字符串对象以及堆中字面量为"ab"的字符串对象的引用

  • System.out.println(str2 == str1);执行时,str1为堆中的"ab"字符串对象的引用,str2为字符串常量池中返回的堆中的"ab"字符串对象的引用,两者均指向堆中的"ab"字符串对象,所以结果为true

  • System.out.println(str2 == "ab");执行时,str2为字符串常量池中返回的堆中的"ab"字符串对象的引用,而“ab”是从字符串常量池查找相对应字面量的字符串对象,此时字符串常量池会返回堆中的"ab"字符串对象的引用,所以结果为true

  • 对案例进行调整,在第7行加入String str = "ab"

  • 那么“ab”字符串对象就会先加入到字符串常量池中。

  • 在执行到第8行时,str1对应的字符串对象的字面量在字符串常量池已经存在,就不会将str1对应的对象引用加入到字符串常量池中了,而str2就会指向字符串常量池中的“ab”字符串对象。

  • 所以此时str1指向的是堆中的"ab"字符串对象,而str2指向的是字符串常量池中的"ab"字符串对象,故str2 != str1,str2 == "ab"。

6.5.4 综合案例

public class StringDemo03 {


    public static void main(String[] args) throws Exception {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";
        String s4 = s1 + s2;
        String s5 = "ab";
        String s6 = s4.intern();

        System.out.println(s3 == s4);
        System.out.println(s3 == s5);
        System.out.println(s3 == s6);

        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        x2.intern();

        // 如果调换17,18行的位置呢
        // 如果是jdk1.6呢
        System.out.println(x1 == x2);

    }
}
  • s3 == s4 false,s3是字符串常量池中对象的引用,s4是堆中对象的引用
  • s3 == s5 true,"a" + "b" 会在编译器优化成"ab",s3和s5均指向字符串常量池中的同一个对象引用
  • s3 == s6 true,s4.intern()会返回字符串常量池中"ab"字符串对象的引用,故s3和s6相等
  • x1 == x2 false,因为先定义了x1 = "cd",此时字符串常量池中已存在"cd"字符串对象,所以x2.intern(),并不会将x2对应的字符串对象引用加入到字符串常量池中,所有x1 != x2
  • 若如果调换17,18行的位置,x1 == x2 true,因为x2.intern()将x2的字符串对象的引用加入字符串常量池时,字符串常量池并没有字面量为"cd"的字符串对象,所以在定义x1 = "cd"的时候,返回的是x2的字符串对象的引用,所以两者相等
  • 如果是jdk1.6,x1 == x2 false,同样的道理,先定义了x1 = "cd",两者一个是堆中对象,一个是字符串常量池的对象
  • 如果是jdk1.6且调换了17,18行的位置,x1 == x2 false,x2.intern()执行时,虽然字符串常量池中并无字面量为"cd"的字符串对象,但是,1.6的处理是在字符串常量池中新建一个字面量为"cd"的字符串对象,所以x1 = "cd"得到的是字符串常量池中的对象引用,而x2是堆中的对象引用,故不相等。

6.5.5 字符串常量池的位置

  • jdk1.7之前,字符串常量池位于永久代中
  • jdk1.7及之后,字符串常量池位于堆中

验证案例

运行参数中加入-Xmx10m,让堆空间容易溢出

public class StringDemo05 {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int j = 0;
        try {
            for (int i = 0; i < 260000; i++, j++) {
                list.add(String.valueOf(i).intern());
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }
}

执行结果

java.lang.OutOfMemoryError: GC overhead limit exceeded
145776
    at java.lang.Integer.toString(Integer.java:401)
    at java.lang.String.valueOf(String.java:3099)
    at com.loksail.learndemo.stringp.StringDemo05.main(StringDemo05.java:16)
  • 输出结果不是预期的java heap space,这是因为并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。也就是GC频繁,却回收了较少的内存,这是因为jvm默认开启了-XX:+UseGCOverheadLimit
  • 在运行参数中加入 -XX:-UseGCOverheadLimit

执行结果

java.lang.OutOfMemoryError: Java heap space
146427
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create byte arrau at JPLISAgent.c line: 813
  • java.lang.OutOfMemoryError: Java heap space堆内存溢出,也就验证了字符串常量池存在堆中。
  • 如果使用jdk1.6运行则会抛出java.lang.OutOfMemoryError: PermGen space异常,验证了字符串常量池存在永久代中(此处不做演示)。

6.5.6 字符串常量池的垃圾回收

字符串常量池位于堆中,当堆内存满了就会触发垃圾回收

案例

运行参数中加入 -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

  • -Xmx10m 设置最大堆内存为10m
  • -XX:+PrintStringTableStatistics 打印字符串常量池的信息
  • -XX:+PrintGCDetails -verbose:gc 打印GC信息
public class StringDemo02 {

    public static void main(String[] args) throws Exception {
        int j = 0;
        try {

        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }
}

执行结果

0
Heap
 PSYoungGen      total 2560K, used 1958K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 95% used [0x00000000ffd00000,0x00000000ffee9a28,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
 Metaspace       used 3216K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13268 =    318432 bytes, avg  24.000
Number of literals      :     13268 =    567360 bytes, avg  42.762
Total footprint         :           =   1045880 bytes
Average bucket size     :     0.663
Variance of bucket size :     0.662
Std. dev. of bucket size:     0.814
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1770 =     42480 bytes, avg  24.000
Number of literals      :      1770 =    158488 bytes, avg  89.541
Total footprint         :           =    681072 bytes
Average bucket size     :     0.029
Variance of bucket size :     0.030
Std. dev. of bucket size:     0.172
Maximum bucket size     :         3

可以看到,结果并没有触发GC,主要关注StringTable statistics的相关信息

  • Number of buckets为桶的数目也就数组的长度,默认是60013
  • Number of entries 键值对数目,也就是字符串对象数目为 1770
  • Number of literals 字符串字面量数目为 1770

调整代码,向字符串常量池中增加字符串对象

public class StringDemo02 {

    public static void main(String[] args) throws Exception {
        int j = 0;
        try {
            for (int i = 0; i < 100; i++, j++) {
               String.valueOf(i).intern();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }
}

执行结果(部分)

StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      1870 =     44880 bytes, avg  24.000
Number of literals      :      1870 =    163288 bytes, avg  87.320
Total footprint         :           =    688272 bytes
Average bucket size     :     0.031
Variance of bucket size :     0.031
Std. dev. of bucket size:     0.177
Maximum bucket size     :         3

可以看到字符串常量池中的字符串对象数目增加了100个

调整代码,循环次数增加到10000次

public class StringDemo02 {

    public static void main(String[] args) throws Exception {
        int j = 0;
        try {
            for (int i = 0; i < 10000; i++, j++) {
               String.valueOf(i).intern();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }
}

执行结果

[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->761K(9728K), 0.0011489 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
10000
Heap
 PSYoungGen      total 2560K, used 856K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 18% used [0x00000000ffd00000,0x00000000ffd5c3a8,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 273K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 3% used [0x00000000ff600000,0x00000000ff6446c0,0x00000000ffd00000)
 Metaspace       used 3224K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13268 =    318432 bytes, avg  24.000
Number of literals      :     13268 =    567360 bytes, avg  42.762
Total footprint         :           =   1045880 bytes
Average bucket size     :     0.663
Variance of bucket size :     0.662
Std. dev. of bucket size:     0.814
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      8712 =    209088 bytes, avg  24.000
Number of literals      :      8712 =    491784 bytes, avg  56.449
Total footprint         :           =   1180976 bytes
Average bucket size     :     0.145
Variance of bucket size :     0.157
Std. dev. of bucket size:     0.396
Maximum bucket size     :         4

可以看到,字符串常量池中的字符串对象数目并非10000多,那是因为触发了GC,因为我们创建的字符串对象没有被引用,所以在GC过程中会回收部分字符串对象的内存,所以最终字符串对象的数目没有达到预期值。

6.5.7 字符串常量池性能调优

  1. 调整-XX:StringTableSize=桶个数
  • 上文提到过StringTable采用HashTable结构进行存储,也就是数组加链表的方式,那么桶的个数越多,越不容易出现哈希冲突,查找的效率越高

  • jdk1.8 StringTable默认桶个数为60013,最小桶个数为1009

    案例

    运行参数:-XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

    public class StringDemo02 {
    
       public static void main(String[] args) throws Exception {
           List<String> list = Stream.iterate(1, i -> i + 1).limit(460000).map(j -> String.valueOf(j)).collect(Collectors.toList());
           long start = System.nanoTime();
           for (String s : list) {
               s.intern();
           }
           System.out.println((System.nanoTime() - start) / 100000);
       }
    }
    

    运行结果

    979
    Heap
    PSYoungGen      total 75776K, used 46359K [0x000000076b580000, 0x0000000770a00000, 0x00000007c0000000)
     eden space 65024K, 71% used [0x000000076b580000,0x000000076e2c5d58,0x000000076f500000)
     from space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
     to   space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
    ParOldGen       total 173568K, used 0K [0x00000006c2000000, 0x00000006cc980000, 0x000000076b580000)
     object space 173568K, 0% used [0x00000006c2000000,0x00000006c2000000,0x00000006cc980000)
    Metaspace       used 4445K, capacity 4778K, committed 4992K, reserved 1056768K
     class space    used 502K, capacity 571K, committed 640K, reserved 1048576K
    SymbolTable statistics:
    Number of buckets       :     20011 =    160088 bytes, avg   8.000
    Number of entries       :     16555 =    397320 bytes, avg  24.000
    Number of literals      :     16555 =    731512 bytes, avg  44.187
    Total footprint         :           =   1288920 bytes
    Average bucket size     :     0.827
    Variance of bucket size :     0.832
    Std. dev. of bucket size:     0.912
    Maximum bucket size     :         6
    StringTable statistics:
    Number of buckets       :     60013 =    480104 bytes, avg   8.000
    Number of entries       :    462084 =  11090016 bytes, avg  24.000
    Number of literals      :    462084 =  25865720 bytes, avg  55.976
    Total footprint         :           =  37435840 bytes
    Average bucket size     :     7.700
    Variance of bucket size :     4.377
    Std. dev. of bucket size:     2.092
    Maximum bucket size     :        16
    

    桶个数为60013,总共运行时间是0.9s多,没有触发垃圾回收,字符串常量池中的字符串对象个数为462084个

    运行参数增加-XX:StringTableSize=200000,运行结果

    727
    Heap
    PSYoungGen      total 75776K, used 46358K [0x000000076b580000, 0x0000000770a00000, 0x00000007c0000000)
     eden space 65024K, 71% used [0x000000076b580000,0x000000076e2c5ac0,0x000000076f500000)
     from space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
     to   space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
    ParOldGen       total 173568K, used 0K [0x00000006c2000000, 0x00000006cc980000, 0x000000076b580000)
     object space 173568K, 0% used [0x00000006c2000000,0x00000006c2000000,0x00000006cc980000)
    Metaspace       used 4442K, capacity 4778K, committed 4992K, reserved 1056768K
     class space    used 502K, capacity 571K, committed 640K, reserved 1048576K
    SymbolTable statistics:
    Number of buckets       :     20011 =    160088 bytes, avg   8.000
    Number of entries       :     16555 =    397320 bytes, avg  24.000
    Number of literals      :     16555 =    731512 bytes, avg  44.187
    Total footprint         :           =   1288920 bytes
    Average bucket size     :     0.827
    Variance of bucket size :     0.832
    Std. dev. of bucket size:     0.912
    Maximum bucket size     :         6
    StringTable statistics:
    Number of buckets       :    200000 =   1600000 bytes, avg   8.000
    Number of entries       :    462083 =  11089992 bytes, avg  24.000
    Number of literals      :    462083 =  25865672 bytes, avg  55.976
    Total footprint         :           =  38555664 bytes
    Average bucket size     :     2.310
    Variance of bucket size :     3.434
    Std. dev. of bucket size:     1.853
    Maximum bucket size     :        11
    

    桶个数为200000,总共运行时间是0.7s多,没有触发垃圾回收,字符串常量池中的字符串对象个数为462083个

    运行参数调整-XX:StringTableSize=1009,运行结果

    40503
    Heap
    PSYoungGen      total 75776K, used 47659K [0x000000076b580000, 0x0000000770a00000, 0x00000007c0000000)
     eden space 65024K, 73% used [0x000000076b580000,0x000000076e40ac60,0x000000076f500000)
     from space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
     to   space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
    ParOldGen       total 173568K, used 0K [0x00000006c2000000, 0x00000006cc980000, 0x000000076b580000)
     object space 173568K, 0% used [0x00000006c2000000,0x00000006c2000000,0x00000006cc980000)
    Metaspace       used 4934K, capacity 5018K, committed 5248K, reserved 1056768K
     class space    used 559K, capacity 603K, committed 640K, reserved 1048576K
    SymbolTable statistics:
    Number of buckets       :     20011 =    160088 bytes, avg   8.000
    Number of entries       :     19153 =    459672 bytes, avg  24.000
    Number of literals      :     19153 =    819112 bytes, avg  42.767
    Total footprint         :           =   1438872 bytes
    Average bucket size     :     0.957
    Variance of bucket size :     0.957
    Std. dev. of bucket size:     0.978
    Maximum bucket size     :         7
    StringTable statistics:
    Number of buckets       :      1009 =      8072 bytes, avg   8.000
    Number of entries       :    463479 =  11123496 bytes, avg  24.000
    Number of literals      :    463479 =  25967096 bytes, avg  56.026
    Total footprint         :           =  37098664 bytes
    Average bucket size     :   459.345
    Variance of bucket size :    61.796
    Std. dev. of bucket size:     7.861
    Maximum bucket size     :       480
    

    桶个数为200000,总共运行时间是4s多,没有触发垃圾回收,字符串常量池中的字符串对象个数为463479个

    总结:

  • StringTable桶个数越大,字符串常量池中字符串对象查找的效率越高。

  • 当字符串常量较多时,可以调整StringTableSize来提高查找效率。

  1. 考虑将字符串对象放入字符串常量池

    案例

    public class StringDemo02 {
    
       public static void main(String[] args) throws Exception {
           List<Integer> list = Stream.iterate(1, i -> i + 1).limit(10).collect(Collectors.toList());
           List<String> result = new ArrayList<>();
           for (int i = 0; i < 1000000; i++) {
               for (Integer num : list) {
                   result.add(new String(String.valueOf(num)));
               }
           }
           System.out.println("---------结束--------");
           System.gc();
           System.in.read();
       }
    }
    

    使用jvisualvm -> 抽样器 -> 内存,查询String对象占用的内存

    浅谈JVM - 内存结构(六)- 方法区

    可以看到此时String对象实例大概有1000多万个

    调整案例,使用intern

    public class StringDemo02 {
    
    public static void main(String[] args) throws Exception {
        List<Integer> list = Stream.iterate(1, i -> i + 1).limit(10).collect(Collectors.toList());
        List<String> result = new ArrayList<>();
        for (int i = 0; i < 1000000; i++) {
            for (Integer num : list) {
                result.add(new String(String.valueOf(num)).intern());
            }
        }
        System.out.println("---------结束--------");
        System.gc();
        System.in.read();
    }
    }
    

    使用jvisualvm -> 抽样器 -> 内存,查询String对象占用的内存

    浅谈JVM - 内存结构(六)- 方法区

    可以看到此时String对象实例只有15000个

    原因分析:

  • 不使用intern()的情况下,创建的1000多万个String对象都会加入到list集合中,gc的时候并不会被垃圾回收,所以内存中有1000多万个String对象实例

  • 使用intern()的情况下,String对象创建了1000多万个,但是都是重复的字符串,使用intern()后会从字符串常量池中获取字符串对象,所以实际加入list集合中的String对象实例只有10个,而其他String对象会在System.gc()后被垃圾回收。

    总结:

  • 当系统存在大量重复的字符串使用时,可以使用intern()来避免大量字符串对象占用内存,从而达到节约堆内存的目的。

  • intern()大量字符串对象进入字符串常量池时,会耗费CPU性能进行查找,但是相比于堆内存占用,还是拥有更高的性价比

  • intern()大量字符串对象进入字符串常量池时,会导致Young GC变慢,这是因为Young GC的时候会扫描整个字符串常量池,当字符串常量池中字符串对象过多时就会影响整个扫描的效率

    参考:VM源码分析之String.intern()导致的YGC不断变长

欢迎关注公众号,后续文章更新通知,一起讨论技术问题 。

浅谈JVM - 内存结构(六)- 方法区

 

相关标签: JVM