由常量池 运行时常量池 String intern方法想到的(三)之String内存模型

在这篇博文中描述的,所有在运行时常量池中出现的字符串其实都是一个String对象。因为,java是一种强类型的语言,要求每一种变量都要有具体的数据类型。但是基本数据类型存放的不是对象(String不属于基本数据类型)。基本数据类型的常量在运行时常量池中存放的是字面值。貌似JVM会自动将boolean、byte、char、short自动转换成int型。(有待确认)。 那如何区分int long float double呢。整形和浮点型很容易区分,int和float只占一个slot,long和double要占两个slot。

声明

本文讨论的内容都是基于JDK1.6。

java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)

String的内存布局

  • s = “12”
public class Test {public static void main(String[] args) {String s = "12";}
}

上面的代码会发生什么? 
看下上面代码的字节码指令:

Compiled from "Test.java"
public class Test extends java.lang.ObjectSourceFile: "Test.java"minor version: 0major version: 50Constant pool:
const #1 = Method       #4.#13; //  java/lang/Object."<init>":()V
const #2 = String       #14;    //  12
const #3 = class        #15;    //  Test
const #4 = class        #16;    //  java/lang/Object
const #5 = Asciz        <init>;
const #6 = Asciz        ()V;
const #7 = Asciz        Code;
const #8 = Asciz        LineNumberTable;
const #9 = Asciz        main;
const #10 = Asciz       ([Ljava/lang/String;)V;
const #11 = Asciz       SourceFile;
const #12 = Asciz       Test.java;
const #13 = NameAndType #5:#6;//  "<init>":()V
const #14 = Asciz       12;
const #15 = Asciz       Test;
const #16 = Asciz       java/lang/Object;{
public Test();Code:Stack=1, Locals=1, Args_size=10:   aload_01:   invokespecial   #1; //Method java/lang/Object."<init>":()V4:   returnLineNumberTable: line 1: 0public static void main(java.lang.String[]);Code:Stack=1, Locals=2, Args_size=10:   ldc     #2; //String 122:   astore_13:   returnLineNumberTable: line 3: 0line 4: 3}

main方法的字节码指令只有2条

   0:   ldc     #2; //String 122:   astore_1

将运行时常量池中的常量”12”压入栈中,然后将这个栈中的”12”存入局部量量表的slot1中(注意,slot0中存放的是this)。 
当javac去编译Test.java时,发现了文本字符串”12”,会将这个”12”放入class文件的常量池中,当class文件被加载到JVM时,会将class文件中的常量池存放在运行时常量池中(这个时候应该是在运行时常量池中new出了一个String对象,如果只是存放字符串,在返回给s引用时,会出现类型不匹配的问题),然后在栈中开辟一个空间用来存放这个文本字符串在运行时常量池中的地址。其内存模型如下所示: 

  • s = new String(“12”)
public class Test {public static void main(String[] args) {String s = new String("12");}
}

其字节码指令如下:

public static void main(java.lang.String[]);Code:Stack=3, Locals=2, Args_size=10:   new     #2; //class java/lang/String3:   dup4:   ldc     #3; //String 126:   invokespecial   #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V9:   astore_110:  return

同样javac会将”12”放入class文件的常量池中,在类加载时存入运行时常量池。从字节码指令上来看,JVM会先在堆上new出一块内存,用来存放String对象,这个时候这个String对象中还没有进行init,也就没有内容,当调用init之后,通过astore_1将堆中的String对象的地址赋值给局部变量s。其内存模型如下所示: 

其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • s = “12” + “3”
public class Test {public static void main(String[] args) {String s = "12" + "3";}
}

其对应的java字节码指令如下:

Compiled from "Test.java"
public class Test extends java.lang.ObjectSourceFile: "Test.java"minor version: 0major version: 50Constant pool:
const #1 = Method       #4.#13; //  java/lang/Object."<init>":()V
const #2 = String       #14;    //  123
const #3 = class        #15;    //  Test
const #4 = class        #16;    //  java/lang/Object
const #5 = Asciz        <init>;
const #6 = Asciz        ()V;
const #7 = Asciz        Code;
const #8 = Asciz        LineNumberTable;
const #9 = Asciz        main;
const #10 = Asciz       ([Ljava/lang/String;)V;
const #11 = Asciz       SourceFile;
const #12 = Asciz       Test.java;
const #13 = NameAndType #5:#6;//  "<init>":()V
const #14 = Asciz       123;
const #15 = Asciz       Test;
const #16 = Asciz       java/lang/Object;{
public Test();Code:Stack=1, Locals=1, Args_size=10:   aload_01:   invokespecial   #1; //Method java/lang/Object."<init>":()V4:   returnLineNumberTable: line 1: 0public static void main(java.lang.String[]);Code:Stack=1, Locals=2, Args_size=10:   ldc     #2; //String 1232:   astore_13:   returnLineNumberTable: line 3: 0line 4: 3}

这个是不是很简单!java编译器在编译阶段就完成了优化,将文本字符串”12”和”3”,在编译时就拼接成了”123“存放在了class文件的常量池中。 
其内存模型如下所示: 

  • s = new String(“12”) + new String(“3”)

下面再看看上篇博文提出的问题是什么样的。

public class Test {public static void main(String[] args) {String s = new String("12") + new String("3");}
}

其java字节码如下所示:

public static void main(java.lang.String[]);Code:Stack=4, Locals=2, Args_size=10:   new     #2; //class java/lang/StringBuilder3:   dup4:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V7:   new     #4; //class java/lang/String10:  dup11:  ldc     #5; //String 1213:  invokespecial   #6; //Method java/lang/String."<init>":(Ljava/lang/String;)V16:  invokevirtual   #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;19:  new     #4; //class java/lang/String22:  dup23:  ldc     #8; //String 325:  invokespecial   #6; //Method java/lang/String."<init>":(Ljava/lang/String;)V28:  invokevirtual   #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;31:  invokevirtual   #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;34:  astore_135:  return

这句话产生的字节码明显多了。主要原因是,javac会对String的+操作符进行优化,使用StringBuilder的append方法实现。 
javac首先会将文本字符串放在class文件的常量池中,当类加载时存放在运行时常量池中。从字节码指令来看,首先在堆中new的是StringBuilder对象,然后new出了String对象,将”12“复制到String对象中,使用StringBuilder的append方法拼接,然后再new出一个String对象,将”3“复制到String对象中,使用append方法拼接,最后调用StringBuilder的toString(从StringBuilder#toString方法的源码可以看出,toString方法会new一个String对象)方法返回一个String引用。StringBuilder的toString方法的源码如下: 
 
其内存模型如下所示: 

其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • s = “12” + new String(“3”)
public class Test {public static void main(String[] args) {String s = "12" + new String("3");}
}

其对应的字节码指令如下:

public static void main(java.lang.String[]);Code:Stack=4, Locals=2, Args_size=10:   new     #2; //class java/lang/StringBuilder3:   dup4:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V7:   ldc     #4; //String 129:   invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;12:  new     #6; //class java/lang/String15:  dup16:  ldc     #7; //String 318:  invokespecial   #8; //Method java/lang/String."<init>":(Ljava/lang/String;)V21:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;24:  invokevirtual   #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;27:  astore_128:  return

这个就无法在编译器进行优化了。java编译器还是会将”12”和”3”放在class文件的常量池中,在类加载时放入运行时常量池中。在执行时会new一个StringBuilder对象,将”12”压入栈中,使用append方法进行连接,然后在堆上new一个String对象用来存放”3”,然后使用append方法进行连接,最后调用StringBuilder的toString(会new一个String对象)方法返回一个String的引用。 
其内存模型如下所示: 

其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • String t = “12”; String s = t + “3”;
public class Test {public static void main(String[] args) {String t = "12";String s = t + "3";}
}

其对应的字节码指令如下:

public static void main(java.lang.String[]);Code:Stack=2, Locals=3, Args_size=10:   ldc     #2; //String 122:   astore_13:   new     #3; //class java/lang/StringBuilder6:   dup7:   invokespecial   #4; //Method java/lang/StringBuilder."<init>":()V10:  aload_111:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;14:  ldc     #6; //String 316:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;19:  invokevirtual   #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;22:  astore_223:  return

虽然 t 是一个String,但是在编译器无法对其进行优化。从上面的字节码中看到这个的Locals=3,因为有两个局部变量 t 和 s。 
java编译器将”12”和”3”放入class文件常量池中,在运行时加载到运行时常量池中。其中”12”会存入到局部变量表的slot1(t 局部变量中),在堆中new一个StringBuilder对象,使用append方法连接”12”,然后调用append方法连接”3”,最后调用StringBuilder#toString返回。 
其内存模型如下所示: 

其中实线箭头表示引用(指针)指向,虚线箭头表示使用来源。

  • final String t = “12”; String s = t + “3”;
public class Test {public static void main(String[] args) {final String t = "12";String s = t + "3";}
}

其对应的字节码指令如下:

public static void main(java.lang.String[]);Code:Stack=1, Locals=3, Args_size=10:   ldc     #2; //String 1232:   astore_23:   return

由此可以看到java编译器进行了优化。

当是final型变量时不会当成变量操作,而是在编译器就进行了替换。注意其Locals=3。 
其内存模型如下所示: 

为此我专门做了一个小小地测试:

package start;public class Test {public static void main(String[] args) {String s1 = new String("StringTest");String s2 = new StringBuffer().append("wb").append("is").append("wb").toString();String s3 = "ihaveaDream";String s4 = new String("Monday") + "Friday";System.out.println( s1.intern() == s1);System.out.println( s2.intern() == s2);System.out.println( s3.intern() == s3);    System.out.println( s4.intern() == s4);   /*** 在jdk1.6中:常量池的内存是与堆内存物理隔离的,因为常量池的内存在永久代进行分配,永久代和Java堆的内存是物理隔离的。* 输出结果为:* false* false* true* false*         对于s1:new一个String,JVM会先去常量池中找“StringTest”,发现没有“StringTest”,这时候会在常量池中new一个“StringTest”,并且* 会在堆内存复制一个在数据上与“StringTest”相同的对象,并返回它的堆内存引用s1。所以s1.intern()时,会先去常量池中找在数据上与“StringTest”* 相同的对象,发现有,返回它,此时s1.intern() 指的就是常量池中的“StringTest”,而s1指的是堆内存中的“StringTest”,所以为false。*        对于s2:先new一个StringBuffer对象,然后进行append()操作。根据javap -verbose Test.class反编译,可以看出在最后一次append()* 操作后,此时常量池中有“wb”和“is”两个对象,最后进行一次toString()操作,会在堆内存中新建一个“wbiswb”对象,也就是s2的指向。所以s2.intern()时,* 发现常量池中并 没有“wbiswb”对象,只有“wb”和“is”两个对象,所以JVM会在常量池中new一个“wbiswb”对象,然后将s2.intern()指向它,也就是说s2.int* ern()指的是常量池中的“wbiswb”对象,s2指的是对内存中的“wbiswb”,明显不是同一个对象。*        对于s3:jvm先去常量池中找“ihaveaDream”对象,发现没有,所以在常量池中new一个“ihaveaDream”对象,然后s3指向它。s3.intern()时,* 先去常量池中找“ihaveaDream”对象,发现有,所以s3.intern()指的就是常量池中的“ihaveaDream”对象,此时 s3 == s3.intern();*        对于s4:根据Test类文件的反编译结果可以看出,此时jvm会在常量池中new“Monday”和“Friday”两个对象,然后中间一个加号,就相当于在堆内存新建一个* StringBuilder对象,StringBuilder对象根StringBuffer对象类似,最后会默认地调用StringBuilder的toString()方法在堆内存新建一个“MondayFriday”* 对象,然后s4指向它。s4.intern()时,jvm先去常量池中找“MondayFriday”对象,没找到,会在常量池中new一个“MondayFriday”对象,然后s4.intern()指向它。* 所以此时s4.intern()指向的是常量池中的“MondayFriday”对象,s4指的是堆内存中的“MondayFriday”对象。所以s4.intern() != s4;* * 在jdk1.7中:实现的常量池在java堆上分配内存* 输出结果为:* false* true* true* true*        *       对于s1:结合上面的分析JVM会在常量池和堆中分别new一个“StringTest”对象,然后s1指向堆中的对象,s1.intern()指向常量池中的对象, s1 != s1.intern();*         对于s2:结合上面的分析JVM会在常量池中创建“wb”和“is”两个对象,在堆中创建“wbiswb”对象,s2指向堆中的“wbiswb”对象,s2.intern()发现此时常量池中并没有* “wbiswb”对象(只有“wb”和“is”两个对象),然后根据jdk1.7会返回堆中的“wbiswb”对象给s2. s2 == s2.intern().*        对于s3:跟jdk1.6的一样。 s3 == s3.intern().*       对于s4:跟s2类似,s4和s4.intern()都是指向堆中的“MondayFriday”对象。 s4 == s4.intern().* * 总结:* 1.对于String S = new String(String s),结果是堆和常量池中都会有 s对象,所以无论是在jdk1.6还是jdk1.7中, S != S.intern().* 2.对于String S = "s",结果是S和S.intern()都指向常量池中的s对象。所以无论是在jdk1.6还是jdk1.7中, S == S.intern()。* 3.对于s.intern()而言:在jdk1.6中永远指向常量池中的对象,没有就创建,然后指向;在jdk1.7中,常量池中有就指向常量池中的对象,没有却是指向堆中的对象。*/}
}

JVM-由常量池 运行时常量池 String intern方法想到的(三)之String内存模型相关推荐

  1. JVM常量池最全详解-常量池/运行时常量池/字符串常量池/基本类型常量池,看这一篇就够了

    JVM常量池最全详解-常量池/运行时常量池/字符串常量池/基本类型常量池,看这一篇就够了! 常量池详解 1. 字面量和符号引用 1.1 字面量 1.2 符号引用 2. 常量池vs运行时常量池 3. 常 ...

  2. JVM详解之:运行时常量池

    文章目录 简介 class文件中的常量池 运行时常量池 静态常量详解 String常量 数字常量 符号引用详解 String Pool字符串常量池 总结 简介 JVM在运行的时候会对class文件进行 ...

  3. JVM - Class常量池 || 运行时常量池

    文章目录 Pre class常量池 字面量 符号引用 运行时常量池 Pre JVM - 深入剖析字符串常量池 JVM - 基本类型的包装类和对象池 class常量池 Class常量池我们可以理解为是C ...

  4. 02.字符串常量池 ? class常量池? 运行时常量池?

    java对象创建流程: 简介: 这几天在看Java虚拟机方面的知识时,看到了有几种不同常量池的说法,然后我就去CSDN.博客园等上找资料,里面说的内容真是百花齐放,各自争艳,因此,我好好整理了一下,将 ...

  5. Java中几种常量池(字符串常量池, Class常量池, 运行时常量池)的区别与联系

    简介: 这几天在看Java虚拟机方面的知识时,看到了有几种不同常量池的说法,然后我就去CSDN.博客园等上找资料,里面说的内容真是百花齐放,各自争艳,因此,我好好整理了一下,将我自认为对的理解写下来与 ...

  6. JVM 内存模型:运行时常量池

    1. 前言 最近研究Java基础知识.发现Java运行时常量池和String字符串有些一些细节的地方,值得我们注意的地方,最为一个Java开发人员对于这种java基本特性和JVM虚拟机的内存模型我们需 ...

  7. [JVM]了断局:常量池 VS 运行时常量池 VS 字符串常量池

    一.前言 最近在看JVM, 常量池, 运行时常量池,字符串常量池 这个看的有点懵. 整理一下. class常量池 是在编译的时候每个class都有的. 在编译阶段,存放的是常量的 符号引用 .    ...

  8. 详解JVM常量池、Class常量池、运行时常量池、字符串常量池(心血总结)

    写在前面:博主是一位普普通通的19届双非软工在读生,平时最大的爱好就是听听歌,逛逛B站.博主很喜欢的一句话花开堪折直须折,莫待无花空折枝:博主的理解是头一次为人,就应该做自己想做的事,做自己不后悔的事 ...

  9. jvm中方法区和常量池详解_Java常量池(静态常量池与运行时常量池)

    1.什么是常量 用final修饰的成员变量表示常量,值一旦给定就无法改变! final修饰的变量有三种:静态变量.实例变量和局部变量,分别表示三种类型的常量. Java中的常量池,实际上分为两种形态: ...

  10. python中常量池和堆_JVM详解之:运行时常量池

    简介 JVM在运行的时候会对class文件进行加载,链接和初始化的过程.class文件中定义的常量池在JVM加载之后会发生什么神奇的变化呢?快来看一看吧. class文件中的常量池 之前我们在讲cla ...

最新文章

  1. [剑指offer] 矩阵覆盖
  2. MV-LDL论文修改20211115(B-Y Rong)
  3. 使用randomaccessfile类将一个文本文件中的内容逆序输出_Java 中比较常用的知识点:I/O 总结...
  4. 假如啤酒只有七天生命
  5. xstatic图形连接linux,RHCE认证考试模拟题深入讲解:linux配置静态IP地址为:192.168.0.X!...
  6. MinGW GCC 7.1.0 2017年6月份出炉啦
  7. 二十、Oracle学习笔记:编写存储过程
  8. Dyn披露DDoS攻击细节:幕后是10万台的物联网设备僵尸网络
  9. 高德地图设置中国经纬度范围
  10. C++中vector初始化
  11. 新零售电商:订单管理系统设计
  12. JAVA查搜索文件内容
  13. AlphaGo已然独孤求败,通过人工智能解放鉴黄劳动力还会有多久?
  14. Java学习笔记——十大经典排序算法总结
  15. 2022暑期实习网易互娱游戏研发
  16. 转:将HTML5封装成android应用APK文件的几种方法
  17. STM32模拟USB多点触控屏
  18. 色阶的中间调调节原理之一
  19. system(“pause“);
  20. RNN、RNNCell

热门文章

  1. 计算机操作系统的主要功能
  2. [从 0 开始写一个操作系统] 三、Bootloader 的实现
  3. 【Linux系列文章】正则表达式与文本处理工具
  4. 禁止chrome更新呢
  5. Logstash:从grok到5.X版本的dissect
  6. 【搜索引擎】Google打不开问题解决
  7. 解决 python安装pip问题
  8. Comparator.comparing排序报空指针异常
  9. 怎么判断日出时间早晚_怎样知道当地每天几点日出?
  10. WEP 加密的工作原理和安全缺陷