一、常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进去方法区的运行时常量池中存放。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如
String类的intern()方法。
常量池中主要存放两大类常量:
字面量(Literal)和
符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如
文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
二、JVM处理String
public class JvmTest {
private String c = "hello";
private static String d = "hello";
public static void main(String[] args) {
String a = "haha";
String b = new String("haha");
}
}
编译:javac JvmTest.java
反编译:javap -v JvmTest
public class spring.proxy.JvmTest
minor version: 0 //class 次版本号
major version: 53 //class 主版本号
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // spring/proxy/JvmTest
super_class: #9 // java/lang/Object
interfaces: 0, fields: 2, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #9.#22 // java/lang/Object."<init>":()V
#2 = String #23 // hello
#3 = Fieldref #8.#24 // spring/proxy/JvmTest.c:Ljava/lang/String;
#4 = String #25 // haha
#5 = Class #26 // java/lang/String
#6 = Methodref #5.#27 // java/lang/String."<init>":(Ljava/lang/String;)V
#7 = Fieldref #8.#28 // spring/proxy/JvmTest.d:Ljava/lang/String;
#8 = Class #29 // spring/proxy/JvmTest
#9 = Class #30 // java/lang/Object
#10 = Utf8 c
#11 = Utf8 Ljava/lang/String;
#12 = Utf8 d
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 <clinit>
#20 = Utf8 SourceFile
#21 = Utf8 JvmTest.java
#22 = NameAndType #13:#14 // "<init>":()V
#23 = Utf8 hello
#24 = NameAndType #10:#11 // c:Ljava/lang/String;
#25 = Utf8 haha
#26 = Utf8 java/lang/String
#27 = NameAndType #13:#31 // "<init>":(Ljava/lang/String;)V
#28 = NameAndType #12:#11 // d:Ljava/lang/String;
#29 = Utf8 spring/proxy/JvmTest
#30 = Utf8 java/lang/Object
#31 = Utf8 (Ljava/lang/String;)V
{
public spring.proxy.JvmTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String hello
7: putfield #3 // Field c:Ljava/lang/String;
10: return
LineNumberTable:
line 8: 0
line 10: 4
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #4 // String haha
2: astore_1
3: new #5 // class java/lang/String
6: dup
7: ldc #4 // String haha
9: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: return
LineNumberTable:
line 15: 0
line 16: 3
line 17: 13
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #2 // String hello
2: putstatic #7 // Field d:Ljava/lang/String;
5: return
LineNumberTable:
line 12: 0
}
8-39行:常量池
41-54:默认构造方法
46: aload_0 指令是把0索引的局部变量进栈 this
47: invokespecial调用父类构造方法
48: ldc指令格式:ldc,index
ldc指令过程:要执行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的常量池入口,JVM将会查找CONSTANT_Integer_info,CONSTANT_Float_info和CONSTANT_String_info入口。如果还没有这些入口,JVM会解析它们。而对于上面的haha,JVM在运行时环境中解析ldc指令时,会找到CONSTANT_String_info入口,同时,将把指向被拘留字符串对象(由解析该入口的进程产生)的引用压入操作数栈。
50: putfield给实例变量c赋值
61-62: 把常量池中的haha字符串压入操作数栈,然后出栈将该值存入由索引1指定的局部变量中。
62: astore_1指令格式:astore_1
astore_1指令过程: 要执行astore_1指令,JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1。
63: new指令格式:new indexbyte1,indexbyte2
new指令过程:要执行new指令,Jvm通过计算(indextype1<<8)|indextype2生成一个指向常量池的无符号16位索引。然后JVM根据计算出的索引查找常量池入口。该索引所指向的常量池入口必须为CONSTANT_Class_info。如果该入口尚不存在,那么JVM将解析这个常量池入口,该入口类型必须是类。JVM从堆中为新对象映像分配足够大的空间,并将对象的实例变量设为默认值。最后JVM将指向新对象的引用objectref压入操作数栈。
64: dup指令格式:dup
dup指令过程:要执行dup指令,JVM复制了操作数栈顶部一个字长的内容,然后再将复制内容压入栈。本指令能够从操作数栈顶部复制任何单位字长的值。但绝对不要使用它来复制操作数栈顶部任何两个字长(long型或double型)中的一个字长。上面例中,即复制引用objectref,这时在操作数栈存在2个引用。
66: invokespecial调用String构造方法
80: putstatic赋值静态变量d
总结:实例变量在构造方法中初始化。
静态变量在静态代码块中初始化。
三、案例分析
案例一
String a = "a1";
String aa = "a" + 1;
System.out.println((a == aa)); //result = true
String ab = "ab";
String b = "b";
String bb = "a" + b;
System.out.println((bb == ab)); //result = false
String c = "ab";
final String cc = "b";
String ccc = "a" + cc;
System.out.println((ccc == c)); //result = true
分析:
1.
“a”和1都是常量,在编译期间JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。故第一个是true。
2. 由于在字符串的"+"连接中,有字符串引用存在,而
引用的值在程序编译期是无法确定的,即"a" + b无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给bb。所以上面程序的结果也就为false。
3. 对于
final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + cc和"a" + "b"效果是一样的。故上面程序的结果为true。
案例二
String a = "a";
String b = "b";
String c = a + b;
0: ldc #16; //String a //将常量池中的a压入操作数栈
2: astore_1 //将引用a存放到1号局部变量中
3: ldc #18; //String b //将常量池中的b压入操作数栈
5: astore_2 //将引用b存放到2号局部变量中
6: new #20; //class java/lang/StringBuilder //检查到非常量的相加,这时创建 StringBuilder 对象,将引用放入栈
9: dup //复制刚放入的引用(这时存在着两个相同的引用)
10: aload_1 //从1号局部变量中加载数据引用a到操作数栈中
11: invokestatic #22; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; //调用String类的valueOf方法
14: invokespecial #28; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V //对StringBulider对象进行一些初始化
17: aload_2 //从2号局部变量中加载数据引用b到操作数栈中
18: invokevirtual #31; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //调用 StringBuilder的append方法,把字符串b添加进去
21: invokevirtual #35; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; //调用 StringBuilder的toString方法
24: astore_3 //将toString的结果保存至3号局部变量
25: return //结束程序返回
分析:可以看出,[2]的流程其实等价于下面JAVA代码:
String c = new StringBuilder().append("a").append(b).toString();
案例三
String a = "ab";
String s1 = "a";
String s2 = "b";
String s = s1 + s2;
System.out.println(s == a);//false
System.out.println(s.intern() == a);//true
分析:
1. a引用的“ab”在
常量池,s是StringBuilder.toString()中新new了一个String,在
堆中分配对象。故第一个判断的结果为false。
2. 当调用 intern 方法时,如果常量池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。故第二个判断结果为true。
分享到:
相关推荐
在Java虚拟机(JVM)中,`String.intern()` 是一个非常特殊且重要的方法,它与字符串常量池紧密相关。字符串常量池是一种内存优化机制,存储了程序中所有的字符串字面值和通过 `intern()` 方法添加的字符串。这个池...
本资源是关于JVM指令的详细手册,涵盖了JVM指令的各种系列命令,包括未归类系列、const系列、push系列、ldc系列、load系列等。每个系列命令都有其特定的功能和用途,下面我们将逐一详细介绍每个系列命令的作用和用法...
Java虚拟机(JVM)是Java技术的核心组成部分之一,它为Java程序提供了运行时环境。本节将详细介绍JVM的基本概念及其核心技术——HotSpot。 **HotSpot VM** 是Oracle公司提供的一个高性能Java虚拟机实现,它具有以下...
JVM提供了一系列工具,如JConsole、VisualVM等,用于监控和分析JVM的运行状态,帮助开发者进行性能调优。 通过深入学习《JVM8虚拟机规范》,开发者可以更好地理解Java程序的运行机制,提高代码质量,有效避免内存...
Java虚拟机(JVM)提供了一系列内置的监控和诊断工具,可以帮助开发者和运维人员高效地获取和分析这些信息。本篇文章将详细介绍以下几个核心的JVM命令工具及其使用方法: 1. **jps** - 显示系统中所有HotSpot虚拟机...
### JVM指令手册知识点 #### 概述 JVM(Java虚拟机)是执行Java字节码的...JVM指令手册是Java开发者必备的参考资料之一,通过学习手册中的指令,开发者可以更好地理解Java程序的执行过程,优化代码,解决性能问题。
字节码是JVM能够识别和执行的一系列指令和数据,是Java平台无关性的基础,因为它允许Java程序在不同的操作系统上运行。字节码使得Java成为一种高级的、平台无关的编程语言。 从提供的部分内容来看,JVM指令集包括...
ldc系列指令用于将常量(如int、float、long、double类型或String类型的常量)从常量池中推送到栈顶。对于更大的常量值,还有ldc_w和ldc2_w指令,它们分别是宽索引版本,可以处理更大范围的常量池索引。 变量加载...
栈由一系列栈帧组成,每个栈帧对应一次方法调用。栈是一种后进先出(LIFO)的数据结构,其中最新进入的栈帧位于栈顶。 3. **本地栈(Native Stack)** - 非所有JVM实现都支持本地方法,但大多数现代JVM会为每个线程...
在这个方法中,我们需要执行一系列操作,例如检查类是否已加载,是否是系统类,从特定位置加载类,定义类,解析类等。 特别地,JVM有一个内置的根装载器(bootstrap ClassLoader),它加载的是Java核心库中的类,被...
JVM指令是由一系列的16进制码组成的,每种指令都有对应的助记符,用于实现特定的操作。深入理解JVM指令对于优化Java代码性能、排查问题以及理解Java运行机制至关重要。 1. **未归类系列**: - `nop` 指令:无操作...
Hawkins是一位资深的虚拟机工程师,通过一系列实例解释了JVM中的关键概念和技术细节。 #### JVM的核心工作流程 JVM的核心工作流程包括四个主要阶段: 1. **解释执行** (Interpret):JVM接收到字节码后首先会通过...
- `ldc` 可以加载int、float或String类型的常量,常量池中的编号作为参数。 - `ldc_w` 同样加载这些类型的常量,但使用宽索引,支持更大的常量池索引。 - `ldc2_w` 用于加载long或double类型的常量,同样使用宽...
JAVA 面试题解惑系列之 String 对象创建机制 本文将深入探讨 JAVA 中 String 对象的创建机制,解答常见的面试题目,并探索 String 对象池的概念和机制。 一、String 对象的创建方式 在 JAVA 中,String 对象可以...
Java 基础系列: Java 基础核心总结 Java 代理 Java 反射 Java 集合 String、StringBuffer 和 StringBuilder Java 中的语法糖 深入理解 static 关键字 深入理解 Java 变量 深入理解 final、...JVM 系列 Linux 系列
JavaTutorial 突击 Java 系列教程、JVM系统学习之路系列 公众号 欢迎微信搜索「山间木匠」,定期分享 JAVA 相关知识,回复【面试】即可获得面试系列...【JVM系统学习之路系列】StringTable 【JVM系统学习之路系列】
"java-JVM-面试题从基础到高级详解-HM"这个资料很可能是涵盖了从基础概念到复杂问题的一系列JVM面试题目,旨在帮助求职者全面准备JVM相关的面试。 一、JVM基础 1. **JVM架构**:JVM主要包括类加载器、运行时数据区...
在Java语言中,String类是用于处理文本的基础类,它封装了char数组,并提供了一系列方法来操作字符串。本部分将详细介绍String类在Java编程中的应用和相关知识点。 首先,字符串在Java中是一个对象类型,可以不需要...