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


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

1.2 方法区可设置大小:
在JDK7中使用参数,前面的是初始值,后面是最大指

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

1.3 方法区的内部结构
1.3.1 类信息

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

- 直接接口是实现的接口,有序表示实现接口的前后顺序
1.3.2 域信息,即那些字段

1.3.3 方法信息

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


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

1.3.3.3 域信息:

1.3.3.4 方法信息
默认会有个构造方法,即init方法

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


1.4常量池和运行时常量池
常量池表包含各种字面量和对类型、域、方法的符号引用。
字面量(Literal):如字符串字面量("Hello")、整型常量(100)、浮点常量(3.14f)、final修饰的编译期常量。
符号引用(Symbolic References):用于描述目标实体的逻辑名称,包括:
类和接口的全限定名(如java/lang/String)
字段的名称与描述符(如name:Ljava/lang/String;)
方法的名称与描述符(如
1.4.1 常量池的作用:

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 图示举例方法区的使用:

1.7 永久代被元空间替代的理由
-
若使用永久代,如果内存设置小了容易出现full gc,会导致stw,影响性能,full gc后可能类还要使用,清理不了多少空间出来,会导致OOM;如果分配大了,容易浪费内存空间,使用元空间,使用的是本地内存。
-
堆的垃圾回收机制是用来回收对象,方法区里的是类,方法区在堆里,虚拟机回收堆时要判断对象能不能回收,还要判断类能不能回收,放在元空间,单独的一块区域,就可以单独地回收类。

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 字符串常量池的位置

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:如果没有,则将该对象的引用地址复制到字符串常量池中,返回该对象的引用地址
浙公网安备 33010602011771号