一、元空间
- 含义:主要
存放类的信息
。 - 类的信息主要包含:
类的信息
、局部变量表
、方法信息
、常量池
。 - 线程安全问题:线程(数据)共享,存在线程安全问题。
类的信息
- 对每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation)。
- JVM 必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名(对于 interface 或是 java.lang.Object,都没有父类)
- 这个类型的修饰符(public,abstract,final 的某个子集)
- 这个类型直接接口的一个有序列表。
- 例如:
shell
$ javap -p -v Metaspace.class (参数 -p 确保能查看 private 权限类型的字段或方法)
# 输出如下类信息:
Classfile /Users/calvin/学习/01-后端开发/Java/02-进阶知识/Jvm 虚拟机/calvin-java-jvm/chapter-02-jvm-structure/target/classes/com/calvin/jvm/structure/Metaspace.class
Last modified 2023-10-7; size 922 bytes
MD5 checksum c2cd9e8e4ae3f1ba55a726ab7f6be504
Compiled from "Metaspace.java"
public class com.calvin.jvm.structure.Metaspace
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
## ACC_PUBLIC 表示该类 是一个 public
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
域(Field)信息
JVM 必须在元空间中保存类型的所有域的相关信息以及域的声明顺序。
域
的相关信息包括:- 域名称
- 域类型
- 域修饰符(
public
,private
,protected
,static
,final
,volatile
,transient
的某个子集)
例如:
shell
$ javap -p -v Metaspace.class (参数 -p 确保能查看 private 权限类型的字段或方法)
# 输出如下域信息:
public static final java.lang.Integer I;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
public final java.lang.String A;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: String a
public final double b;
descriptor: D
flags: ACC_PUBLIC, ACC_FINAL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
方法(Method)信息
JVM 必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称
- 方法的返回类型(包括 void 返回类型),void 在 Java 中对应的为 void.class
- 方法参数的数量和类型(按顺序)
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract 的一个子集)
- 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
- 异常表(abstract 和 native 方法除外),异常表记录每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
例如:
shell
$ javap -p -v Metaspace.class (参数 -p 确保能查看 private 权限类型的字段或方法)
# 输出如下方法信息:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: ldc #6 // String c
2: astore_1
3: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
7: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 34: 0
line 35: 3
line 36: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
3 8 1 c Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: invokestatic #9 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #10 // Field I:Ljava/lang/Integer;
8: return
LineNumberTable:
line 15: 0
}
# descriptor: ([Ljava/lang/String;)V 表示方法返回值类型为 void
# flags: ACC_PUBLIC => 表示方法权限修饰符为 public ACC_STATIC => 表示静态方法修饰符
# stack=2 => 表示操作数栈深度为 2
# locals=2 => 表示局部变量个数为 2 个
# args_size=1 => 参数个数为 1 个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
常量池 (Constant Pool)
- 在 java 用于保存在编译期已确定的,已编译的 class 文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括字符串常量,如 String s = "java"这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是 JVM 的一块特殊的内存空间。
常量
: 是一个固定值。常量池类型
:- class 常量池(静态常量池)
- 运行常量池
- 字符串常量池
- 例如: 源码:
ConstantPool.java
java
package com.calvin.jvm.structure;
/**
* 常量池
*
* @author Calvin
* @date 2023/5/11
* @since v1.0.0
*/
public class ConstantPool {
/**
* 字符串常量
*/
private final String A = "a";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JVM 虚拟机汇编 C 语言
c
Classfile /Users/calvin/学习/Jvm 虚拟机/calvin-java-jvm/chapter-02-jvm-structure/target/classes/com/calvin/jvm/structure/ConstantPool.class
// 常量池
Constant pool:
#1 = Methodref #5.#18 // java/lang/Object."<init>":()V
#2 = String #19 // a
#3 = Fieldref #4.#20 // com/calvin/jvm/structure/ConstantPool.A:Ljava/lang/String;
#4 = Class #21 // com/calvin/jvm/structure/ConstantPool
#5 = Class #22 // java/lang/Object
#6 = Utf8 A
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 ConstantValue
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/calvin/jvm/structure/ConstantPool;
#16 = Utf8 SourceFile
#17 = Utf8 ConstantPool.java
#18 = NameAndType #9:#10 // "<init>":()V
#19 = Utf8 a
#20 = NameAndType #6:#7 // A:Ljava/lang/String;
#21 = Utf8 com/calvin/jvm/structure/ConstantPool
#22 = Utf8 java/lang/Object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
常量池类型
class 常量池(静态常量池)
- 含义: 当 Class 文件 被 Java 虚拟机 加载进来后,存放各种 字面量 (Literal) 和 符号引用。
字面量 (Literal)
- 文本字符串
- 被声明为 final 的常量值
- 基本数据类型的值
- 其他
c
// 例如: 文本字符串
#19 = Utf8 a
1
2
2
符号引用
- 类和结构的完全限定名
- 字段名称和描述符
- 方法名称和描述符
c
// 例如: 方法名称和描述符
#1 = Methodref #5.#18 // java/lang/Object."<init>":()V
1
2
2
- 运行常量池
- Class 文件被加载到内存后, Java 虚拟机会将 Class 文件常量池里的内容 转移到 运行时常量池 里。
- 并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
String
对象用于保存字符串, 也就是一组字符串序列。- 使用: 用 "" 双引号, 例如: "Hello Wold"。
- 编码: Unicode 字符编码。一个字符占用两个字节。
String
是 final 不可以被继承,不可以被修改(内存地址不可修改,内容是可以被修改的)。
- JDK1.6 版本以前,字符串常量池放在方法区(永久区)。
- JDK1.7 版本,常量池放在堆(不合理静态变量不属于堆)。
- JDK1.8 版本,单独将“字符串常量池”放到堆,堆单独给字符串常量池开辟空间,其他常量池存放在方法区。
字符串常量池相关面试题
==
是 内存地址 对比。equals
是 字符串 对比。
java
package com.calvin.jvm.structure;
import java.util.Comparator;
/**
* 字符串常量池
*
* @author Calvin
* @date 2023/6/12
* @since v1.0.0
*/
public class StringConstantPool {
/**
* 通过 equlas 进行字符串比较
*
* @return {@link Boolean}
*/
public static Boolean stringComparingByEq(String s) {
String a2 = "a";
return a2.equals(s);
/* 源码
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}*/
}
/**
* 通过 == 操作符,进行字符串比较
*
* @return {@link Boolean}
*/
public static Boolean stringComparingByEqOperators (String s) {
String a2 = "a";
return a2 == s;
}
/**
* 主方法
*
* @param args 参数
*/
public static void main(String[] args) {
// equals 方法: 值的对比
Boolean a1 = stringComparingByEq("a");
System.out.println("equals 方法: " + a1);
// == 操作符: 地址引用
Boolean a2 = stringComparingByEqOperators("a");
System.out.println("== 操作符: " + a2);
Boolean a3 = stringComparingByEqOperators(new String("a"));
System.out.println("== 操作符: " + a3);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
结果如下:
log
equals 方法: true********
== 操作符: true
== 操作符: false
1
2
3
2
3
java
Boolean a3 = stringComparingByEqOperators(new String("a"));
System.out.println("== 操作符: " + a3);
1
2
2
结果如下:
log
== 操作符: false
1
java
String ab1 = "a" + "b";
String a = "a";
String b = "b";
String ab2 = a + b;
System.out.println("ab1 == ab2: " + (ab1 == ab2));
1
2
3
4
5
2
3
4
5
结果如下:
log
ab1 == ab2: false
1
java
long startMs = System.currentTimeMillis();
String str = "a";
for (int i = 0; i < 1000000; i++) {
// 使用 += 会在堆内存字符常量池不断地添加新的对象,因为String 字符串常量为final 是不可变的,导致了程序变慢。
str += i;
}
long endMs = System.currentTimeMillis();
System.out.println("+= 用时:" + ((endMs - startMs)/1000) + "秒");
long startMs1 = System.currentTimeMillis();
StringBuilder str1 = new StringBuilder("a");
for (int i = 0; i < 1000000; i++) {
str1.append(i);
}
str1.toString();
long endMs1 = System.currentTimeMillis();
System.out.println("StringBuilder.append() 用时:" + ((endMs1 - startMs1)/1000) + "秒");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
结果如下:
log
+= 用时: 332秒
StringBuilder.append() 用时: 0秒
1
2
2