Skip to content

一、元空间

  • 含义:主要存放类的信息
  • 类的信息主要包含:类的信息局部变量表方法信息常量池
  • 线程安全问题:线程(数据)共享,存在线程安全问题。

类的信息

  • 对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation)。
  • JVM 必须在方法区中存储以下类型信息:
    • 这个类型的完整有效名称(全名=包名.类名)
    • 这个类型直接父类的完整有效名(对于 interface 或是 java.lang.Object,都没有父类)
    • 这个类型的修饰符(public,abstract,final 的某个子集)
    • 这个类型直接接口的一个有序列表。
  • 例如:
shell
$ javap -p -v Metaspace.class (参数 -p 确保能查看 private 权限类型的字段或方法)

# 输出如下类信息:

Classfile /Users/calvin/学习/01-后端开发/Java/02-进阶知识/Jvm 虚拟机/calvin-java-jvm/chapter-02-jvm-structure/target/classes/com/calvin/jvm/structure/Metaspace.class
Last modified 2023-10-7; size 922 bytes
MD5 checksum c2cd9e8e4ae3f1ba55a726ab7f6be504
Compiled from "Metaspace.java"
public class com.calvin.jvm.structure.Metaspace
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER

## ACC_PUBLIC 表示该类 是一个 public

域(Field)信息

  • JVM 必须在元空间中保存类型的所有域的相关信息以及域的声明顺序。

  • 的相关信息包括:

    • 域名称
    • 域类型
    • 域修饰符(publicprivateprotectedstaticfinalvolatiletransient的某个子集)
  • 例如:

shell
$ javap -p -v Metaspace.class (参数 -p 确保能查看 private 权限类型的字段或方法)

# 输出如下域信息:

public static final java.lang.Integer I;
    descriptor: Ljava/lang/Integer;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public final java.lang.String A;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_FINAL
    ConstantValue: String a

  public final double b;
    descriptor: D
    flags: ACC_PUBLIC, ACC_FINAL

方法(Method)信息

  • JVM 必须保存所有方法的以下信息,同域信息一样包括声明顺序:

    • 方法名称
    • 方法的返回类型(包括 void 返回类型),void 在 Java 中对应的为 void.class
    • 方法参数的数量和类型(按顺序)
    • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
    • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
    • 异常表(abstract 和 native 方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
  • 例如:

shell
$ javap -p -v Metaspace.class (参数 -p 确保能查看 private 权限类型的字段或方法)

# 输出如下方法信息:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #6                  // String c
         2: astore_1
         3: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: aload_1
         7: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 34: 0
        line 35: 3
        line 36: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
            3       8     1     c   Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        100
         2: invokestatic  #9                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #10                 // Field I:Ljava/lang/Integer;
         8: return
      LineNumberTable:
        line 15: 0
}

# descriptor: ([Ljava/lang/String;)V 表示方法返回值类型为 void
# flags: ACC_PUBLIC => 表示方法权限修饰符为 public ACC_STATIC => 表示静态方法修饰符
# stack=2 => 表示操作数栈深度为 2
# locals=2 => 表示局部变量个数为 2 个
# args_size=1 => 参数个数为 1 个

常量池 (Constant Pool)

  • 在 java 用于保存在编译期已确定的,已编译的 class 文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如 String s = "java"这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是 JVM 的一块特殊的内存空间。
  • 常量: 是一个固定值。
  • 常量池类型:
    • class 常量池(静态常量池)
    • 运行常量池
    • 字符串常量池
  • 例如: 源码: ConstantPool.java
java
package com.calvin.jvm.structure;

/**
 * 常量池
 *
 * @author Calvin
 * @date 2023/5/11
 * @since v1.0.0
 */
public class ConstantPool {

    /**
     * 字符串常量
     */
    private final String A = "a";

}

JVM 虚拟机汇编 C 语言

c
Classfile /Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-02-jvm-structure/target/classes/com/calvin/jvm/structure/ConstantPool.class

// 常量池
Constant pool:
   #1 = Methodref          #5.#18         // java/lang/Object."<init>":()V
   #2 = String             #19            // a
   #3 = Fieldref           #4.#20         // com/calvin/jvm/structure/ConstantPool.A:Ljava/lang/String;
   #4 = Class              #21            // com/calvin/jvm/structure/ConstantPool
   #5 = Class              #22            // java/lang/Object
   #6 = Utf8               A
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               ConstantValue
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/calvin/jvm/structure/ConstantPool;
  #16 = Utf8               SourceFile
  #17 = Utf8               ConstantPool.java
  #18 = NameAndType        #9:#10         // "<init>":()V
  #19 = Utf8               a
  #20 = NameAndType        #6:#7          // A:Ljava/lang/String;
  #21 = Utf8               com/calvin/jvm/structure/ConstantPool
  #22 = Utf8               java/lang/Object

常量池类型

  • class 常量池(静态常量池)

    • 含义: 当 Class 文件Java 虚拟机 加载进来后,存放各种 字面量 (Literal)符号引用
  • 字面量 (Literal)

    • 文本字符串
    • 被声明为 final 的常量值
    • 基本数据类型的值
    • 其他
c
// 例如: 文本字符串
#19 = Utf8               a
  • 符号引用
    • 类和结构的完全限定名
    • 字段名称和描述符
    • 方法名称和描述符
c
// 例如: 方法名称和描述符
#1 = Methodref          #5.#18         // java/lang/Object."<init>":()V
  • 运行常量池
    • Class 文件被加载到内存后, Java 虚拟机会将 Class 文件常量池里的内容 转移到 运行时常量池 里。
    • 并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中
  • String 对象用于保存字符串, 也就是一组字符串序列。
  • 使用: 用 "" 双引号, 例如: "Hello Wold"。
  • 编码: Unicode 字符编码。一个字符占用两个字节。
  • String 是 final 不可以被继承,不可以被修改(内存地址不可修改,内容是可以被修改的)。
  • JDK1.6 版本以前,字符串常量池放在方法区(永久区)。
  • JDK1.7 版本,常量池放在堆(不合理静态变量不属于堆)。
  • JDK1.8 版本,单独将“字符串常量池”放到堆,堆单独给字符串常量池开辟空间,其他常量池存放在方法区。

字符串常量池相关面试题

  • ==内存地址 对比。
  • equals字符串 对比。
java
package com.calvin.jvm.structure;

import java.util.Comparator;

/**
 * 字符串常量池
 *
 * @author Calvin
 * @date 2023/6/12
 * @since v1.0.0
 */
public class StringConstantPool {


    /**
     * 通过 equlas 进行字符串比较
     *
     * @return {@link Boolean}
     */
    public static Boolean stringComparingByEq(String s) {
        String a2 = "a";
        return a2.equals(s);
        /* 源码
          if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }*/
    }


    /**
     * 通过 == 操作符,进行字符串比较
     *
     * @return {@link Boolean}
     */
    public static Boolean stringComparingByEqOperators (String s) {
        String a2 = "a";
        return a2 == s;
    }

    /**
     * 主方法
     *
     * @param args 参数
     */
    public static void main(String[] args) {
        // equals 方法: 值的对比
        Boolean a1 = stringComparingByEq("a");
        System.out.println("equals 方法: " + a1);

        // == 操作符: 地址引用
        Boolean a2 = stringComparingByEqOperators("a");
        System.out.println("== 操作符: " + a2);

        Boolean a3 = stringComparingByEqOperators(new String("a"));
        System.out.println("== 操作符: " + a3);
    }
}

结果如下:

log
equals 方法: true********
== 操作符: true
== 操作符: false
java
  Boolean a3 = stringComparingByEqOperators(new String("a"));
  System.out.println("== 操作符: " + a3);

结果如下:

log
== 操作符: false
java
 String ab1 = "a" + "b";
 String a = "a";
 String b = "b";
 String ab2 = a + b;
 System.out.println("ab1 == ab2: " + (ab1 == ab2));

结果如下:

log
ab1 == ab2: false
java
 long startMs = System.currentTimeMillis();
 String str = "a";
 for (int i = 0; i < 1000000; i++) {
     // 使用 += 会在堆内存字符常量池不断地添加新的对象,因为String 字符串常量为final 是不可变的,导致了程序变慢。
         str += i;
 }
 long endMs = System.currentTimeMillis();
 System.out.println("+= 用时:" + ((endMs - startMs)/1000) + "秒");

 long startMs1 = System.currentTimeMillis();
 StringBuilder str1 = new StringBuilder("a");
 for (int i = 0; i < 1000000; i++) {
     str1.append(i);
 }
 str1.toString();
 long endMs1 = System.currentTimeMillis();
 System.out.println("StringBuilder.append() 用时:" + ((endMs1 - startMs1)/1000) + "秒");

结果如下:

log
+= 用时: 332秒
StringBuilder.append() 用时: 0秒