运行时内存区域这块,如果不将内存各个区域做什么的了解清楚,后面看的会很累。
之前将JVM运行时内存区域的内容,整理在了一篇文章中。
在后续深入、细致的学习中,整理的内容越来越多,一篇的话,会导致篇幅过长。
所以将《JVM运行时内存区域详解》分为以下几个章节:
这里将《Java虚拟机规范中文版》上传了,点击下面链接,即可下载
目录
《Java Virtual Machine Specification Java SE 7 中文版》
《Java Virtual Machine Specification Java SE 7 中文版 - 第五章 加载、链接与初始化》
运行时常量池
《深入理解Java虚拟机:JVM高级特性与最佳实践》
运行时常量池(Runtime Constant Pool)是方法区的一部分。
Class文件中除了有类版本、字段、方法、接口等描述信息外,还有一项信息是常量池Constant Pool。
(注意:这里的常量池是Class文件中的,可以理解为Class文件常量池,每个Class文件都有,不要和JVM中的常量池混淆)
Java 虚拟机指令执行时不依赖与类、接口,实例或数组的运行时布局,而是依赖常量池(constant_pool)表中的符号信息。
当类或接口创建时,它的二进制表示中的 constant_pool 表被用来构造运行时常量池。运行时常量池中的所有引用最初都是符号引用。
以上,可以理解,类加载完成之后,所有类中的constant pool中的符号表,被添加到Runtime Constant Pool(运行时常量池)中。
对于运行时常量池,Java虚拟机规范并没有做任何细节的要求,不同厂商不同实现。
不过一般来说,除了保存Class文件中描述的符号应用外,还会把翻译出来的直接引用也存储在运行时常量池中。
Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池。
运行期间也可能将新的常量放入池中,比如:String类的intern()方法。
《Java Virtual Machine Specification Java SE 7 中文版》
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式。
它包含了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。
运行时常量池扮演了类似传统语言中符号表(Symbol Table)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。
每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
《Java Virtual Machine Specification Java SE 7 中文版 - 第五章 加载、链接与初始化》
Java 虚拟机为每个类型都维护一个常量池。
它是 Java 虚拟机中的运行时数据结构,像传统编程语言实现中的符号表一样有很多用途。
摘录书中关于字符常量的内容:
CONSTANT_String_info 结构包含了由Unicode 码点(Code points)序列来组成字符常量。
Java语言需要全局统一的字符常量(这就意味着如果不同字面量(Literal)包含着相同的码点序列,就必须引用着相同的 String 类的实例) 。
此外,在任意字符串上调用 String.intern 方法,如果那个字符串是字面量的话,方法的结果应当是对相同字面量的 String 实例的引用。因此:("a" + "b" + "c").intern() == "abc" 必须返回true。
不同字面量,如果是相同的Unicode码点序列,就会引用相同的实例。
举个栗子:
String str1 = "abc";
String str2 = "a"+"b"+"c";
str2.intern() == str1;
class编译之后,str1 和 str2 都是"abc"
根据这个栗子,再品味下上面的话:
这就意味着如果不同字面量(Literal)包含着相同的码点序列,就必须引用着相同的 String 类的实例
附件:常量池类型
CONSTANT_Class | 用于表示类或接口 |
CONSTANT_Fieldref、CONSTANT_Methodref、CONSTANT_InterfaceMethodref | 字段,方法和接口方法 |
CONSTANT_String | 用于表示 java.lang.String 类型的常量对象 |
CONSTANT_Integer、CONSTANT_Float | 表示 4 字节( int 和 float)的数值常量 |
CONSTANT_Long、CONSTANT_Double | 表示 8 字节( long 和 double)的数值常量 |
CONSTANT_NameAndType | 用于表示字段或方法(和上面的不是同一个) |
CONSTANT_Utf8 | 用于表示字符串常量的值 |
CONSTANT_MethodHandle | 用于表示方法句柄 |
CONSTANT_MethodType | 用于表示方法类型 |
CONSTANT_InvokeDynamic | 用于表示 invokedynamic 指令所使用到的引导方法( Bootstrap Method)、 引导方法使用到动态调用名称( Dynamic Invocation Name)、参数和请求返回类型、 以及可以选择性的附加被称为静态参数( Static Arguments) 的常量序列。 |
什么是字面量
在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
-----来自 百度百科
下面代码中"String str1 = 'abc' ",'abc'就是字面量,'123'、'123'、'1'、'10'也都是字面量。
什么是符号引用
符号引用属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
上面这种干巴巴的解释,自己看的都是一头雾水,网上找了点资料:https://www.zhihu.com/question/30300585
看下RednaxelaFX是如何解释的(基于RednaxelaFX的内容,又添加了查阅到的其他资料,想了解的看原文)
public class X {
public void foo() {
bar();
}
public void bar() { }
}
将这个java文件编译成class文件之后,使用javap -v 命令,查看汇编内容。
Classfile /C:/Users/CYX/Desktop/X.class
Last modified 2018-10-22; size 394 bytes
MD5 checksum ff677d972ff716aed189e3c7cf453ebc
Compiled from "X.java"
public class com.server.X
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
#2 = Methodref #3.#17 // com/server/X.bar:()V
#3 = Class #18 // com/server/X
#4 = Class #19 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lcom/server/X;
#12 = Utf8 foo
#13 = Utf8 bar
#14 = Utf8 SourceFile
#15 = Utf8 X.java
#16 = NameAndType #5:#6 // "<init>":()V
#17 = NameAndType #13:#6 // bar:()V
#18 = Utf8 com/server/X
#19 = Utf8 java/lang/Object
{
public com.server.X();
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 Lcom/server/X;
public void foo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method bar:()V
4: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/server/X;
public void bar();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/server/X;
}
SourceFile: "X.java"
在上面Class文本中,有一个Constant pool(常量池),里面存储了该Class文件里面的大部分常量的内容。
考察foo()方法里面的一条字节码指令:
1: invokevirtual #2 // Method bar:()V
这个字节码指令在代码中对应:bar();,来看下是啥意思
invokevirtual
对普通实例方法调用是在运行时根据对象类型进行分派的,这类方法通过调用invokevirtual指令实现。
每条invokevirtual指令都会带有一个表示索引的参数,运行时常量池在该索引的项为某个方法的符号引用。
这个符号引用可以提供方法所在对象类型的内部二进制名称、方法名称和方法描述。
"#2"这个参数是Class文件里常量池的下标,那么去Class常量池中找下标为2的常量池项:
#2 = Methodref #3.#17 // com/server/X.bar:()V
顺着后面的"#3"、"#14",把能传递引用到的常量池项都找出来
(按照深度优先进行排序)
1: invokevirtual #2 // Method bar:()V
#2 = Methodref #3.#17 // com/server/X.bar:()V
#3 = Class #18 // com/server/X
#18 = Utf8 com/server/X
#17 = NameAndType #13:#6 // bar:()V
#13 = Utf8 bar
#6 = Utf8 ()V
转换成关系树是这个样子
标记为Utf8的常量池项在Class文件中实际为CONSTANT_Utf8_info,是以略微修改过的UTF-8编码的字符串文本。
由此可以看出,Class文件中的invokevirtual指令的操作数经过几层间接之后,最后都是由字符串来表示的。
这就是Class文件里的“符号引用”的实态:带有类型(tag) / 结构(符号间引用层次)的字符串。
由此可见,符号引用通常是设计字符串的——用文本形式来表示引用关系。