JVM (二) 方法区与字符串常量池

1 方法区

image
image

1.1 方法区、堆、栈的交互示意图

image

1.2 方法区可设置大小:

在JDK7中使用参数,前面的是初始值,后面是最大指
image

在JDK8中使用参数,前面的是初始值,后面是最大指
image

1.3 方法区的内部结构

1.3.1 类信息

image

  • 枚举类、注解、结构都算是类信息(类型信息)

image

  • 直接接口是实现的接口,有序表示实现接口的前后顺序

1.3.2 域信息,即那些字段

image

1.3.3 方法信息

image

1.3.3 方法区示例代码及字节码:

1.3.3.1 代码

image
image

1.3.3.2 类型信息

里面其实还有该类的类加载器的信息,即由哪个类加载器加载的。
image

1.3.3.3 域信息:

image

1.3.3.4 方法信息

默认会有个构造方法,即init方法
image

1.3.3. 静态变量需要注意的点:静态变量属于类,null也能访问;以及,常量编译后已确定,有ConstantValue信息。

image
image

1.4常量池和运行时常量池

常量池表包含各种字面量和对类型、域、方法的符号引用。

字面量(Literal)‌:如字符串字面量("Hello")、整型常量(100)、浮点常量(3.14f)、final修饰的编译期常量。
符号引用(Symbolic References)‌:用于描述目标实体的逻辑名称,包括:

类和接口的全限定名(如java/lang/String)

字段的名称与描述符(如name:Ljava/lang/String;)

方法的名称与描述符(如:(Ljava/lang/String;)V)

1.4.1 常量池的作用:

image

1.4.2 常量池举例

public class ConstantPoolExample {
    private static final int STATIC_FINAL_INT = 100;
    private static int staticInt = 200;
    private String instanceString = "Hello";
    private static String staticString = "World";

    public void methodExample(String param) {
        int localInt = 300;
        String localString = "Local";
        System.out.println(param + localString);
    }

    public static void main(String[] args) {
        ConstantPoolExample obj = new ConstantPoolExample();
        obj.methodExample("Test");
    }
}

常量池描述:

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // ConstantPoolExample
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               ConstantPoolExample
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #2.#8          // ConstantPoolExample.instanceString:Ljava/lang/String;
   #8 = NameAndType        #9:#10         // instanceString:Ljava/lang/String;
   #9 = Utf8               instanceString
  #10 = Utf8               Ljava/lang/String;
  #11 = Fieldref           #2.#12         // ConstantPoolExample.staticString:Ljava/lang/String;
  #12 = NameAndType        #13:#10        // staticString:Ljava/lang/String;
  #13 = Utf8               staticString
  #14 = String             #15            // Hello
  #15 = Utf8               Hello
  #16 = String             #17            // World
  #17 = Utf8               World
  #18 = String             #19            // Local
  #19 = Utf8               Local
  #20 = String             #21            // Test
  #21 = Utf8               Test
  #22 = String             #23            // TestLocal
  #23 = Utf8               TestLocal
  #24 = Integer            100           // 静态常量 final int
  #25 = Integer            200           // 静态变量 int
  #26 = Integer            300           // 局部变量 int
  #27 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #28 = Class              #30            // java/io/PrintStream
  #29 = NameAndType        #31:#32        // println:(Ljava/lang/String;)V
  #30 = Utf8               java/io/PrintStream
  #31 = Utf8               println
  #32 = Utf8               (Ljava/lang/String;)V
  #33 = Class              #34            // java/lang/String
  #34 = Utf8               java/lang/String
  #35 = Utf8               ConstantPoolExample
  #36 = Utf8               Code
  #37 = Utf8               LineNumberTable
  #38 = Utf8               LocalVariableTable
  #39 = Utf8               this
  #40 = Utf8               LConstantPoolExample;
  #41 = Utf8               param
  #42 = Utf8               Ljava/lang/String;
  #43 = Utf8               localInt
  #44 = Utf8               I
  #45 = Utf8               localString
  #46 = Utf8               Ljava/lang/String;
  #47 = Utf8               main
  #48 = Utf8               ([Ljava/lang/String;)V
  #49 = Utf8               methodExample
  #50 = Utf8               (Ljava/lang/String;)V
  #51 = Utf8               SourceFile
  #52 = Utf8               ConstantPoolExample.java

字面量:

字符串字面量‌:
#14 = String #15 // Hello:"Hello" 字符串字面量,存储在常量池中。
#16 = String #17 // World:"World" 字符串字面量。
#18 = String #19 // Local:"Local" 字符串字面量。
#20 = String #21 // Test:"Test" 字符串字面量。
#22 = String #23 // TestLocal:"TestLocal" 字符串字面量(在 System.out.println("Test" + "Local"); 中)。

整型字面量‌:
#24 = Integer 100:STATIC_FINAL_INT 的值 100,由于是 final 且为基本类型,通常会被编译器优化并直接放入字节码指令中,但这里作为常量池条目列出。
#25 = Integer 200:staticInt 的值 200。
#26 = Integer 300:localInt 的值 300。

‌符号引用:这些是编译时用来描述类、字段、方法等的名称和签名的引用,在类加载时会被解析为直接引用。

类的全限定名 (Class Name)‌:
#2 = Class #4 // ConstantPoolExample:引用了 ConstantPoolExample 类。
#28 = Class #30 // java/io/PrintStream:引用了 java.io.PrintStream 类。
#33 = Class #34 // java/lang/String:引用了 java.lang.String 类。

字段的名称和描述符 (Field Name and Descriptor)‌:
#7 = Fieldref #2.#8 // ConstantPoolExample.instanceString:Ljava/lang/String;:引用了类 ConstantPoolExample 中的字段 instanceString。
#11 = Fieldref #2.#12 // ConstantPoolExample.staticString:Ljava/lang/String;:引用了类 ConstantPoolExample 中的静态字段 staticString。

方法的名称和描述符 (Method Name and Descriptor)‌:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V:引用了 java.lang.Object 类的构造方法。
#27 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V:引用了 java.io.PrintStream.println 方法。

名称和类型 (NameAndType)‌:
#3 = NameAndType #5:#6 // "<init>":()V:表示类的构造方法名称和描述符。
#8 = NameAndType #9:#10 // instanceString:Ljava/lang/String;:表示字段 instanceString 的名称和类型。
#12 = NameAndType #13:#10 // staticString:Ljava/lang/String;:表示字段 staticString 的名称和类型。
#29 = NameAndType #31:#32 // println:(Ljava/lang/String;)V:表示方法 println 的名称和描述符。

其他常量‌
#4 = Utf8 ConstantPoolExample:存储了类名 ConstantPoolExample 的 UTF-8 编码。
#5 = Utf8 <init>:存储了构造方法名称 <init> 的 UTF-8 编码。
#6 = Utf8 ()V:存储了构造方法的描述符 ()V。
#31 = Utf8 println:存储了方法 println 的名称。
#32 = Utf8 (Ljava/lang/String;)V:存储了方法 println 的描述符。
#34 = Utf8 java/lang/String:存储了 java.lang.String 的 UTF-8 编码。
#35 = Utf8 ConstantPoolExample:存储了类名 ConstantPoolExample 的 UTF-8 编码。
#36 = Utf8 Code:存储了代码块的名称。
#37 = Utf8 LineNumberTable:存储了行号表的名称。
#38 = Utf8 LocalVariableTable:存储了局部变量表的名称。
#39 = Utf8 this:存储了 this 关键字的名称。
#40 = Utf8 LConstantPoolExample;:存储了 this 的类型描述符。
#41 = Utf8 param:存储了参数 param 的名称。
#42 = Utf8 Ljava/lang/String;:存储了参数 param 的类型描述符。
#43 = Utf8 localInt:存储了局部变量 localInt 的名称。
#44 = Utf8 I:存储了局部变量 localInt 的类型描述符。
#45 = Utf8 localString:存储了局部变量 localString 的名称。
#46 = Utf8 Ljava/lang/String;:存储了局部变量 localString 的类型描述符。
#47 = Utf8 main:存储了 main 方法的名称。
#48 = Utf8 ([Ljava/lang/String;)V:存储了 main 方法的描述符。
#49 = Utf8 methodExample:存储了 methodExample 方法的名称。
#50 = Utf8 (Ljava/lang/String;)V:存储了 methodExample 方法的描述符。
#51 = Utf8 SourceFile:存储了源文件信息的名称。
#52 = Utf8 ConstantPoolExample.java:存储了源文件名 ConstantPoolExample.java。

1.4.3 运行时常量池

定义
运行时常量池是类加载阶段,JVM将Class文件常量池内容‌复制并升级‌到方法区(JDK 8+为元空间)后形成的‌运行时内存结构‌。

核心作用‌:
符号引用解析‌:将#12等符号引用转换为直接引用(如方法在内存中的真实地址)。
动态常量支持‌:支持运行时新增常量,如String.intern()、Lambda表达式生成的常量、动态代理类的常量。

存储位置‌:
JDK 6–7:位于‌永久代(PermGen)‌。
JDK 8+:随方法区迁移至‌元空间(Metaspace)‌,使用本地内存(Native Memory)。

1.4.4 运行时常量池之动态常量支持‌

示例 1 :intern,jdk6及以前

    public static void getNowTime(){
        String nowTime = String.valueOf(System.currentTimeMillis());
        nowTime.intern();
    }

示例 2 :动态代理类的常量

public interface UserService {
        void addUser(String name);
        String getUser();
}

public class UserServiceImpl implements UserService {
        private String user;
        @Override
        public void addUser(String name) {
            this.user = name;
            System.out.println("添加用户: " + name);
        }

        @Override
        public String getUser() {
            System.out.println("获取用户: " + user);
            return user;
        }
}

public class UserServiceInvocationHandler implements InvocationHandler {
        private Object target; // 被代理的真实对象
        public UserServiceInvocationHandler(Object target) {
            this.target = target;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("【代理前置通知】开始执行方法: " + method.getName());
            Object result = method.invoke(target, args); // 调用真实对象的方法
            System.out.println("【代理后置通知】方法执行完毕: " + method.getName());
            return result;
        }
}

public class DynamicProxyDemo {
        public static void main(String[] args) {
              // 1. 创建真实对象
              UserService realService = new UserServiceImpl();
              // 2. 创建InvocationHandler
              UserServiceInvocationHandler handler = new UserServiceInvocationHandler(realService);
              // 3. 动态创建代理对象(核心步骤)
              UserService proxy = (UserService) Proxy.newProxyInstance(
                      UserService.class.getClassLoader(), // 类加载器
                      new Class[]{UserService.class},     // 代理接口
                      handler                              // 调用处理器
              );
       }
}
  • 代理类的运行时生成‌当执行 Proxy.newProxyInstance() 时,JVM会在内存中动态生成一个全新的代理类(如 $Proxy0)。这个类在编译期‌不存在对应的 .class 文件‌,因此它的Class文件常量池在磁盘上是不存在的。

  • 运行时常量池的动态构建‌,JVM在生成 $Proxy0 类的字节码时,会同时为它在方法区(元空间)创建对应的‌运行时常量池‌。

1.6 图示举例方法区的使用:

image

1.7 永久代被元空间替代的理由

  • 若使用永久代,如果内存设置小了容易出现full gc,会导致stw,影响性能,full gc后可能类还要使用,清理不了多少空间出来,会导致OOM;如果分配大了,容易浪费内存空间,使用元空间,使用的是本地内存。

  • 堆的垃圾回收机制是用来回收对象,方法区里的是类,方法区在堆里,虚拟机回收堆时要判断对象能不能回收,还要判断类能不能回收,放在元空间,单独的一块区域,就可以单独地回收类。

image

1.8 方法区回收

1.8.1 回收内容

  • 废弃常量‌:
    回收运行时常量池中不再被引用的常量(如字符串字面量、数值常量等)。例如字符串 "abc" 若未被任何对象引用,则可能被回收。
    • 字面量回收机制‌:与堆对象回收类似,通过可达性分析判断。若常量未被任何 GC Roots 引用(如无字符串对象引用该字面量),则被标记为废弃常量。

字面量回收示例

String s1 = "abc";  // 常量池中创建字面量 "abc"
String s2 = "abc";  // 复用常量池中的 "abc"
s1 = null;          // 断开引用
s2 = null;          // 断开引用
// 若后续无代码引用 "abc",且发生 Full GC,则 "abc" 可能被回收
  • 无用类‌:
  • 需同时满足以下三个条件才可被卸载:
    • 该类的所有实例对象已被回收(堆中无实例);
    • 加载该类的 ClassLoader 已被回收;
    • 该类的 java.lang.Class 对象未被任何地方引用

1.8.2 回收的‌主要时机‌

  • Full GC 期间‌:方法区回收通常伴随整堆回收触发
  • 元空间(Metaspace)不足‌:当类元数据占用超过 -XX:MaxMetaspaceSize 阈值时
  • 显式调用 System.gc()(实际回收由 JVM 决定)

1.8.3 回收效率低的原因‌

  • 类加载器通常为长生命周期(如应用类加载器);
  • 静态变量、框架缓存(如 Spring)或线程上下文持有类引用,导致类难以满足卸载条件。

2 字符串常量池

2.1 概述

字符串常量池(String Constant Pool)是‌方法区(Method Area)‌中的一块特殊内存区域,用于存储‌编译期确定的字符串字面量‌(literal)及其引用。它的核心作用是:避免重复创建内容相同的字符串对象,实现“一个字面量,一个对象”的共享机制。

  • Java 中的字符串对象有两种存储位置:

    • 字符串常量池‌:存放字符串字面量,具有唯一性。
    • 堆内存‌:通过 new 关键字创建的对象String存放在堆中。
  • String 类内部使用 char[] value 存储字符数据

2.1.1 字符串常量池的位置

image

2.2 字符串之拼接

2.2.1 常量与常量拼接的结果放在常量池中,原理是编译优化

示例,常量优化

    public void test1(){
        String s3 = "aabb";//aabb放进在字符串常量池中,即,会在字符串常量池中新建字符串aabb
        String s4 = "aa" + "bb"; // 编译期优化为,String s4 = "aabb",并且aa和bb不会放进字符串常量池
        System.out.println(s3 == s4); // true
    }
    public void test2(){
        final String s1 = "aa" ;
        final String s2 = "bb" ;
        String s3 = "aabb";
        String s4 = s1+s2; // 编译期优化为,String s4 = "aa"+"bb",
        System.out.println(s3 == s4);//true
    }

2.2.2 有变量的拼接,底层是new StringBuilder

        String s1 = "aa";//aa放进在字符串常量池中,即,会在字符串常量池中新建字符串aa
        String s5 = s1 + "bb";
        System.out.println(s3 == s5);// false

等同于下面的代码

        StringBuilder stringBuilder1 =new StringBuilder();
        StringBuilder stringBuilder2 = stringBuilder1.append("aa");
        StringBuilder stringBuilder3 = stringBuilder2.append("bb");
        //在堆区新建一个String对象,内容为aabb,并不会放进在字符串常量池中,即,不会在字符串常量池中新建字符串aabb
        String s5 = stringBuilder3.toString();
  • StringBuilder.toString() 调用 new String(value, 0, count)

  • 这个构造函数的签名是 String(char value[], int offset, int count)。它接收的是一个 char[] 数组,以及起始偏移和长度信息。这个 char[] 数组来自 StringBuilder 内部维护的缓冲区。

  • StringBuilder 的 append 操作是在运行时动态修改其内部的 char[] 数组。当调用 这个构造函数仅仅根据给定的字符数组,在堆上创建一个新的 String 对象。

  • 最终结果‌:new String(value, 0, count) 只在‌堆内存‌中创建了一个新的 String 对象,不会在字符串常量池中新建一个字符串对象。

    • 该构造方法不同于new String("aabb")。

2.2.3 new String()

会创建 ‌6 个对象‌

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

1、
new String("a")‌,创建两个对象,第一个:字符串常量池中的 a ,第二个,堆的String对象,里面内容为 a

2、
new String("b")‌,创建两个对象,第三个:字符串常量池中的 b ,第四个,堆的String对象,里面内容为 b

3、
处理字符串拼接,第五个
StringBuilder sb = new StringBuilder();
sb.append(new String("a"));
sb.append(new String("b"));

4、
sb.toString() 时,会在堆内存中创建一个新的 String 对象(第六个对象),其内容为 "ab"。

2.2.4 字串拼接与append方法的效率对比

    public static void test6(){
        String a="a";
        a+="b";
        a+="c";
        a+="d";
        a+="e";
        
        StringBuilder stringBuilder=new StringBuilder("a");
        stringBuilder.append("b");
        stringBuilder.append("b");
        stringBuilder.append("b");
        stringBuilder.append("b");
        stringBuilder.append("b");
    }
  • append方法快

2.3 intern()

intern() 方法的作用,将堆中的字符串对象“归档”到字符串常量池中‌,如果池中已有相同内容的字符串,则返回池中的引用;否则,将当前对象加入池中并返回对象池中的引用。

示例 2.3.1

    public static void test3(){
        String s1 ="aabb";
        String s2 = "aa" ;
        String s3 = s2+"bb";
        String s4 =s3.intern();
        System.out.println(s1 == s4);//true
    }
    public static void test4(){
        System.out.println(("a"+"b").intern() == "ab");//true
    }

示例2.3.2 之jdk6和jdk7的区别

1、

    public static void test5(){
        String s3 = new String("1")+new String("1");
        s3.intern();
        String s4 = "11";
        //jdk6是false
        //jdk7开始,是true
        System.out.println(s3 == s4);
    }

2、

    public static void test6(){
        String s3 = new String("1")+new String("1");
        String s4 = "11";
        String s5 = s3.intern();
        System.out.println(s3 == s4);
        System.out.println(s3 == s5);
        System.out.println(s4 == s5);
    }
  • 尝试将这个字符串对象(比如上述的“11”)放入字符串常量池中。如果字符串常量池中有,则不会放入,并且返回字符串常量池中该对象的引用

    • jdk6:如果没有,则将该对象复制一份到字符串常量池中,并且返回字符串常量池中该对象的引用
    • jdk7:如果没有,则将该对象的引用地址复制到字符串常量池中,返回该对象的引用地址
posted @ 2025-12-13 12:49  dvdhellohaha  阅读(23)  评论(0)    收藏  举报