运行时栈帧结构
栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈数据区的组成元素。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。
每一个栈帧在编译程序代码的时候所需要多大的局部变量表,多深的操作数栈都已经决定了,并且写入到方发表的 Code 属性之中,一次一个栈帧需要多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。
典型的栈帧主要由 局部变量表(Local Stack Frame)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、返回地址(Return Address)组成,如下图所示:
public static void main(String[] args) { String s1 = "Hello"; String s2 = "Hello";
//s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好, String s3 = "Hel" + "lo"; //Hel被包装为对象,lo被包装为对象, //然后调用 StringBuilder.append方法, //然后调用String.toStringui方法, //存入变量s4 String s4 = "Hel" + new String("lo"); //调用new 生成新对象 String s5 = new String("Hello"); String s6 = s5.intern(); String s7 = "H"; String s8 = "ello"; String s9 = s7 + s8; System.out.println(s1 == s2); // true System.out.println(s1 == s3); // true System.out.println(s1 == s4); // false System.out.println(s1 == s9); // false System.out.println(s4 == s5); // false System.out.println(s1 == s6); // true }
其对应指令集如下:
0: ldc #5 // String Hello 2: astore_1 3: ldc #5 // String Hello 5: astore_2 6: ldc #5 // String Hello 8: astore_3 9: new #6 // class java/lang/StringBuilder 12: dup 13: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 16: ldc #8 // String Hel 18: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: new #10 // class java/lang/String 24: dup 25: ldc #11 // String lo 27: invokespecial #12 // Method java/lang/String."<init>":(Ljava/lang/String;)V 30: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 33: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 36: astore 4 38: new #10 // class java/lang/String 41: dup 42: ldc #5 // String Hello 44: invokespecial #12 // Method java/lang/String."<init>":(Ljava/lang/String;)V 47: astore 5 49: aload 5 51: invokevirtual #14 // Method java/lang/String.intern:()Ljava/lang/String; 54: astore 6 56: ldc #15 // String H 58: astore 7 60: ldc #16 // String ello 62: astore 8 64: new #6 // class java/lang/StringBuilder 67: dup 68: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 71: aload 7 73: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 76: aload 8 78: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 81: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 84: astore 9
常量池:
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),
字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,
符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 字段名称和描述符
- 方法名称和描述符
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。