经常在网上各大版块都能看到对于java字符串运行时内存分配的探讨,形如:String a = "123",String b = new String("123"),这两种形式的字符串是存放在什么地方的呢,其实这两种形式的字符串字面值"123"本身在运行时既不是存放在栈上,也不是存放在堆上,他们是存放在方法区中的某个常量区,并且对于相同的字符串字面值在内存中只保留一份。下面我们将以实例来分析。

1.==运算符作用在两个字符串引用比较的两个案例:

public class StringTest {
public static void main(String[] args) {
//part 1
String s1 = "i love china";
String s2 = "i love china";
System.out.println("result:" + s1 == s2);//程序运行结果为true
//part 2
String s3 = new String("i love china");
String s4 = new String("i love china");
System.out.println("result:" + s3 == s4);//程序运行结果为false
} }

我们知道java中==运算符比较的是变量的值,对于引用类型对应的变量的值存放的是引用对象的地址,在这里String是引用类型,这里面的四个变量的值存放的其实是指向字符串的地址。对于part2的执行结果是显然的,因为new操作符会使jvm在运行时在堆中创建新的对象,两个不同的对象的地址是不同的。但是由part1的执行结果,可以看出s1和s2是指向的同一个地址,那么由变量s1,s2指向的字符串是存放在什么地方的呢,jvm又是对字符串如何处理的呢。同样的对于变量s3,s4所指向的堆中的不同的字符串对象,他们会分别在自己的对象空间中保存一份"i love china"字符串吗,为了了解jvm是如何处理字符串,首先我们看java编译器生成的字节码指令。通过字节码指令我们来分析jvm将会执行哪些操作。

2.以下为程序生成的部分字节码信息。红色标注的是我们需要关注的部分。

Constant pool:
#1 = Class #2 // StringTest
#2 = Utf8 StringTest
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LStringTest;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = String #17 // i love china 字符串地址的引用
#17 = Utf8 i love china
#18 = Fieldref #19.#21 // java/lang/System.out:Ljava/io/PrintStream;
#19 = Class #20 // java/lang/System
#20 = Utf8 java/lang/System
#21 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Class #25 // java/lang/StringBuilder
#25 = Utf8 java/lang/StringBuilder
#26 = String #27 // result:
#27 = Utf8 result:

#28 = Methodref #24.#29 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#29 = NameAndType #5:#30 // "<init>":(Ljava/lang/String;)V
#30 = Utf8 (Ljava/lang/String;)V
#31 = Methodref #24.#32 // java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
#32 = NameAndType #33:#34 // append:(Z)Ljava/lang/StringBuilder;
#33 = Utf8 append
#34 = Utf8 (Z)Ljava/lang/StringBuilder;
#35 = Methodref #24.#36 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#36 = NameAndType #37:#38 // toString:()Ljava/lang/String;
#37 = Utf8 toString
#38 = Utf8 ()Ljava/lang/String;
#39 = Methodref #40.#42 // java/io/PrintStream.println:(Ljava/lang/String;)V
#40 = Class #41 // java/io/PrintStream
#41 = Utf8 java/io/PrintStream
#42 = NameAndType #43:#30 // println:(Ljava/lang/String;)V
#43 = Utf8 println
#44 = Class #45 // java/lang/String
#45 = Utf8 java/lang/String
#46 = Methodref #44.#29 // java/lang/String."<init>":(Ljava/lang/String;)V
#47 = Utf8 args
#48 = Utf8 [Ljava/lang/String;
#49 = Utf8 s1
#50 = Utf8 Ljava/lang/String;
#51 = Utf8 s2
#52 = Utf8 s3
#53 = Utf8 s4
#54 = Utf8 StackMapTable
#55 = Class #48 // "[Ljava/lang/String;"
#56 = Utf8 SourceFile
#57 = Utf8 StringTest.java

...........
//对应的方法的字节码指令,由jvm运行时解释执行。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=5, args_size=1
0: ldc #16 // String i love china,该指令是将常量池的#16处符号引用,在这里为字符串“ilove china”符号引用push到栈顶。该指令与底下的指令2对应于程序中的String s1 = "i love china"语句
2: astore_1 //将栈顶的对象引用赋值给局部变量1.
3: ldc #16 // String i love china,同0处的指令,指向的是同一个符号引用处的常量。该指令与底下的指令5对应于程序中的 String s2 = "i love china"语句。
5: astore_2 //将栈顶的对象引用赋值给局部变量2.
6: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #24 // class java/lang/StringBuilder
12: dup
13: ldc #26 // String result:
15: invokespecial #28 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
18: aload_1
19: aload_2
20: if_acmpne 27 //弹出栈顶两个对象引用进行比较其是否相等,不等,转到指令27处,执行,相等执行下一条指令
23: iconst_1
24: goto 28
27: iconst_0
28: invokevirtual #31 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
31: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #39 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: new #44 // class java/lang/String,创建一个对象,该对象位于常量池#44引用处,这里为String对象,并将对象引用push到栈顶。
40: dup //拷贝栈顶一份对象引用push到栈顶。
41: ldc #16 // String i love china,同0,3处指令。
43: invokespecial #46 // Method java/lang/String."<init>":(Ljava/lang/String;)V
46: astore_3
47: new #44 // class java/lang/String//创建一个对象,并将对象引用push到栈顶
50: dup
51: ldc #16 // String i love china, 将字符串的符号引用push到栈顶。
53: invokespecial #46 // Method java/lang/String."<init>":(Ljava/lang/String;)V,根据栈顶的对应的对象引用及字符串引用调用对象的init初始化方法,对字符串对象初始化
56: astore 4 //将栈顶对象引用赋值给变量4.
58: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
61: new #24 // class java/lang/StringBuilder
64: dup
65: ldc #26 // String result:
67: invokespecial #28 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
70: aload_3
71: aload 4
73: if_acmpne 80
76: iconst_1
77: goto 81
80: iconst_0
81: invokevirtual #31 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
84: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
87: invokevirtual #39 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
90: return
.........

LineNumberTable:
line 7: 0
line 8: 3
line 9: 6
line 11: 37
line 12: 47
line 13: 58
line 14: 90
LocalVariableTable:
Start Length Slot Name Signature
0 91 0 args [Ljava/lang/String;//局部变量0
3 88 1 s1 Ljava/lang/String;  //局部变量1
6 85 2 s2 Ljava/lang/String;//局部变量2
47 44 3 s3 Ljava/lang/String;//局部变量3
58 33 4 s4 Ljava/lang/String;//局部变量4


字节码中红色的部分是与我们讨论相关的。通过生成的字节码,我们可以对示例程序得出如下结论。

1).java编译器在将程序编译成字节码的过程中,对遇到的字符串常量"i love china"首先判断其是否在字节码常量池中存在,不存在创建一份,存在的话则不创建,也就是相等的字符串,只保留一份,通过符号引用可以找到它,这样使得程序中的字符串变量s1和s2都是指向常量池中的同一个字符串常量。在运行时jvm会将字节码常量池中的字符串常量存放在方法区中的通常称之为常量池的位置,并且字符串是以字符数组的形式通过索引来访问的。jvm在运行时将s1与s2指向的字符串相对引用地址指向字符串实际的内存地址。

2).对于String s3 = new String("i love china"),String s4 = new String("i love china"),由字节码可以看出其是调用了new指令,jvm会在运行时创建两个不同的对象,s3与s4指向的是不同的对象地址。所以s3==s4比较的结果为false。

其次,对于s3与s4对象的初始化,从字节码看出是调用对象的init方法并且传递的是常量池中”i love china”的引用,那么创建String对象及初始化究竟干了什么,我们可以查看通过查看String的源码及String对象生成的字节码,以便更好的了解对于new String("i love china")时,在对象内部是做了字符串的拷贝还是直接指向该字符串对应的常量池的地址的引用。

3.String对象的部分源码:

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; /** Cache the hash code for the string */
private int hash; // Default to 0 public String() {
this.value = new char[0];
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

  从源码中我们看到String类里有个实例变量 char value[],通过构造方法我们可知,对象在初始化时并没有做拷贝操作,只是将传递进来的字符串对象的地址引用赋给了实例变量value。由此我们可以初步的得出结论:即使使用new String("abc")创建了一个字符串对象时,在内存堆中为该对象分配了空间,但是在堆上并没有存储"abc"本身的任何信息,只是初始化了其内部的实例变量到"abc"字符串的引用。其实这样做也是为了节省内存的存储空间,以及提高程序的性能。

4.下面我们来看看String对象部分字节码信息:

  public java.lang.String();
descriptor: ()V
flags: 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: iconst_0
6: newarray char
8: putfield #2 // Field value:[C
11: return
LineNumberTable:
line 137: 0
line 138: 4
line 139: 11 public java.lang.String(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0 //将局部变量0push到栈顶,自身对象的引用。
1: invokespecial #1 // Method java/lang/Object."<init>":()V 弹出栈顶对象引用调用该对象的#1处的初始化方法。
4: aload_0 //将自身对象引用push到栈顶。
5: aload_1 //传递的字符串引用push到栈顶。
6: getfield #2 // Field value:[C // 弹出栈顶的字符串引用并将其赋值给#2处的实例变量,并将其存放到栈上。
9: putfield #2 // Field value:[C // 弹出栈顶的字符串引用及对象自身的引用并将字符串的引用赋值给本对象自身的实例变量。
12: aload_0
13: aload_1
14: getfield #3 // Field hash:I
17: putfield #3 // Field hash:I
20: return

从字节码的角度我们可以得出结论,new String("abc")在构造新对象时执行的是字符串引用的赋值,而不是字符串的拷贝。以上是从源码及字节码的角度来对字符串的内存分配进行的分析与总结。

java 字符串内存分配的分析与总结的更多相关文章

  1. JAVA虚拟机内存分配与回收机制

    Java虚拟机(Java Virtual Machine) 简称JVM Java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现.Java虚拟机有自己想象中的硬件,如处理器.堆栈.寄存器等 ...

  2. java继承内存分配

    java继承内存分配 继承的基本概念: * Java不支持多继承,也就是说子类至多只能有一个父类. * 子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法. * 子类中定义的成员 ...

  3. 图解Java继承内存分配

    图解Java继承内存分配   继承的基本概念: (1)Java不支持多继承,也就是说子类至多只能有一个父类. (2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法. (3)子 ...

  4. (转载)图解Java多态内存分配以及多态中成员方法的特点

    图解Java多态内存分配以及多态中成员方法的特点   图解Java多态内存分配以及多态中成员方法的特点   Person worker = new Worker(); 子类实例对象地址赋值给父类类型引 ...

  5. JAVA中内存分配的问题

    JAVA中内存分配的问题 1. 有这样一种说法,如今争锋于IT战场的两大势力,MS一族偏重于底层实现,Java一族偏重于系统架构.说法根据无从考证,但从两大势力各自的社区力量和图书市场已有佳作不难看出 ...

  6. java中内存分配策略及堆和栈的比较

    Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间 ...

  7. Java 对象内存分配与回收

    JVM内存区域模型: * 程序计数器,内存区域极小,是当前线程的字节码执行行号指示器: * 虚拟机栈.本地方法栈,即平时所说的“栈”,是虚拟机用来执行方法(包括Java.非Java方法)时,使用的临时 ...

  8. map的内存分配机制分析

    该程序演示了map在形成的时候对内存的操作和分配. 因为自己对平衡二叉树的创建细节理解不够,还不太明白程序所显示的日志.等我明白了,再来修改这个文档. /* 功能说明: map的内存分配机制分析. 代 ...

  9. list的内存分配机制分析

    该程序演示了list在内存分配时候的问题.里面的备注信息是我的想法. /* 功能说明: list的内存分配机制分析. 代码说明: list所管理的内存地址可以是不连续的.程序在不断的push_back ...

随机推荐

  1. C++ Primer 学习笔记_104_特殊工具与技术 --嵌套类

    特殊工具与技术 --嵌套类 能够在还有一个类内部(与后面所讲述的局部类不同,嵌套类是在类内部)定义一个类,这种类是嵌套类,也称为嵌套类型.嵌套类最经常使用于定义运行类. 嵌套类是独立的类,基本上与它们 ...

  2. Python 爬虫 —— BeautifulSoup

    from bs4 import BeautifulSoup % 首字母大写,显然这是一个类 1. BeautifulSoup 类 HTML 解析类(parser) r = requests.get(. ...

  3. [PHP7.0-PHP7.2]的新特性和新变更

    php7发布已经升级到7.2.里面发生了很多的变化.本文整理php7.0至php7.2的新特性和一些变化. 参考资料: http://php.net/manual/zh/migration70.new ...

  4. XAML的命名空间

    原文:XAML的命名空间 一个最简单的XAML例子   <Window x:Class="WpfApplication1.MainWindow" xmlns="ht ...

  5. pdf密码解除工具

    PDF Password Remover 3.0下载地址: 链接:https://pan.baidu.com/s/1hAmcGB-vMxz79IGGskdzHQ 提取码:q6y8

  6. centos7安装 lamp

    1.安装apache yum install httpd #根据提示,输入Y安装即可成功安装 systemctl start httpd.service #启动apache systemctl sto ...

  7. Java 访问修饰符详解

    访问修饰符定义了类.属性和方法的访问权限,Java 中包含四种,访问权限从小到大为 private.default.protected 和 public. public,公共修饰符,被其修饰的类.属性 ...

  8. C#判断系统是32位还是64位

    bool type; type = Environment.Is64BitOperatingSystem; Console.WriteLine(type);

  9. phpstudy+phpstorm+debug

    文:phpstudy+phpstorm+debug 一.配置前说明: 1.phpStudy集成了XDebug扩展,所以不用单独下载XDebug. 2.打开XDebug扩展:其它选项菜单 > PH ...

  10. DLL中类的显式链接(用虚函数进行显式链接)

    DLL的显式链接在某些时候比隐式链接具有更大的灵活性.比如,如果在运行时发现DLL无法找到,程序可以显示一个错误信息并能继续运行.当你想为你的程序提供插件服务时,显式链接也很有用处. 显式链接到全局C ...