`

理解Java常量池--从JVM原理上理解字符串的比较

阅读更多

http://www.zz-jb.com/accp/xinwenxiangxi.asp?bianhao=559

 

 理解Java常量池--从JVM原理上理解字符串的比较                  

                                                                                    株洲北大青鸟    李栋

JVM运行时数据区的内存模型由五部分组成:

1】方法区
2】堆
3JAVA
4PC寄存器
5】本地方法栈

对于String s = "haha" ,它的虚拟机指令:
0:   ldc     "16; //String haha   
2:   astore_1
3:   return

对于上面虚拟机指令,其各自的指令流程在《深入JAVA虚拟机》这样描述到(结合上面实例)

ldc指令格式:ldc,index

ldc指令过程:

要执行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的常量池入口,JVM将会查找CONSTANT_Integer_infoCONSTANT_Float_infoCONSTANT_String_info入口。如果还没有这些入口,JVM会解析它们。而对于上面的hahaJVM会找到CONSTANT_String_info入口,同时,将把指向被拘留String对象(由解析该入口的进程产生)的引用压入操作数栈。

astore_1指令格式:astore_1

astore_1指令过程:

要执行astore_1指令,JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1

return 指令的过程:

从方法中返回,返回值为void

谈一下我个人理解:

从上面的ldc指令的执行过程可以得出:s的值是来自被拘留String对象(由解析该入口的进程产生)的引用,即可以理解为是从被拘留String对象的引用复制而来的,故我个人的理解是s的值是存在栈当中。上面是对于s值得分析,接着是对于"haha"值的分析,我们知道,对于String s = "haha" 其中"haha"值在JAVA程序编译期就确定下来了的。简单一点说,就是haha的值在程序编译成class文件后,就在class文件中生成了(大家可以用UE编辑器或其它文本编辑工具在打开class文件后的字节码文件中看到这个haha值)。执行JAVA程序的过程中,第一步是class文件生成,然后被JVM装载到内存执行。那么JVM装载这个class到内存中,其中的haha这个值,在内存中是怎么为其开辟空间并存储在哪个区域中呢?

说到这里,我们不妨先来了解一下JVM常量池这个结构,《深入JAVA虚拟机》书中有这样的描述:

常量池

虚拟机必须为每个被装载的类型维护一个常量池常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integerfloating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。

在介绍完JVM常量池的概念后,接着谈开始提到的"haha"的值的内存分布的位置。对于haha的值,实际上是在class文件被JVM装载到内存当中并被引擎在解析ldc指令并执行ldc指令之前,JVM就已经为haha这个字符串在常量池CONSTANT_String_info表中分配了空间来存储haha这个值。既然haha这个字符串常量存储在常量池中,根据《深入JAVA虚拟机》书中描述:常量池是属于类型信息的一部分,类型信息也就是每一个被转载的类型,这个类型反映到JVM内存模型中是对应存在于JVM内存模型的方法区中,也就是这个类型信息中的常量池概念是存在于在方法区中,而方法区是在JVM内存模型中的堆中由JVM来分配的。所以,haha的值是应该是存在堆空间中的。

而对于String s = new String("haha") ,它的JVM指令:
0:   new     "16; //class String
3:   dup
4:   ldc     "18; //String haha
6:   invokespecial   "20; //Method java/lang/String."":(Ljava/lang/String;)V
9:   astore_1
10:  return

对于上面虚拟机指令,其各自的指令流程在《深入JAVA虚拟机》这样描述到(结合上面实例)

new指令格式:new indexbyte1,indexbyte2

new指令过程:

要执行new指令,Jvm通过计算(indextype1<<8)|indextype2生成一个指向常量池的无符号16位索引。然后JVM根据计算出的索引查找常量池入口。该索引所指向的常量池入口必须为CONSTANT_Class_info。如果该入口尚不存在,那么JVM将解析这个常量池入口,该入口类型必须是类。JVM从堆中为新对象映像分配足够大的空间,并将对象的实例变量设为默认值。最后JVM将指向新对象的引用objectref压入操作数栈。

dup指令格式:dup

dup指令过程:

要执行dup指令,JVM复制了操作数栈顶部一个字长的内容,然后再将复制内容压入栈。本指令能够从操作数栈顶部复制任何单位字长的值。但绝对不要使用它来复制操作数栈顶部任何两个字长(long型或double)中的一个字长。上面例中,即复制引用objectref,这时在操作数栈存在2个引用。

ldc指令格式:ldc,index

ldc指令过程:

要执行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的常量池入口,JVM将会查找CONSTANT_Integer_infoCONSTANT_Float_infoCONSTANT_String_info入口。如果还没有这些入口,JVM会解析它们。而对于上面的haha,JVM会找到CONSTANT_String_info入口,同时,将把指向被拘留String对象(由解析该入口的进程产生)的引用压入操作数栈。

invokespecial指令格式:invokespecial,indextype1,indextype2

invokespecial指令过程:对于该类而言,该指令是用来进行实例初始化方法的调用。鉴于该指令篇幅,具体可以查阅《深入JAVA虚拟机》中描述。上面例子中,即通过其中一个引用调用String类的构造器,初始化对象实例,让另一个相同的引用指向这个被初始化的对象实例,然后前一个引用弹出操作数栈。

astore_1指令格式:astore_1

astore_1指令过程:

要执行astore_1指令,JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1

return 指令的过程:

从方法中返回,返回值为void

要执行astore_1指令,JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1


通过上面6个指令,可以看出,String s = new String("haha");中的haha存储在堆空间中,而s则是在操作数栈中。
上面是对shaha值的内存情况的分析和理解;那对于String s = new String("haha");语句,到底创建了几个对象呢?
我的理解:这里"haha"本身就是常量池中的一个对象,而在运行时执行new String()时,将常量池中的对象复制一份放到堆中,并且把堆中的这个对象的引用交给s持有。所以这条语句就创建了2String对象。


下面是一些String相关的常见问题:

String中的final用法和理解
final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;//
此句编译不通过

final StringBuffer a = new StringBuffer("111");
a.append("222");//
编译通过

可见,final只对引用的""(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。

String 常量池问题的几个例子

下面是几个常见例子的比较分析和理解:
[1]
String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + "true";
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true

分析:JVM对于字符串常量的"+"号连接,将程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true

[2]
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = false

分析:JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false

[3]
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = true

分析:和[3]中唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + bb"a" + "b"效果是一样的。故上面程序的结果为true

[4]
String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static String getBB() {
return "b";
}

分析:JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态连接并分配地址为b,故上面程序的结果为false

通过上面4个例子可以得出得知:
String  s  =  "a" + "b" + "c";  
就等价于String s = "abc";  

String  a  =  "a";  
String  b  =  "b";  
String  c  =  "c";  
String  s  =   a  +  b  +  c;  

这个就不一样了,最终结果等于:  
StringBuffer temp = new StringBuffer();  
temp.append(a).append(b).append(c);  
String s = temp.toString();

由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因分析,形如这样的代码:

public class Test {
public static void main(String args[]) {
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
}
}

每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。 如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBufferStringBulider对象来进行append操作。

String对象的intern方法理解和分析:

public class Test4 {
private static String a = "ab";
public static void main(String[] args){
String s1 = "a";
String s2 = "b";
String s = s1 + s2;
System.out.println(s == a);//false
System.out.println(s.intern() == a);//true 
}
}

这里用到Java里面是一个常量池的问题。对于s1+s2操作,其实是在堆里面重新创建了一个新的对象,s保存的是这个新对象在堆空间的的内容,所以sa的值是不相等的。而当调用s.intern()方法,却可以返回s常量池中的地址值,因为a的值存储在常量池中,故s.interna的值相等

分享到:
评论
1 楼 luliangy 2011-11-06  
讲的太深入啦,看不懂!

相关推荐

    第4节: 揭秘JVM字符串常量池和Java堆-01

    第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: 揭秘JVM字符串常量池和Java堆-01第4节: ...

    jvm如何处理长字符串

    jvm如何处理长字符串?java的classs文件中,constant_utf8_info的长度是u2,也就是说,一个字符串最长是65535个字节,但是,在本机做测试,超过这个长度的字符串也是允许的,原因是什么?

    JVM常量池教程吐血整理干货.md

    字符串常量池(全局常量池) 包装类型缓存池 JVM常量池 Jvm常量池分为: Class常量池(静态常量池) 运行时常量池 字符串常量池(全局常量池) 包装类型缓存池 Class常量池(静态常量池) 当Java源文件被编译后,就会生成...

    06-VIP-JVM调优实战及常量池详解(1)1

    1. 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建 2. JVM为了提高性能和减少内存开销,在实例化字符串

    字符数组的存储方式 字符串常量池.docx

    字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建做了一定的优化,在Permanent Generation中专门有一块区域用来存储字符串常量池(一组指针指向Heap中的...

    深入解析JVM之内存结构及字符串常量池(推荐)

    Java作为一种平台无关性的语言,其主要依靠于Java虚拟机——JVM,接下来通过本文给大家介绍JVM之内存结构及字符串常量池的相关知识,需要的朋友可以参考下

    Java中的字符串常量池详细介绍

    主要介绍了Java中的字符串常量池详细介绍,JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池,需要的朋友可以参考下

    图解JVM的内存结构及字符串常量池方法详解.docx

    JVM包含了非常多的知识,比较核心的有 内存结构 、 类加载 、 类文件结构 、 垃圾回收 、 执行 引擎 、 性能调优 、 监控 等等这些知识,但所有的功能都是围绕着 内存结构 展开的,因为我们编译后的...

    06-VIP-JVM调优实战及常量池详解(预习)1

    1. 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建 2. JVM为了提高性能和减少内存开销,在实例化字符串

    JVM教程吐血整理干货.md

    JVM JVM运行时内存分区 程序计数器 程序计数器的特点 Java虚拟机栈 栈帧 局部变量表 操作数栈 ...(永久代和元空间都是方法区的实现),字符串常量池也移动到了heap空间 jdk8之后的jvm内存分区 程

    java中常量以及常量池

    1、举例说明 变量 常量 字面...  静态常量池:*.class文件中的常量池,class文件中的常量池不仅仅包含字符串,数值字面量,还包含类、方法的信息,占用class文件绝大部分空间。  运行时常量池:是jvm虚拟机在完成类装

    深入Java虚拟机(原书第2版).pdf【附光盘内容】

     作者以易于理解的方式深入揭示了java虚拟机的内部工作原理,深入理解这些内容,将对读者更快速地编写更高效的程序大有裨益!  本书共分20章,第1-4章解释了java虚拟机的体系结构,包括java栈、堆、方法区、执行...

    深入理解JVM实战篇-String类1

    A2:当一个String实例调 用 intern() 方法时,Java查找常量池中是否有相同Unicode 的字符串常量,如果有,则返回其的引用,如果没有,则在

    AIC的Java课程1-6章

     [*]了解Java内存机制:栈、堆、常量池等,理解垃圾回收机制。 第3章 面向过程(数组和方法) 4课时  理解如何声明数组、构造数组、初始化数组以及使用数组中的各个元素。  清楚数组作为...

    String s = new String(” a “) 到底产生几个对象?

    老生常谈的一个梗,到2020了还在争论,你们一天天的,哎哎哎,我不是针对你一个,我是说...毕竟我和各位都是人才,java知识底蕴不能如此短浅,这题还没谢幕我们还能对面试官多哔哔几句:字符串常量池在不同版本的jvm中

    String_字符串.html

    String对象是不可变的。JVM对其做了一个优化,在内存中开辟了一段区域作为字符串常量池。通过"字面量"形式创建的字符串对象都会缓存并重用。

    JavaFocus::hammer: Java重点内容 博客文章 样例

    JavaFocus:hammer: Java学习重点 ...success和isSuccessequals 和 ==String为什么是不可变的字符串常量池为什么直接定义的字符串可以调用String对象的各种方法JDK6 和 JDK7 substring原理的改变字符串拼接的几种方式

    JAVA 范例大全 光盘 资源

    实例37 比较字符串 86 实例38 Java字符串与文件的互转 88 实例39 截取带汉字的字符串 91 实例40 替换字符串中的部分字符 92 实例41 Java字符串之密码加密 93 实例42 正则表达式验证字符串 95 第7章 Java常用类...

    java范例开发大全源代码

     实例83 字符串间的比较 112  实例84 字符集的解码方法 113  实例85 寻找指定字符第一次出现的位置 114  实例86 寻找指定字符最后出现的位置 115  实例87 我究竟有多长 116  实例88 替换指定的字符 ...

    java范例开发大全

    实例83 字符串间的比较 112 实例84 字符集的解码方法 113 实例85 寻找指定字符第一次出现的位置 114 实例86 寻找指定字符最后出现的位置 115 实例87 我究竟有多长 116 实例88 替换指定的字符 117 实例89 分割字符串 ...

Global site tag (gtag.js) - Google Analytics