探究Java常量本质及三种常量池(JVM)
简介: 探究Java常量本质及三种常量池
可以从他人的博文,还有一些书籍中了解到 常量是放在常量池 中,细节的内容无从得知,相信每个人都会觉得面前的东西是一个几乎完全的黑盒,总是觉得不舒服,翻阅《深入理解Java虚拟机》,会发现这本书中对常量的介绍更多地偏重于字节码文件的结构,还有在自动内存管理机制中也介绍了运行时常量池。下面换种思路来看一下
Java中的常量池分为三种形态:静态常量池,字符串常量池以及运行时常量池。
一、静态常量池
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 字段名称和描述符
- 方法名称和描述符
二、运行时常量池
运行时常量池,则是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_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: 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被放在了该类的常量池中
本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化
类的初始化 涉及到类的加载机制 这里暂时写不说 这个留到之后必须要好好说说
三、字符串常量池(string pool也有叫做string literal pool)
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)
字符串常量池的位置的说法不太准确:
- 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
- 在JDK7.0版本,字符串常量池被移到了堆中了。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身)
,也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
字符串常量池设计原理
字符串常量池底层是hotspot的C++实现的,底层类似一个 HashTable(Key-Value), 保存的本质上是字符串对象的引用。看一道比较常见的面试题,下面的代码创建了多少个 String 对象?
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
- 在 JDK 1.6 下输出是 false,创建了 6 个对象:s1(hello)、new String、he、new
String、llo、s2(hello);在调用s1.intern()的时候没有Hello,所以会在堆中创建一个hello,并复制到常量池中;- 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象:s1、new String、he、new
String、llo;在调用s1.intern()的时候没有Hello,就在常量池中创建一个引用指向堆中的hello中;当然这里没有考虑GC,但这些对象确实存在或存在过
为什么输出会有这些变化呢?主要还是字符串池从永久代中脱离、移入堆区的原因, intern() 方法也相应发生了变化:
在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal()
相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将StringTable 的一个表项指向这个新创建的实例。
在 JDK 1.7(及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和JDK1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
由上面两个图,也不难理解为什么 JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在JDK 1.7 及以上版本抛出 OutOfMemoryError: Java heap space 。
四、回到运行常量池(runtime constant pool)
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_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnstatic {};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 code14: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V17: 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_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: bipush 613: invokevirtual #6 // Method java/io/PrintStream.println:(I)V16: 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_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: bipush 613: invokevirtual #6 // Method java/io/PrintStream.println:(I)V16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;19: sipush 12822: invokevirtual #6 // Method java/io/PrintStream.println:(I)V25: 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_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: bipush 613: invokevirtual #6 // Method java/io/PrintStream.println:(I)V16: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;19: sipush 12822: invokevirtual #6 // Method java/io/PrintStream.println:(I)V25: 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_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #4 // String Hello,world5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;11: iconst_112: invokevirtual #6 // Method java/io/PrintStream.println:(I)V15: return
}
这里变成了 iconst_1
iconst 1表示将int类型1推送至栈顶(iconst_m1-iconst_5)
当大于5的时候 就变为了bipush
m1对应的是-1
五、八种基本类型的包装类和对象池
java中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。因为一般这种比较小的数用到的概率相对较大。
public class Test {public static void main(String[] args) {//5种整形的包装类Byte,Short,Integer,Long,Character的对象,//在值小于127时可以使用对象池Integer i1 = 127; //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池Integer i2 = 127;System.out.println(i1 == i2);//输出true//值大于127时,不会从对象池中取对象Integer i3 = 128;Integer i4 = 128;System.out.println(i3 == i4);//输出false//用new关键词新生成对象不会使用对象池Integer i5 = new Integer(127);Integer i6 = new Integer(127);System.out.println(i5 == i6);//输出false//Boolean类也实现了对象池技术Boolean bool1 = true;Boolean bool2 = true;System.out.println(bool1 == bool2);//输出true//浮点类型的包装类没有实现对象池技术Double d1 = 1.0;Double d2 = 1.0;System.out.println(d1 == d2);//输出false}
}
参考文章1
参考文章2
探究Java常量本质及三种常量池(JVM)相关推荐
- JVM 三种常量池:Class、字符串、基本类型
本文主要讲述三种常量池: Class 常量池.字符串常量池.还有基本类型常量池. 默认 jdk 版本:jdk 1.8 Class 常量池 Class 常量池可以理解为 Class 文件中的资源仓库, ...
- JAVA的三种常量池
Java有三种常量池,即字符串常量池(又叫全局字符串池).class文件常量池.运行时常量池. 1. 字符串常量池(也叫全局字符串池.string pool.string literal pool) ...
- java long常量池_Java-常量池
Java-常量池 常量池是类文件中最复杂的数据结构.对于JVM字节码来说,如果操作数是很常用的数字,比如 0,这些操作数是内嵌到字节码中的.如果是字符串常量和较大的整数等,Class文件则会把这些操作 ...
- java dateformat类_JAVA--常量池,Date类,SimpleDateFormat类与Calendar类
String常量池: JVM在使用字符串类型时,为了提高内存使用效率,当使用字面量(常量)给变量赋值时,在方法区内提供了用于存储字面量对象的一个常量池. 原理: 当使用字面量赋值时,先去方法区内的常量 ...
- JAVA8常量池监控_深入探索Java常量池
Java的常量池通常分为两种:静态常量池和运行时常量池 静态常量池:class文件中的常量池,class文件中的常量池包括了字符串(数字)字面值,类和方法的信息,占用了class文件的大部分空间. 运 ...
- Java常量池的大概理解
触摸java常量池 java常量池是一个经久不衰的话题,也是面试官的最爱,题目花样百出,小菜早就对常量池有所耳闻,这次好好总结一下. 理论 小菜先拙劣的表达一下jvm虚拟内存分布: 程序计数器是jvm ...
- Java常量设计与常量池
常量设计 常量:一种不会修改的变量 Java是没有const关键字 不能修改,final 不会修改/只读/只要一份,static 方便访问,public Java中的常量 public static ...
- java常量池的理解_Java常量池的大概理解
转载自:http://www.cnblogs.com/iyangyuan/p/4631696.html 理论 小菜先拙劣的表达一下jvm虚拟内存分布: 程序计数器是jvm执行程序的流水线,存放一些跳转 ...
- Java常量池[乐乐独记]
Java常量池[乐乐独记] 1.字面量和符号引用 1.1.字面量 1.2.符号引用 2.常量池 2.1.静态常量池 2.2.运行时常量池 2.3.字符串常量池 2.3.1.字符串常量池的概念 2.3. ...
最新文章
- Python3学习之路
- C++ 中常用数学函数
- The type android.support.v4.view.ScrollingView cannot be resolved. It is indirectly referenced from
- 《linux内核设计与实现》读书笔记第一、二章
- MySQL小问题:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents...
- 左手手机右手智慧屏 华为9月要搞大事情
- LoadRunner接口工作总结
- Medieval Rampage
- iOS开源项目周报0302
- 【SpringBoot_ANNOTATIONS】自动装配 05 @Profile环境搭建
- vba移动文件_你想要的爬虫、VBA系列教程这里都有!
- maven的下载与安装教程(超详细)
- 百度地图做电子围栏总结
- NLP+句法结构(三)︱中文句法结构(CIPS2016、依存句法、文法)
- EXCEL之绝对引用、相对引用和混合引用
- Visual Studio .Net团队开发(Visual SourceSave6.0 在C#中的应用)
- 快看快看,这款免费的低代码平台绝绝子
- 为什么能力越低的人,越容易产生对自己过高的评价?
- 图片+文案(在图片上)
- 虚拟机中如何给Ubuntu系统进行联网