首页 > 应用开发 > > Java中的几种常量池
1. 运行时常量池:方法区的一部分,存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储到运行时常量池中。运行时常量池具备动态性,也就是并非预置入 Class 文件的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中。
jvm 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
而当类加载到内存中后,jvm 就会将静态常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
静态常量池中存的是字面量和符号引用,也就是说它们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve )之后,也就是把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的 StringTable ,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的。
我们看一个例子
import java.util.UUID;
public class Test {
public static void main(String[] args) {
System.out.println(TestValue.str);
}
}
class TestValue{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("TestValue static code");
}
}
结果:
从声明本身str 都是常量,关键的是这个常量的值能否在编译时期确定下来,显然这里的例子在编译期的时候显然是确定不下来的。需要在运行期才能能够确定下来,这要求目标类要进行初始化
当常量的值并非编译期间可以确定的,那么其值不会被放到调用类的常量池中
这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化。
(这个涉及到类的加载机制,后面会写这里做个标记)
反编译探究一下:
Compiled from "Test.java"
class com.leetcodePractise.tstudy.TestValue {
public static final java.lang.String str;
com.leetcodePractise.tstudy.TestValue();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static {};
Code:
0: invokestatic #2 // Method java/util/UUID.randomUUID:()Ljava/util/UUID;
3: invokevirtual #3 // Method java/util/UUID.toString:()Ljava/lang/String;
6: putstatic #4 // Field str:Ljava/lang/String;
9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
12: ldc #6 // String TestValue static code
14: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return
}
很明显TestValue 类会初始化出来
常量介绍完之后 这里记录一下反编译及助记符的笔记
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
System.out.println(Father.s);
}
}
class Father{
public static final String str = "Hello,world";
public static final short s = 6;
static {
System.out.println("Father static block");
}
}
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: bipush 6
13: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
16: return
}
bipush 表示将单字节( -128-127 )的常量值推送至栈顶
再加入
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
System.out.println(Father.s);
System.out.println(Father.t);
}
}
class Father{
public static final String str = "Hello,world";
public static final short s = 6;
public static final int t = 128;
static {
System.out.println("Father static block");
}
}
进行反编译
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: bipush 6
13: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: sipush 128
22: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
25: return
}
sipush 表示将一个短整型常量值( -32768~32767 )推送至栈顶
再进行更改
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
System.out.println(Father.t);
}
}
class Father{
public static final String str = "Hello,world";
public static final int t = 1;
static {
System.out.println("Father static block");
}
}
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: bipush 6
13: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
19: sipush 128
22: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
25: return
}
D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iconst_1
12: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
15: return
}
这里变成了 iconst_1
2. 字符串常量池:本质是一个 HashSet<String> ,这是一个纯运行时的结构,而且是惰性维护的。注意它只存储 String 对象的引用,而不存储 String 对象的内容,根据这个引用可以得到具体的 String 对象。
3.Class 常量池:主要存放两大类常量:字面量和符号引用。加载 Class 文件时, Class 文件中 String 对象会进入字符串常量池(这里的进入是指 放入字符串的引用,字符串本身还是在堆中),别的大都会进入运行时常量池。
字面量比较接近Java 语言层面常量的概念,如文本字符串、声明为 final 的常量值
符号引用属于编译原理的概念:
类和接口的全定限名
字段的名称和描述符
方法的名称和描述符
符号引用将在解析阶段被替换为直接引用。因为Java 代码在进行编译时,并不像 C 那样有 " 连接 " 这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接。也就是说, Class 文件不会保存外汇返佣http://www.kaifx.cn/各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期间 转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
而运行时常量池,则是jvm 虚拟机在完成类装载操作后,将 class 文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性, Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是 String 类的 intern() 方法。
String 的 intern() 方法会查找在常量池中是否存在一份 equal 相等的字符串 , 如果有则返回该字符串的引用 , 如果没有则添加自己的字符串进入常量池。
那这样来看,通过静态常量池,即*.class 文件中的常量池 更能够探究常量的含义了
下面看一段代码
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
}
}
class Father{
public static String str = "Hello,world";
static {
System.out.println("Father static block");
}
}
输出结果为
再看另一个:
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Father.str);
}
}
class Father{
public static final String str = "Hello,world";
static {
System.out.println("Father static block");
}
}
结果:
只有一个
是不是发现很吃惊啊
我们对第二个演示的代码块进行反编译一下
D:\CodePractise\untitled\out\production\untitled\com\company>javap -c Main.class
Compiled from "Main.java"
public class com.company.Main {
public com.company.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello,world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
这里有一个Main() 是构造方法 下面的是 main 方法
0: getstatic # 2 对应的是 System.out
3: ldc #4 对应的值 直接是 Hello,world 了 确定的值 没有从 Father 类中取出
ldc 表示将 int , float 或是 String 类型的常量值从常量池中推送至栈顶
竟然没有!!! 即使删除Father.class 文件 这段代码照样可以运行 它和 Father 类 没有半毛钱的关系了
实际上,在编译阶段 常量就会被存入到调用这个常量的方法所在的类的常量池当中
从这个例子中 可以看出 这里的str 是一个常量 调用这个常量的方法是 main 方法 main 方法所在的类是 Main ,也就是说编译之后 str 被放在了该类的常量池中
本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化
4.String 的 intern 方法:
JDK7 中,如果字符串常量池中已经有了这个字符串,那么直接返回常量池中的它的引用,如果没有,那就将它的引用保存一份到字符串常量池,然后直接返回这个引用。
5. 字面量进入字符串常量池的时机:
就HotSpot VM 的实现来说,加载类的时候,那些字符串字面 量会进入当前类的运行时常量池,不会进入全局字符串常量池 ( 即在字符串常量池中没有相应的引用,在堆中也没有生成对应的对象 ) 。加载类的时,没有解析字符串字面量,等到执行 ldc 指令的时候就会触发这个解析的动作。 ldc 指令的语义是:到当前类的运行时常量池区查找该 index 对应的项,如果该项没有解析就解析,并返回解析后的内容。在遇到 String 类型常量时,解析的过程是如果发现字符串常量池中已经有了内容匹配的 String 类型的引用,就直接返回这个引用,如果没有内容匹配的 String 实例的引用,就会在 Java 堆中创建一个对应内容的 String 对象,然后在字符串常量池中记录下这个引用。
说明:自己的一点理解,上面说的时对字符串的解析,其实对方法解析也是类似,有些方法也是lazy resolve ,有一部分符号引用是在类加载阶段或者第一次使用的时候就转化为直接引用,被称为静态解析(例如静态方法、私有方法等非虚方法),另一部分将在每一次运行期间转换为直接引用,被称为动态连接(例如静态分派),这部分也是 lazy resolve 。
6. 例题分析:
例1 :
class Test{
public static String s1 = "static";
public static void main(String[] args) {
String s2 = new String("he")+new String("llo");
s2.intern();
String s3 = "hello";
System.out.println(s2==s3); //true
}
}
"static" "he" "llo" "hello" 都会进入 Class 常量池,类加载阶段由于解析阶段时 lazy 的,所以不会创建实例,更不会驻留字符串常量池。但要注意这个“ static" 和其他三个不一样,它是静态的,在加载阶段的初始化阶段,会为静态遍历执行初始值,也就是将 "static" 赋值给 s1 ,所以会创建 "static" 字符串对象, 并且会保存一个指向它的引用到字符串常量池。
运行main 方法后,执行 String s2 = new String("he")+new String("llo") 语句,创建 "he" 和 "llo" 的对象,并会保存引用到字符串常量池中,然后内部创建一个 StringBuilder 对象,一路 append, 最后调用 toString() 方法得到一个 String 对象(内容时 hello, 注意这个toString 方法会 new 一个 String 对象),并把它赋值给 s2 (注意这里没有把 hello 的引用放入字符串常量池)。
然后执行语句:s1.intern() ,此时字符串常量池中没有,它会将上面的这个 hello 对象的引用保存到字符串常量池,然后返回这个引用,但是这个返回的引用没有变量区接收,所以没用。
然后执行:String s3 = "hello" 因为字符串常量池中已经有了,所以直接指向堆中 "hello" 对象
然后执行:System.out.println(s2==s3) ,此时返回 true 。
例题2 :
class JianZhiOffer{
public static void main(String[] args) {
String s1 = new String("he")+new String("llo"); // 第一句
String s2 = new String("h")+new String("ello"); // 第二句
String s3 = s1.intern(); // 第三句
String s4 = s2.intern(); // 第四句
System.out.println(s1==s3); // 第五句
System.out.println(s1==s4); // 第六句
}
}
类加载阶段,什么都没干。
第一句: 创建 "he" 和 "llo" 对象,并放入字符串常量池,然后创建 "hello" 对象,没有放入字符串常量池, s2 指向这个 "hello"对象。
第二句: 创建了 "h" 和 "ello" 对象,并放入字符串常量池,然后创建 "hello" 对象,没有放入字符串常量池, s3 指向这个"hello" 对象。
第三句: 字符串常量池中没有 "hello" ,所以会把 s1 指向 String 对象的引用放入字符串常量池,然后将这个引用返回给了 s3 ,所以 s1==s3 是 true
第四句:字符串常量池中有了"hello" ,所以将 s4 指向的 s3 指向的对象 "hello" ,所以第六句 s4==s1 是 true 。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/4548/viewspace-3162425/ ,如需转载,请注明出处,否则将追究法律责任。