java变量存储位置
前提
下面的东西都是在jdk1.8基础上做出的探究
首先我们先解释一下在各种网站上出现的一些名词到底是什么?
1.字面量:声明为final的int、long、double、char等基本类型的常量值(如final int a = 1 这是一个常量值,但是int a =1 这是一个变量值),特殊的字符串文本"abc",abc也是字面量,字面量一出生,其内容、大小全部固定,不会发生改变,编译时期存储在class文件的常量池中,可以在类加载阶段后存放到方法区的运行时常量池中。
class文件的常量池:一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。
2.符号引用:符号引用主要包含下面三类常量
(1)类和接口的全限定名(Fully Qualified Name)
(2)字段的名称和描述符(Descriptor)
(3)方法的名称和描述符
3.对象和引用:
对象是存储在堆内存中的实体,它包含了数据和方法,当创建一个对象时候,实际上在内存中分配了一块空间来存储该对象的数据。
引用是一个变量,它存储了一个对象的内存地址(即对象在堆内存中的位置)。这样你可以通过引用来操作和访问对象。Person p = new Person()person 是一个引用,然后指向的是堆内存中的对象实体。
4.虚拟机栈是jvm内存模型的一部分,方法区也是,但是jdk8中方法区实际实现是元空间,元空间在本地内存当中。元空间的大小仅受本地内存限制
5.元空间
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
探究方法区
我们就从我们classloader将class字节码文件记载到jvm内存中来进行探究,各种变量分别存储在了什么位置。
首先我们看一下方法区(逻辑概念)包含什么
方法区包含:
运行时常量池
自动和方法数据
构造函数和普通方法的字节码内容
一些特殊方法
这里虽然没有说明“字符串常量池”,但是它也是方法区的一部分。
取消永久代后,使用元空间来实现方法区。
在JDK1.8中,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。注意这里的剩余内容:说明原来移除从永久代移出的字符串常量池,静态常量,在更换了方法区实现后,并没有顺势进入到元空间,那么它们到哪里去了呢?
字符串常量池和运行时常量池究竟去了哪里?
在JDK1.8中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will always be your father",变动的只是方法区中内容的物理存放位置。正如上面所说,类型信息(元数据信息)等其他信息被移动到了元空间中;但是运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的。
JDK1.8中字符串常量池和运行时常量池逻辑上属于方法区,但是实际存放在堆内存中,因此既可以说两者存放在堆中,也可以说两则存在于方法区中,这就是造成误解的地方。
结论
好了按照上面的说法,我们在加载class字节码文件的时候,直接将字节码文件当中的常量池中的字面量和符号引用保存在了我们的方法区,保存在什么位置中了呢,运行时常量池。那么我们就有疑问了,那么符号引用所对应的对象存储在哪里了呢,存储在了堆中。
下面探究在加载class字节码时候不同位置的变量存储在什么地方
(1)对于局部变量肯定就是定义在方法中的变量,如果是基本类型,会把值直接存储在栈帧当中的局部变量表中,而栈帧所在位置就是虚拟机栈;如果是引用类型,会把这个对象的引用存储在栈帧当中,而对象实例会存储在堆中。栈帧退出去之后,堆当中的对象就没有其他的引用了,这时候就会被回收。
(2)而类的成员变量,不管是基本类型还是引用类型都存储在这个对象当中,这个对象存储在堆当中,所以就是存储在堆当中。
(3)对于静态变量,是在堆当中进行存储
· ②从JDK8.0开始,static修饰的成员变量位于堆空间中。
说明 : 当类加载器将含有static修饰的成员变量的类加载到方法区时,会根据反射机制生成一 个字节码文件对象,即Class对象。Class对象在堆空间中,而static变量保存在Class实例的尾部。如下图所示 : (即所有对象访问的某个类变量,其实就是那一份)
静态变量根本不能在方法中进行定义。
public class Test {
// 成员变量,存放在堆中
int a = 10;
// 被 static 修饰,也存放在堆中,但属于类,不属于对象 // JDK1.7 静态变量从永久代移动了 Java 堆中
static int b = 20;
public void method() {
// 局部变量,存放在栈中
int c = 30;
static int d = 40; // 编译错误,不能在方法中使用 static 修饰局部变量
}
}
下面探究各种常量池
字符串常量池
字符串常量池是什么?
在 HotSpot VM 里实现的 string pool 功能的是一个 StringTable 类,它是一个 Hash 表,默认值大小长度是1009;里面存的是驻留字符串的引用(而不是驻留字符串实例自身)。也就是说某些普通的字符串实例被这个 StringTable 引用之后就等同被赋予了“驻留字符串”的身份。这个 StringTable 在每个 HotSpot VM 的实例里只有一份,被所有的类共享。
StringTable 本质上就是个 HashSet<String>
。这是个纯运行时的结构,而且是惰性(lazy)维护的。注意它只存储对java.lang.String 实例的引用,而不存储 String 对象的内容。 注意,它只存了引用,根据这个引用可以得到具体的 String 对象。
关于字符串常量池 更加详细的信息,请看我的另一篇博客 java中的字符串池(创建字符串时的执行流程)-CSDN博客
class 文件常量池(class constant pool)
我们都知道,class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)
。 字面量比较接近 Java 语言层面常量的概念,如文本字符串、被声明为 final 的常量值等
。 符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
运行时常量池
运行时常量池是方法区的一部分。
当 Java 文件被编译成 class 文件之后,也就是会生成上面所说的 class 常量池,那么运行时常量池又是什么时候产生的呢?
JVM 在执行某个类的时候,必须经过加载、连接、初始化
,而连接又包括验证、准备、解析(resolve)三个阶段。而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面也说了,class 常量池中存的是字面量和符号引用
,也就是说它们存的并不是对象的实例,而是对象的符号引用值。而经过resolve 之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是上面所说的 StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
封装类常量池
以下几个封装类Byte,Short,Integer,Long,Character,Boolean在Java中实现了常量池(Double和Float没有)
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
不同封装类常量池的范围有限:
Byte,Short,Integer,Long : [-128~127]
Character : [0~127]
Boolean : [True, False]因此会产生以下差异
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2);Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);
结果是 True Flase
工作机制
关于 JVM 执行的时候,还涉及到了字符串常量池
。
在类加载阶段, JVM 会在堆中创建对应这些 class 文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用。具体在 resolve 阶段执行。这些常量全局共享。
这里说的比较笼统,没错,是 resolve 阶段(将运行时常量池中的符号引用转为直接引用 可能是堆当中地址),但是并不是大家想的那样,立即就创建对象并且在字符串常量池中驻留了引用。 JVM 规范里明确指定 resolve 阶段可以是 lazy 的。
VM 规范里 Class 文件常量池项的类型,有两种东西:CONSTANT_Utf8 和CONSTANT_String
。前者是 UTF-8 编码的字符串类型,后者是 String 常量的类型,但它并不直接持有 String 常量的内容,而是只持有一个 index,这个 index 所指定的另一个常量池项必须是一个 CONSTANT_Utf8 类型的常量,这里才真正持有字符串的内容。
在HotSpot VM中,运行时常量池里,
CONSTANT_Utf8 -> Symbol*(一个指针,指向一个Symbol类型的C++对象,内容是跟Class文件同样格式的UTF-8编码的字符串)
CONSTANT_String -> java.lang.String(一个实际的Java对象的引用,C++类型是oop)
CONSTANT_Utf8 会在类加载的过程中就全部创建出来,而 CONSTANT_String 则是 lazy resolve
的,例如说在第一次引用该项的 ldc 指令被第一次执行到的时候才会 resolve。那么在尚未 resolve 的时候,HotSpot VM 把它的类型叫做JVM_CONSTANT_UnresolvedString
,内容跟 Class 文件里一样只是一个 index;等到 resolve 过后这个项的常量类型就会变成最终的 JVM_CONSTANT_String
,而内容则变成实际的那个 oop。
看到这里想必也就明白了, 就 HotSpot VM 的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在 StringTable 中并没有相应的引用,在堆中也没有对应的对象产生)。所以上面提到的,经过 resolve 时,会去查询全局字符串池,最后把符号引用替换为直接引用。(即字面量和符号引用虽然在类加载的时候就存入到运行时常量池,但是对于 lazy resolve 的字面量,具体操作还是会在 resolve 之后进行的。)
通俗点讲就是 一开始加载字节码时候,在链接阶段实际上不会直接进行resolve,只有用到这个引用的时候才进行resolve,经过 resolve 时,会去查询全局字符串池,最后把符号引用替换为直接引用。具体resolve是如何查询的如下:
在遇到 String 类型常量时,resolve 的过程如果发现 StringTable 已经有了内容匹配的 java.lang.String 的引用,则直接返回这个引用;反之,如果 StringTable 里尚未有内容匹配的 String 实例的引用,则会在 Java 堆里创建一个对应内容的 String 对象,然后在 StringTable 记录下这个引用,并返回这个引用。
用图解的方式展示:
String s1 = "abc";
resolve 过程在字符串常量池中发现没有”abc“的引用,便在堆中新建一个”abc“的对象,并将该对象的引用存入到字符串常量池中,然后把这个引用返回给 s1。
String s2 = "abc";
resolve 过程会发现 StringTable 中已经有了”abc“对象的引用,则直接返回该引用给 s2,并不会创建任何对象。
String s3 = "xxx";
同第一行代码一样,在堆中创建对象,并将该对象的引用存入到 StringTable,最后返回引用给 s3。
总结
1、全局字符串常量池在每个 VM 中只有一份,存放的是字符串常量的引用值。
2、class 常量池是在编译的时候每个 class 都有的,在编译阶段,存放各种字面量和符号引用。
3、运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个 class 都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
4、class 文件常量池中的字符串字面量在类加载时进入到运行时常量池,在真正在 resolve 阶段(即执行 ldc 指令时)时将该字符串的引用存入到字符串常量池中,另外运行时常量池相对于 class 文件常量池具备动态性,有些常量不一定在编译期产生,也就是并非预置入 class 文件常量池的内容才能进入到方法区运行时常量池,运行期间通过 intern 方法,将字符串常量存入到字符串常量池中和运行时常量池
5.总结起来就是我们classsloader将class字节码文件加载到我们jvm方法区中,然后class当中的class文件常量池中的符号引用放在运行时常量池中,然后符号引用对应的对象存储在堆中,但是何时在堆中创建对象,何时将对象引用放在常量池中,有一个触发时机,是lazy的。
最后附上一张图
这是我个人的学习见解,如果有错误希望大佬指正。