06_方法区
方法区定义

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
The following exceptional condition is associated with the method area:If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an
OutOfMemoryError.
从Oracle的官方文档可以得知:
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。
静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中。
实例变量存在堆内存中,和方法区无关。
方法区逻辑上是堆的组成部分,但不同的JVM厂商的方法区不一定是堆的一部分,JVM规范并未强制方法区的位置
- 在JDK1.6及之前版本,HotSpot虚拟机使用“永久代(permanent generation)”的概念作为实现,即将GC分代收集扩展至方法区。这种实现比较偷懒,可以不必为方法区编写专门的内存管理,但带来的后果是容易碰到内存溢出的问题(因为永久代有-XX:MaxPermSize的上限)。在JDK1.7+之后,HotSpot逐渐改变方法区的实现方式,如1.7版本移除了方法区中的字符串常量池。
- 1.8版本中移除了方法区并使用metaspace(元数据空间)作为替代实现。metaspace占用系统内存,也就是说,只要不碰触到系统内存上限,方法区会有足够的内存空间。但这不意味着我们不对方法区进行限制,如果方法区无限膨胀,最终会导致系统崩溃。

方法区内存溢出
由于不同版本的JVM方法区的实现不同,在处理方法区内存溢出时也要注意JVM的版本,下面从1.6和1.8两个版本的JVM来说明这个问题
- 1.6导致永久代内存溢出
java.lang.OutOfMemoryError: PermGen space - 1.8导致元空间内存溢出
java.lang.OutOfMemoryError: Metaspace - 1.6设置永久代最大内存JVM参数
-XX:MaxPermSize=8m - 1.8设置元空间最大内存JVM参数
-XX:MaxMetaspaceSize=8m
jdk1.8.0_91
示例代码如下:
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示方法区内存溢出
* 1.8 -XX:MaxMetaspaceSize=8m 设置元空间大小
*/
public class T01 extends ClassLoader {//ClassLoader用于加载类的字节码
public static void main(String[] args) {
int j = 0;
try {
T01 t = new T01();
for (int i = 0; i < 10000; i++, j++) {
ClassWriter writer = new ClassWriter(0);//Class Writer 作用是生成类的二进制字节码
/*
visit的参数分别为(版本号,public,类名,包名,父类,接口)
*/
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] bytes = writer.toByteArray();
/*
执行类的加载
*/
t.defineClass("Class" + i, bytes, 0, bytes.length);
}
} finally {
System.out.println(j);
}
}
}
直接运行T01程序,并不会造成方法区内存溢出,输出如下结果:
10000
其原因是JVM1.8使用metaspace(元数据空间)作为替代方法区的替代实现,metaspace使用的是本地内存,一般情况下本地内存都足够大(比如我的机器,16G内存,上面的代码循环10000次毫无压力)
此时添加JVM运行参数-XX:MaxMetaspaceSize=8m降低最大元空间内存大小,输出以下结果:
5411
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 org.slumberjax.jvm.d06.T01.main(T01.java:31)
从结果中看出是Metaspace元空间内存溢出,并且只循环了5411次就溢出了
jdk1.6.0_45
示例代码如下:
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;
/**
* 演示方法区内存溢出
* 1.6 -XX:MaxPermSize=8m 设置永久代大小
*/
public class T01 extends ClassLoader {//ClassLoader用于加载类的字节码
public static void main(String[] args) {
int j = 0;
try {
T01 t = new T01();
for (int i = 0; i < 20000; i++, j++) {
ClassWriter writer = new ClassWriter(0);//Class Writer 作用是生成类的二进制字节码
/*
visit的参数分别为(版本号,public,类名,包名,父类,接口)
*/
writer.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] bytes = writer.toByteArray();
/*
执行类的加载
*/
t.defineClass("Class" + i, bytes, 0, bytes.length);
}
} finally {
System.out.println(j);
}
}
}
添加JVM参数-XX:MaxPermSize=8m并在jdk1.6.0_45运行该程序(此程序无法在1.8的jdk环境运行),输出如下结果
19318
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
at java.lang.ClassLoader.defineClass(ClassLoader.java:465)
at org.slumberjax.jvm.d06.T01.main(T01.java from InputFileObject:33)
从错误结果可以看出确实是永久代溢出早场的方法区内存溢出
实际场景
像spring,cglib,mybatis这些框架在运行时都使用到了java类字节码动态生成技术,而静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中。,使用这些框架时在运行期间会生成大量的类,造成方法区的内存溢出
运行时常量池
示例代码如下:
public class T02 {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
使用javap工具对该类字节码进行处理,执行命令javap -v T02.class输出如下内容:
Last modified 2020-8-8; size 559 bytes
MD5 checksum 2fea7fb3ce1e608fbce7d033f4ddfbce
Compiled from "T02.java"
public class org.slumberjax.jvm.d06.T02
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello world!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // org/slumberjax/jvm/d06/T02
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/slumberjax/jvm/d06/T02;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 T02.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello world!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 org/slumberjax/jvm/d06/T02
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public org.slumberjax.jvm.d06.T02();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/slumberjax/jvm/d06/T02;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "T02.java"
其中描述类的基本信息的内容如下:
Last modified 2020-8-8; size 559 bytes
MD5 checksum 2fea7fb3ce1e608fbce7d033f4ddfbce
Compiled from "T02.java"
public class org.slumberjax.jvm.d06.T02
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
其中描述常量池的内容如下:
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello world!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // org/slumberjax/jvm/d06/T02
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/slumberjax/jvm/d06/T02;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 T02.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello world!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 org/slumberjax/jvm/d06/T02
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
其中描述类方法定义的内容如下:
{
public org.slumberjax.jvm.d06.T02();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/slumberjax/jvm/d06/T02;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
其中main方法部分如下:

其中#2#3#4对应的在常量池中的值:

总结:
- 常量池: 就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
- 运行时常量池: 常量池是*.class文件中的,当该类被加载以后,虚拟机将它的常量池信息里面的符号地址变为真实地址,并放入方法区内存中,而方法区内存中这个符号替换为真实地址的常量池,就是所谓的运行时常量池
StringTable(串池)
StringTable底层是HashTable结构,该结构能避免字符串的重复创建
StringTable串池与常量池的关系
有如下代码:
public class T03 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}
执行javap -v T03.class,输出如下内容:
Classfile /D:/Projects/x/JVM/1.8/target/classes/org/slumberjax/jvm/d06/T03.class
Last modified 2020-8-8; size 507 bytes
MD5 checksum d73e74da4c0cddd5b8f50b39c34c1a67
Compiled from "T01.java"
public class org.slumberjax.jvm.d06.T03
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // org/slumberjax/jvm/d06/T03
#6 = Class #29 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/slumberjax/jvm/d06/T01;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 s1
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 s2
#21 = Utf8 s3
#22 = Utf8 SourceFile
#23 = Utf8 T01.java
#24 = NameAndType #7:#8 // "<init>":()V
#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab
#28 = Utf8 org/slumberjax/jvm/d06/T03
#29 = Utf8 java/lang/Object
{
public org.slumberjax.jvm.d06.T01();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/slumberjax/jvm/d06/T03;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, 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: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
}
SourceFile: "T03.java"
其中main方法部分的JVM指令为:

其执行流程如下:
-
先将常量池中的符号替换为真实地址,加载到方法区内存中,成为运行时常量池(下面的地址为假设)
Constant pool: 0x10001 = Methodref 0x10006.0x100024 // java/lang/Object."<init>":()V 0x10002 = String 0x100025 // a 0x10003 = String 0x100026 // b 0x10004 = String 0x100027 // ab 0x10005 = Class 0x100028 // org/slumberjax/jvm/d06/T03 0x10006 = Class 0x100029 // java/lang/Object同时main方法中的虚拟机指令是这样的,都被转换为了实际的地址:
0: ldc 0x10002 // String a 2: astore_1 3: ldc 0x10003 // String b 5: astore_2 6: ldc 0x10004 // String ab内存中只有内存地址,没有所谓的编号
-
解释器执行0:ldc时通过0x10002 地址顺腾摸瓜加载字符a,并将字符a变为"a"字符串对象
与此同时将"a"字符串放入StringTable中,如果StringTable中有字符串"a",则不再放入StringTable中,没有才放入,避免重复创建字符串对象
因为解释器执行到某行代码时才会去操作StringTable,因此这种操作是惰性的,并不是一开始就预加载
-
解释器执行astore_1时将字符a存储到本地变量表LocalVariableTable

-
后续步骤相同,不再赘述
整个main方法执行完毕后,StringTable中存放了["a","b","ab"]三个字符串
总结:
- 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
- 串池的作用是避免重复创建字符串对象
- 在创建字符串对象时,会将运行时常量池中的字符转换为字符串对象放入本地变量表中,同时将字符串对象加入串池
StringTable字符串变量拼接
有如下代码:
public class T04 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
}
}
执行javap -v T04.class,输出如下内容:
Last modified 2020-8-8; size 691 bytes
MD5 checksum 61637a147f614aa69915bfec80235917
Compiled from "T02.java"
public class org.slumberjax.jvm.d06.T04
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#29 // java/lang/Object."<init>":()V
#2 = String #30 // a
#3 = String #31 // b
#4 = String #32 // ab
#5 = Class #33 // java/lang/StringBuilder
#6 = Methodref #5.#29 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #36 // org/slumberjax/jvm/d06/T04
#10 = Class #37 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lorg/slumberjax/jvm/d06/T02;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 s1
#23 = Utf8 Ljava/lang/String;
#24 = Utf8 s2
#25 = Utf8 s3
#26 = Utf8 s4
#27 = Utf8 SourceFile
#28 = Utf8 T02.java
#29 = NameAndType #11:#12 // "<init>":()V
#30 = Utf8 a
#31 = Utf8 b
#32 = Utf8 ab
#33 = Utf8 java/lang/StringBuilder
#34 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #40:#41 // toString:()Ljava/lang/String;
#36 = Utf8 org/slumberjax/jvm/d06/T02
#37 = Utf8 java/lang/Object
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
#41 = Utf8 ()Ljava/lang/String;
{
public org.slumberjax.jvm.d06.T04();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/slumberjax/jvm/d06/T04;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, 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: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
line 9: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
}
SourceFile: "T04.java"
其中main方法的虚拟机指令如下:

从图上可以看出 String s4 = s1 + s2实际上在执行时是通过StringBuilder来进行拼接的,实际运行的代码为String s4 = new StringBuilder().append("a").append("b").toString()
查看StringBuilder的toString()方法源码:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
可以看出该方法使用new关键字创建了新的字符串对象,new出来的对象都属于堆区
有一道典型的面试题,代码如下,问输出结果:
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
}
通过前面的知识我们知道"a","b","ab"均存储于串池中
s1,s2,s3是方法中的变量,变量存储在栈中,这三个变量存储的是串池中"a","b","ab"的地址
而s4底层实现时是通过StringBuilder拼接,最后调用toString()方法,而该方法中使用new关键创建了新的字符串对象
这样就有两个值为"ab",但内存地址不同的字符串对象,==比较的是地址,因此上面的程序输出结果为false
StringTable编译期优化
代码如下:
public class T05 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
System.out.println(s3 == s5);
}
}
输出结果:
true
对于结果为什么是这样,我不能不能臆测,从原理上去分析,执行javap -v T03.class,输出如下内容:
Classfile /D:/Projects/x/JVM/1.8/target/classes/org/slumberjax/jvm/d06/T05.class
Last modified 2020-8-8; size 958 bytes
MD5 checksum 6b6126cc39aca1821d0e674f5a53ec5a
Compiled from "T05.java"
public class org.slumberjax.jvm.d06.T05
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#36 // java/lang/Object."<init>":()V
#2 = String #37 // a
#3 = String #38 // b
#4 = String #39 // ab
#5 = Class #40 // java/lang/StringBuilder
#6 = Methodref #5.#36 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#42 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #45.#46 // java/io/PrintStream.println:(Z)V
#11 = Class #47 // org/slumberjax/jvm/d06/T05
#12 = Class #48 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lorg/slumberjax/jvm/d06/T05;
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 s1
#25 = Utf8 Ljava/lang/String;
#26 = Utf8 s2
#27 = Utf8 s3
#28 = Utf8 s4
#29 = Utf8 s5
#30 = Utf8 StackMapTable
#31 = Class #23 // "[Ljava/lang/String;"
#32 = Class #49 // java/lang/String
#33 = Class #50 // java/io/PrintStream
#34 = Utf8 SourceFile
#35 = Utf8 T03.java
#36 = NameAndType #13:#14 // "<init>":()V
#37 = Utf8 a
#38 = Utf8 b
#39 = Utf8 ab
#40 = Utf8 java/lang/StringBuilder
#41 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = NameAndType #53:#54 // toString:()Ljava/lang/String;
#43 = Class #55 // java/lang/System
#44 = NameAndType #56:#57 // out:Ljava/io/PrintStream;
#45 = Class #50 // java/io/PrintStream
#46 = NameAndType #58:#59 // println:(Z)V
#47 = Utf8 org/slumberjax/jvm/d06/T05
#48 = Utf8 java/lang/Object
#49 = Utf8 java/lang/String
#50 = Utf8 java/io/PrintStream
#51 = Utf8 append
#52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#53 = Utf8 toString
#54 = Utf8 ()Ljava/lang/String;
#55 = Utf8 java/lang/System
#56 = Utf8 out
#57 = Utf8 Ljava/io/PrintStream;
#58 = Utf8 println
#59 = Utf8 (Z)V
{
public org.slumberjax.jvm.d06.T05();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/slumberjax/jvm/d06/T05;
public static void main(java.lang.String[]);
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 5
39: if_acmpne 46
42: iconst_1
43: goto 47
46: iconst_0
47: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
50: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
line 9: 29
line 11: 33
line 12: 50
LocalVariableTable:
Start Length Slot Name Signature
0 51 0 args [Ljava/lang/String;
3 48 1 s1 Ljava/lang/String;
6 45 2 s2 Ljava/lang/String;
9 42 3 s3 Ljava/lang/String;
29 22 4 s4 Ljava/lang/String;
33 18 5 s5 Ljava/lang/String;
StackMapTable: number_of_entries = 2
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 ]
}
SourceFile: "T05.java"
其中main方法虚拟机指令如下:

发现s5并没有采用s4的StringBuilder方式,而是直接识别为ab,造成这个的原因是javac在编译期间的优化,结果在编译器确定为ab,此时执行指令29: ldc ,先从串池中查找字符串"ab",如果有就直接使用串池中的"ab",没有再加入到串池中,很显然前面的代码在处理s3时已经将"ab"放入了串池中,因此这里的s3和s5都是串池中的"ab",==比较的结果为true
StringTable字符串延期初始化
借助Intellij Idea的断点调试功能来验证前面提到的串池惰性,字符串延期初始化(执行到具体行的代码时才去初始化字符串),代码如下:
public class T06 {
public static void main(String[] args) {
System.out.println();
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("0");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("0");
}
}
断点位置如图:

逐行推进断点,结果如下:


执行完输出1到0的10个字符串后,结果如下:

执行完全部代码后,结果如下

借助上面的结果,从实践上验证了串池的理论,惰性加载字符串,避免字符串的重复创建
StringTable_intern_1.8
在jdk1.8中,调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,则放入成功
- 如果串池有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
案例1分析:
public class T07 {
public static void main(String[] args) {
String s = new String("a") + new String("b");//执行该行代码后串池数据为["a","b"]
/*
String s2 = s.intern();将s放入了串池中,此时用s2接收的其实就是s 因此s2==s为true
*/
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则不放入,如果没有则放入,最后返回串池中的对象,因此此处放入串池的是s,赋值给s2的也是s,此时串池["a","b","ab"]
System.out.println(s2 == "ab");//true 创建常量字符串"ab"时发现串池中有已有值为"ab"的字符串(s.intern()放入,s赋值给了s2),不再放入
System.out.println(s2 == s);//true
}
}
案例2分析:
public class T08 {
public static void main(String[] args) {
String x = "ab";//字符串常量将被放入串池["ab"]
/*
new StringBuilder().append("a").append("b").toString() 通过StringBuilder的toString()方法在堆区new出一个"ab"字符串对象
*/
String s = new String("a") + new String("b");
/*
s.intern();尝试将这个堆区的"ab"放入串池时
发现串池中已有值为"ab"的字符串,放入失败,返回的是串池中原有的"ab"
因此 s2 == "ab"
*/
String s2 = s.intern();
System.out.println(s2 == x);//true
System.out.println(s == x);//false
}
}
StringTable_intern_1.6
在jdk1.6中,调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象
将1.8环境下的案例2在1.6环境下运行:
public class T08 {
public static void main(String[] args) {
String x = "ab";//字符串常量将被放入串池["ab"]
/*
new StringBuilder().append("a").append("b").toString() 通过StringBuilder的toString()方法在堆区new出一个"ab"字符串对象
*/
String s = new String("a") + new String("b");
/*
s.intern();尝试将这个堆区的"ab"放入串池时
发现串池中已有值为"ab"的字符串,放入失败,返回的是串池中原有的"ab"
因此 s2 == "ab"
*/
String s2 = s.intern();
System.out.println(s2 == x);//true
System.out.println(s == x);//false
}
}
因为串池中已有"ab",此时得到的结果和1.8环境下是一致的
修改代码如下:
public class T09 {
public static void main(String[] args) {
/*
new StringBuilder().append("a").append("b").toString() 通过StringBuilder的toString()方法在堆区new出一个"ab"字符串对象
*/
String s = new String("a") + new String("b");
/*
s.intern();尝试将这个堆区的"ab"放入串池时
发现串池中没有有值为"ab"的字符串,这里区别于1.8,创建一个副本放入串池中,返回的是放入串池中s的副本的引用
因此 s2 == s 为false
*/
String s2 = s.intern();
String x = "ab";//此时将字符串常量"ab"放入串池,但串池中已有前面s.intern()放入的s的副本"ab",直接将串池中的s的副本"ab"赋值给x
System.out.println(s2 == x);//true
System.out.println(s == x);//false
System.out.println(s == s2);//false
}
}
此时已经发现了不同之处
StringTable面试题
有了前面的基础,我们来分析下面的代码就再清晰不过了,下面的代码在1.8环境下运行:
public class T09 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";//编译期优化为"ab"并放入串池
String s4 = s1 + s2;//new StringBuilder().append("a").append("b").toString(); toString()方法中new了个新的String对象
String s5 = "ab";
String s6 = s4.intern();//尝试将s4放入串池,但串池中已有前面s3放入的"ab",此时返回的是原有串池冲的引用和s3的引用相同
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
String x2 = new String("c") + new String("d");//new StringBuilder().append("c").append("d").toString();
String x1 = "cd";//字符串常量 直接放入串池中(前面没有放入任何"cd"到串池)
x2.intern();//调用intern尝试将x2放入串池,但串池中已有"cd",放入串池失败,返回和x1相同的串池引用,但没有接收返回值
System.out.println(x1 == x2);//false
}
}
如果将倒数第2和第3行代码调换,又是下面的情况
public class T08 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";//编译期优化为"ab"并放入串池
String s4 = s1 + s2;//new StringBuilder().append("a").append("b").toString(); toString()方法中new了个新的String对象
String s5 = "ab";
String s6 = s4.intern();//尝试将s4放入串池,但串池中已有前面s3放入的"ab",此时返回的是原有串池冲的引用和s3的引用相同
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
String x2 = new String("c") + new String("d");//new StringBuilder().append("c").append("d").toString();
x2.intern();//调用intern尝试将x2放入串池,但串池没有"cd",将x2的引用放入串池
String x1 = "cd";//字符串常量 放入串池时发现串池中已有"cd",则直接使用串池中的"cd"的引用(其实就是x2的)
System.out.println(x1 == x2);//true
}
}
将上面第二个例子在1.6环境下运行进行分析
public class T10 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";//编译期优化为"ab"并放入串池
String s4 = s1 + s2;//new StringBuilder().append("a").append("b").toString(); toString()方法中new了个新的String对象
String s5 = "ab";
String s6 = s4.intern();//尝试将s4放入串池,但串池中已有前面s3放入的"ab",此时返回的是原有串池冲的引用和s3的引用相同
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
String x2 = new String("c") + new String("d");//new StringBuilder().append("c").append("d").toString();
x2.intern();//调用intern尝试将x2放入串池,但串池中没有"cd",将x2的副本放入串池
String x1 = "cd";//字符串常量 前面放入了x2的副本到串池中,此处的x1引用的是串池中x2的副本
System.out.println(x1 == x2);//false
}
}
因为放入的是副本,所以结果与1.8环境下运行结果不同
StringTable 垃圾回收
StringTable在内存紧张时,会发生垃圾回收
StringTable调优
-
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=xxxx -
考虑是否需要将字符串对象入池
可以通过intern方法减少重复入池

浙公网安备 33010602011771号