1.运行时常量池:方法区的一部分,存放编译器生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池。通常来讲,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储到运行时常量池中。运行时常量池具有动态性,也就是并不是预置入Class文件的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中。java
jvm在执行某个类的时候,必须通过加载、链接、初始化,而链接又包括验证、准备、解析三个阶段。app
而当类加载到内存中后,jvm就会将静态常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每一个类都有一个。dom
静态常量池中存的是字面量和符号引用,也就是说它们存的并非对象的实例,而是对象的符号引用值。而通过解析(resolve)以后,也就是把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是咱们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的。jvm
咱们看一个例子布局
import java.util.UUID;ui
public class Test {翻译
public static void main(String[] args) {code
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。