浅谈JVM - 内存结构(六)- 方法区
注:本文虽然讨论的是方法区,但是为了解答某些问题也涉及到了Class文件结构和类加载的相关知识
java7及以前,方法区的实现是永久代,java8以后,方法区的实现是元空间
6.1 定义
- 方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。
- 方法区与堆有很多共性:线程共享、内存不连续、可扩展、可垃圾回收,同样当无法再扩展时会抛出OutOfMemoryError异常。
6.2 组成
6.3 方法区内存溢出
java7及以前,使用
-XX:MaxPermSize
限制永久代的最大内存,当加载大量的类时,会抛出java.lang.OutOfMemoryError:PermGen space
异常,永久代内存不足时会触发FullGCjava8以后,使用
-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
类型存储,同时字符串字面量包括成员变量中和局部变量中的字面量,而基本数据类型的字面量仅是成员变量中字面量 -
符号引用:符号引用属于编译原理方面的概念,是相对于直接引用来说的,主要包括了下面三类常量:
-
类和接口的全限定名(Fully Qualified Name)
- 接口的全限定名对应#101的java/lang/Object
- 类的全限定名对应#100的com/loksail/learndemo/demo/Demo01
-
方法的名称和描述符(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
-
-
字段的名称和描述符
-
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
初始化自定义值201. 静态成员变量的初始化是在类加载到内存时,在类加载的准备阶段时,会先为静态成员变量初始化默认值,如
private static int a = 1;
会在此时为a初始化默认值0,然后在类加载的初始化阶段,会为静态成员变量初始化自定义值,如private static int a = 1;
会在此时为a初始化自定义值12. 实例成员变量的初始化是在创建对象时,先会执行JVM的
new
指令,从方法区内对实例变量的定义拷贝一份到堆区,为成员变量初始化默认值,如private int a = 1;
会在此时为a初始化默认值0;然后再调用实例构造器的<init>
为成员变量初始化自定义值,如private int a = 1;
会在此时为a初始化自定义值12. 对于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
-
运行完第六行后,此时字符串对象个数为2305,也就说明了。运行时常量池中对应“1”字符串字面量仅仅是符号,直到运行对应的字节码指令才转换成字符串对象
-
运行完第7行后,此时字符串对象个数为2306,运行时常量池中对应“2”字符串字面量转换成字符串对象
-
运行完第8行后,此时字符串对象个数为2307,运行时常量池中对应“3”字符串字面量转换成字符串对象
-
运行完第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"
放入字符串常量池中,然后通过StringBuilder
的append
和toString
方法在堆中创建字面量为"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 字符串常量池性能调优
- 调整-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来提高查找效率。
-
考虑将字符串对象放入字符串常量池
案例
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对象占用的内存
可以看到此时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对象占用的内存
可以看到此时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
的时候会扫描整个字符串常量池,当字符串常量池中字符串对象过多时就会影响整个扫描的效率
欢迎关注公众号,后续文章更新通知,一起讨论技术问题 。
推荐阅读
-
[二]Java虚拟机 jvm内存结构 运行时数据内存 class文件与jvm内存结构的映射 jvm数据类型 虚拟机栈 方法区 堆 含义
-
荐 【探究JVM四】Java方法执行的线程内存模型——虚拟机栈 字节码指令追踪,万字长文深入探究内部结构
-
03-JVM内存模型:堆与方法区
-
JVM 内存初学 (堆(heap)、栈(stack)和方法区(method) )(转载)
-
OOM实战:堆内存溢出 虚拟机栈和本地方法栈溢出 jvm栈容量太小 栈帧太大 栈太小,导致线程分配少,创建更多的线程将导致oom 方法区和运行时常量池溢出
-
JVM入门(位置、体系结构、类加载器、双亲委派机制、沙箱安全机制、Native、PC寄存器、方法区、堆(新生区{伊甸园区、幸存区}、养老区、永久区)、OOM、GC算法、JMM)
-
jvm体系结构 & 本地方法接口 & pc寄存器 & 方法区 &栈
-
jvm知识点-内存结构区
-
jvm知识点-内存结构区
-
03-JVM内存模型:堆与方法区