java 字符串内存分配的分析与总结(推荐)

经常在网上各大版块都能看到对于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
91 0 args [Ljava/lang/String;//局部变量0
88 1 s1 Ljava/lang/String; //局部变量1
85 2 s2 Ljava/lang/String;//局部变量2
44 3 s3 Ljava/lang/String;//局部变量3
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对象的部分源码:

<SPAN style="FONT-SIZE: 14pt">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];
  }</SPAN>
  <SPAN style="BACKGROUND-COLOR: #ffffff; FONT-SIZE: 18pt"> public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
  }
</SPAN>

从源码中我们看到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 字符串内存分配的分析与总结(推荐)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • JAVA垃圾收集器与内存分配策略详解

    引言 垃圾收集技术并不是Java语言首创的,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.垃圾收集技术需要考虑的三个问题是: 1.哪些内存需要回收 2.什么时候回收 3.如何回收 java内存运行时区域的分布,其中程序计数器,虚拟机栈,本地方法区都是随着线程而生,随线程而灭,所以这几个区域就不需要过多考虑回收问题.但是堆和方法区就不一样了,只有在程序运行期间我们才知道会创建哪些对象,这部分内存的分配和回收都是动态的.垃圾收集器所关注的就是这部分内存. 一 对象

  • Java中内存分配的几种方法

    一.数组分配的上限 Java里数组的大小是受限制的,因为它使用的是int类型作为数组下标.这意味着你无法申请超过Integer.MAX_VALUE(2^31-1)大小的数组.这并不是说你申请内存的上限就是2G.你可以申请一个大一点的类型的数组.比如: 复制代码 代码如下: final long[] ar = new long[ Integer.MAX_VALUE ]; 这个会分配16G -8字节,如果你设置的-Xmx参数足够大的话(通常你的堆至少得保留50%以上的空间,也就是说分配16G的内存,

  • Java GC 机制与内存分配策略详解

    Java GC 机制与内存分配策略详解 收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现 自动内存管理解决的是:给对象分配内存 以及 回收分配给对象的内存 为什么我们要了解学习 GC 与内存分配呢? 在 JVM 自动内存管理机制的帮助下,不再需要为每一个new操作写配对的delete/free代码.但出现内存泄漏和溢出的问题时,如果不了解虚拟机是怎样使用内存的,那么排查错误将是一项非常艰难的工作. GC(垃圾收集器)在对堆进行回收前,会先确定哪些对象"存活",哪些已经&quo

  • 浅谈java+内存分配及变量存储位置的区别

    Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识.一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中(new 出来的对象) ◆堆:存放用new产生的数据 ◆静态域:存放在对象中用static定义的静态成员 ◆常量池:存放常量 ◆非RAM存储:硬盘等永久

  • 关于Java变量的声明、内存分配及初始化详解

    实例如下: class Person { String name; int age; void talk() { System.out.println("我是: "+name+", 今年: "+age+"岁"); } } public class TestJava2_1 { public static void main(String args[]) { Person p; if (p == null) { p = new Person(); }

  • 基于Java 数组内存分配的相关问题

    可能Java 数组大家都很熟悉,最近我遇到了一个关于Java 数组内存分配的问题.呵呵.突然就发现许多书上"基本数据类型存储在栈内存当中,对象则保存在堆内存"这句话完全是错误的.下面是个简单的例子代码: 复制代码 代码如下: public class Test {    public static void main(String[] argv) {// 静态初始化数组String[] names = { "Michael", "Orson",

  • java程序运行时内存分配详解

    一. 基本概念 每运行一个java程序会产生一个java进程,每个java进程可能包含一个或者多个线程,每一个Java进程对应唯一一个JVM实例,每一个JVM实例唯一对应一个堆,每一个线程有一个自己私有的栈.进程所创建的所有类的实例(也就是对象)或数组(指的是数组的本身,不是引用)都放在堆中,并由该进程所有的线程共享.Java中分配堆内存是自动初始化的,即为一个对象分配内存的时候,会初始化这个对象中变量.虽然Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在栈中分配,也就是说

  • Java 内存分配深入理解

    Java 内存分配深入理解 本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java.这类文章网上有很多,但大多比较零碎.本文从认知过程角度出发,将带给读者一个系统的介绍. 进入正题前首先要知道的是Java程序运行在JVM(Java  Virtual Machine,Java虚拟机)上,可以把JVM理解成Java程序和操作系统之间的桥梁,JVM实现了Java的平台无关性,由此可见JVM的重要性.所以在学习Java内存分配原理的时候一定要牢记这一切都是在JVM中进行的,JVM是

  • java 字符串内存分配的分析与总结(推荐)

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

  • Java虚拟机内存分配与回收策略问题精细解读

    本文参考于<深入理解Java虚拟机> 内存分配与回收策略 Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存. 1. 综述 对象的内存分配,从概念上讲,应该都是在堆上分配(而实际上也有可能经过即时编译后被拆散为标量类型并间接地在栈上分配).在经典分代的设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代.对象分配的规则并不是固定的,<Java虚拟机规范>并未规定新对象的创

  • C#字符串内存驻留机制分析

    在这之前我写过一些文章来介绍关于字符串内存分配和驻留的文章,涉及到的观点主要有:字符串的驻留机制避免了对具有相同字符序列的字符串对象的重复创建:被驻留的字符串是不受GC管辖的,即被驻留的字符串对象不能被GC回收:被驻留的字符串是被同一进程中所有应用程序域共享的.至于具体的原因,相信在<关于CLR内存管理一些深层次的讨论>中,你可以找到答案.由于这些天来在做一些关于内存泄露审查的工作,所以想通过具体的Memory Profiling工具来为你证实上面的结论.我采用的Memory Profilin

  • C#字符串内存分配与驻留池学习分享

    刚开始学习C#的时候,就听说CLR对于String类有一种特别的内存管理机制:有时候,明明声明了两个String类的对象,但是他们偏偏却指向同一个实例.如下: 复制代码 代码如下: String s1 ="Hello";String s2 ="Hello";                       //s2和s1的实际值都是Hellobool same = (object) s1 == (object) s2;//这里比较s1.s2是否引用了同一个对象实例//所

  • Java常见内存溢出异常分析与解决

    Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等,而Hotspot jvm的实现中,将堆内存分为了三部分,新生代,老年代,持久带,其中持久带实现了规范中规定的方法区,而内存模型中不同的部分都会出现相应的OutOfMemoryError错误,接下来我们就分开来讨论一下.java.lang.OutOfMemoryError这个错误我相信大部分开发人员都有遇到过,产生该错误的原因大都出于以下原因: JVM内存过小.程序不严密,产生了过多的垃圾. 导致OutOfMemor

  • Java 堆内存溢出原因分析

    前言 任何使用过基于 Java 的企业级后端应用的软件开发者都会遇到过这种低劣.奇怪的报错,这些报错来自于用户或是测试工程师: java.lang.OutOfMemoryError:Java heap space. 为了弄清楚问题,我们必须返回到算法复杂性的计算机科学基础,尤其是"空间"复杂性.如果我们回忆,每一个应用都有一个最坏情况特征.具体来说,在存储维度方面,超过推荐的存储将会被分配到应用程序上,这是不可预测但尖锐的问题.这导致了堆内存的过度使用,因此出现了"内存不够&

  • java字符串拼接与性能分析详解

    假设有一个字符串,我们将对这个字符串做大量循环拼接操作,使用"+"的话将得到最低的性能.但是究竟这个性能有多差?如果我们同时也把StringBuffer,StringBuilder或String.concat()放入性能测试中,结果又会如何呢?本文将会就这些问题给出一个答案! 我们将使用Per4j来计算性能,因为这个工具可以给我们一个完整的性能指标集合,比如最小,最大耗时,统计时间段的标准偏差等.在测试代码中,为了得到一个准确的标准偏差值,我们将执行20个拼接"*"

  • 理解Javascript_01_理解内存分配原理分析

    原始值和引用值 在ECMAScript中,变量可以存放两种类型的值,即原始值和引用值. 原始值指的就是代表原始数据类型(基本数据类型)的值,即Undefined,Null,Number,String,Boolean类型所表示的值. 引用值指的就是复合数据类型的值,即Object,Function,Array,以及自定义对象,等等 栈和堆 与原始值与引用值对应存在两种结构的内存即栈和堆 栈是一种后进先出的数据结构,在javascript中可以通过Array来模拟栈的行为 复制代码 代码如下: va

随机推荐