澳门新葡萄京官网首页 22

澳门新葡萄京官网首页深入理解Java:String

一、引题

在java语言的所有数据类型中,String类型是比较特殊的一种类型,同时也是面试的时候经常被问到的一个知识点,本文结合java内存分配深度分析关于String的许多令人迷惑的问题。下面是本文将要涉及到的一些问题,如果读者对这些问题都了如指掌,则可忽略此文。

1、java内存具体指哪块内存?这块内存区域为什么要进行划分?是如何划分的?划分之后每块区域的作用是什么?如何设置各个区域的大小?

2、String类型在执行连接操作时,效率为什么会比StringBuffer或者StringBuilder低?StringBuffer和StringBuilder有什么联系和区别?

3、java中常量是指什么?String s = “s” 和 String s = new String(“s”)
有什么不一样?

本文经多方资料的收集整理和归纳,最终撰写成文,如果有错误之处,请多多指教!

转自:

在讲解String之前,我们先了解一下Java的内存结构。

二、java内存分配

摘要:
从整体上介绍java内存的概念、构成以及分配机制,在此基础上深度解析java中的String类型,从内存分配情况来解析String对象的特性。

一、Java内存模型

按照官方的说法:Java
虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。

    JVM主要管理两种类型内存:堆和非堆,堆内存(Heap Memory)是在 Java
虚拟机启动时创建,非堆内存(Non-heap Memory)是在JVM堆之外的内存。

简单来说,非堆包含方法区、JVM内部处理或优化所需的内存(如
JITCompiler,Just-in-time
Compiler,即时编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。

   
Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、
anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int,
short, long, byte, float, double, boolean, char)和对象句柄(引用)。

   
虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集合,包括直接常量(string,integer和
floating point常量)和对其他类型,字段和方法的符号引用。
  对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,
对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在Method
Area,而不是堆中。常量池中保存着很多String对象;
并且可以被共享使用,因此它提高了效率

具体关于JVM和内存等知识请参考:

JVM 基础知识

Java
内存模型及GC原理

1、JVM简介

Java虚拟机(Java Virtual Machine
简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java
最具吸引力的特性之一。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

一个运行时的Java虚拟机实例的天职是:负责运行一个java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。

如下图所示,JVM的体系结构包含几个主要的子系统和内存区:

垃圾回收器(Garbage
Collection):
负责回收堆内存(Heap)中没有被使用的对象,即这些对象已经没有被引用了。

类装载子系统(Classloader
Sub-System):
除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。

执行引擎(Execution
Engine):
负责执行那些包含在被装载类的方法中的指令。

运行时数据区(Java Memory Allocation
Area):
又叫虚拟机内存或者Java内存,虚拟机运行时需要从整个计算机内存划分一块内存区域存储许多东西。例如:字节码、从已装载的class文件中得到的其他信息、程序创建的对象、传递给方法的参数,返回值、局部变量等等。

澳门新葡萄京官网首页 1

一、引题

二、案例解析

  1 public static void main(String[] args) {
  2         /**
  3          * 情景一:字符串池
  4          * JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象;
  5          * 并且可以被共享使用,因此它提高了效率。
  6          * 由于String类是final的,它的值一经创建就不可改变。
  7          * 字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
  8          */
  9         String s1 = "abc";
 10         //↑ 在字符串池创建了一个对象  
 11         String s2 = "abc";
 12         //↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象  
 13         System.out.println("s1 == s2 : "+(s1==s2));
 14         //↑ true 指向同一个对象,  
 15         System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
 16         //↑ true  值相等  
 17         //↑------------------------------------------------------over  
 18         /**
 19          * 情景二:关于new String("")
 20          *
 21          */
 22         String s3 = new String("abc");
 23         //↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中;  
 24         //↑ 还有一个对象引用s3存放在栈中  
 25         String s4 = new String("abc");
 26         //↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象  
 27         System.out.println("s3 == s4 : "+(s3==s4));
 28         //↑false   s3和s4栈区的地址不同,指向堆区的不同地址;  
 29         System.out.println("s3.equals(s4) : "+(s3.equals(s4)));
 30         //↑true  s3和s4的值相同  
 31         System.out.println("s1 == s3 : "+(s1==s3));
 32         //↑false 存放的地区多不同,一个栈区,一个堆区  
 33         System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
 34         //↑true  值相同  
 35         //↑------------------------------------------------------over  
 36         /**
 37          * 情景三:
 38          * 由于常量的值在编译的时候就被确定(优化)了。
 39          * 在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。
 40          * 这行代码编译后的效果等同于: String str3 = "abcd";
 41          */
 42         String str1 = "ab" + "cd";  //1个对象  
 43         String str11 = "abcd";
 44         System.out.println("str1 = str11 : "+ (str1 == str11));
 45         //↑------------------------------------------------------over  
 46         /**
 47          * 情景四:
 48          * 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。
 49          *
 50          * 第三行代码原理(str2+str3):
 51          * 运行期JVM首先会在堆中创建一个StringBuilder类,
 52          * 同时用str2指向的拘留字符串对象完成初始化,
 53          * 然后调用append方法完成对str3所指向的拘留字符串的合并,
 54          * 接着调用StringBuilder的toString()方法在堆中创建一个String对象,
 55          * 最后将刚生成的String对象的堆地址存放在局部变量str3中。
 56          *
 57          * 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。
 58          * str4与str5地址当然不一样了。
 59          *
 60          * 内存中实际上有五个字符串对象:
 61          *       三个拘留字符串对象、一个String对象和一个StringBuilder对象。
 62          */
 63         String str2 = "ab";  //1个对象  
 64         String str3 = "cd";  //1个对象                                         
 65         String str4 = str2+str3;
 66         String str5 = "abcd";
 67         System.out.println("str4 = str5 : " + (str4==str5)); // false  
 68         //↑------------------------------------------------------over  
 69         /**
 70          * 情景五:
 71          *  JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。
 72          *  运行期的两个string相加,会产生新的对象的,存储在堆(heap)中
 73          */
 74         String str6 = "b";
 75         String str7 = "a" + str6;
 76         String str67 = "ab";
 77         System.out.println("str7 = str67 : "+ (str7 == str67));
 78         //↑str6为变量,在运行期才会被解析。  
 79         final String str8 = "b";
 80         String str9 = "a" + str8;
 81         String str89 = "ab";
 82         System.out.println("str9 = str89 : "+ (str9 == str89));
 83         //↑str8为常量变量,编译期会被优化  
 84         //↑------------------------------------------------------over  
 85     }

 

2、java内存分区

从上节知道,运行时数据区即是java内存,而且数据区要存储的东西比较多,如果不对这块内存区域进行划分管理,会显得比较杂乱无章。程序喜欢有规律的东西,最讨厌杂乱无章的东西。
根据存储数据的不同,java内存通常被划分为5个区域:程序计数器(Program
Count Register)、本地方法栈(Native Stack)、方法区(Methon
Area)、栈(Stack)、堆(Heap)。

程序计数器(Program Count
Register):
又叫程序寄存器。JVM支持多个线程同时运行,当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)。如果线程正在执行的是一个Java方法(非native),那么PC寄存器的值将总是指向下一条将被执行的指令,如果方法是
native的,程序计数器寄存器的值不会被定义。
JVM的程序计数器寄存器的宽度足够保证可以持有一个返回地址或者native的指针。

栈(Stack):又叫堆栈。JVM为每个新创建的线程都分配一个栈。也就是说,对于一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的
Java堆栈里新压入一个帧,这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其他数据。从Java的这种分配机制来看,堆栈又可以这样理解:栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。其相关设置参数:

  • -Xss –设置方法栈的最大值

本地方法栈(Native Stack):存储本地方方法的调用状态。

澳门新葡萄京官网首页 2

方法区(Method
Area):
当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息(包括类信息、常量、静态变量等)放到方法区中,该内存区域被所有线程共享,如下图所示。本地方法区存在一块特殊的内存区域,叫常量池(Constant
Pool),这块内存将与String类型的分析密切相关。

澳门新葡萄京官网首页 3

堆(Heap):Java堆(Java
Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在此区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存,但是这个对象的引用却是在栈(Stack)中分配。因此,执行String
s = new
String(“s”)时,需要从两个地方分配内存:在堆中为String对象分配内存,在栈中为引用(这个堆对象的内存地址,即指针)分配内存,如下图所示。

澳门新葡萄京官网首页 4

JAVA虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令,正如你无法用Java代码区明确释放一个对象一样。虚拟机自己负责决定如何以及何时释放不再被运行的程序引用的对象所占据的内存,通常,虚拟机把这个任务交给垃圾收集器(Garbage
Collection)。其相关设置参数:

  • -Xms — 设置堆内存初始大小
  • -Xmx — 设置堆内存最大值
  • -XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数
  • -XX:PretenureSizeThreshold —
    设置超过指定大小的大对象直接分配在旧生代中

Java堆是垃圾收集器管理的主要区域,因此又称为“GC 堆”(Garbage
Collectioned
Heap)。现在的垃圾收集器基本都是采用的分代收集算法,所以Java堆还可以细分为:新生代(Young
Generation)和老年代(Old
Generation),如下图所示。分代收集算法的思想:第一种说法,用较高的频率对年轻的对象(young
generation)进行扫描和回收,这种叫做minor collection,而对老对象(old
generation)的检查回收频率要低很多,称为major
collection。这样就不需要每次GC都将内存中所有对象都检查一遍,以便让出更多的系统资源供应用系统使用;另一种说法,在分配对象遇到内存不足时,先对新生代进行GC(Young
GC);当新生代GC之后仍无法满足内存空间分配需求时,
才会对整个堆空间以及方法区进行GC(Full GC)。

澳门新葡萄京官网首页 5

在这里可能会有读者表示疑问:记得还有一个什么永久代(Permanent
Generation)
的啊,难道它不属于Java堆?亲,你答对了!其实传说中的永久代就是上面所说的方法区,存放的都是jvm初始化时加载器加载的一些类型信息(包括类信息、常量、静态变量等),这些信息的生存周期比较长,GC不会在主程序运行期对PermGen
Space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen
Space错误。其相关设置参数:

  • -XX:PermSize –设置Perm区的初始大小
  • -XX:MaxPermSize –设置Perm区的最大值

新生代(Young
Generation)
又分为:Eden区和Survivor区,Survivor区有分为From Space和To
Space。Eden区是对象最初分配到的地方;默认情况下,From Space和To
Space的区域大小相等。JVM进行Minor
GC时,将Eden中还存活的对象拷贝到Survivor区中,还会将Survivor区中还存活的对象拷贝到Tenured区中。在这种GC模式下,JVM为了提升GC效率,
将Survivor区分为From Space和To
Space,这样就可以将对象回收和对象晋升分离开来。新生代的大小设置有2个相关参数:

  • -Xmn — 设置新生代内存大小。

  • -XX:SurvivorRatio — 设置Eden与Survivor空间的大小比例

老年代(Old
Generation)
: 当 OLD 区空间不够时, JVM 会在 OLD 区进行 major
collection;完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”Out
of memory错误”  。

 
 
在java语言的所有数据类型中,String类型是比较特殊的一种类型,同时也是面试的时候经常被问到的一个知识点,本文结合java内存分配深度分析关于String的许多令人迷惑的问题。下面是本文将要涉及到的一些问题,如果读者对这些问题都了如指掌,则可忽略此文。

总结:

三、String类型的深度解析

让我们从Java数据类型开始说起吧!Java数据类型通常(分类方法多种多样)从整体上可以分为两大类:基础类型和引用类型,基础类型的变量持有原始值,引用类型的变量通常表示的是对实际对象的引用,其值通常为对象的内存地址。对于基础类型和引用类型的细分,直接上图吧,大家看了一目了然。当然,下图也仅仅只是其中的一种分类方式。

 (原文图丢失)

针对上面的图,有3点需要说明:

  •    
    char类型可以单独出来形成一类,很多基本类型的分类为:数值类型、字符型(char)和bool型。
  •    
    returnAddress类型是一个Java虚拟机在内部使用的类型,被用来实现Java程序中的finally语句。
  •    
    String类型在上图的什么位置?yes,属于引用类型下面的类类型。下面开始对String类型的挖掘!

 
 
1、java内存具体指哪块内存?这块内存区域为什么要进行划分?是如何划分的?划分之后每块区域的作用是什么?如何设置各个区域的大小?

1.String类初始化后是不可变的(immutable)

这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:String
str=”kv”+”ill”+” “+”ans”;
就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和”
” 生成 “kvill “存在内存中,最后又和生成了”kvill
ans”;并把这个字符串的地址赋给了str,就是因为String的”不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原
因了,因为StringBuffer是可改变的。
  下面是一些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是不负责的。

1、String的本质

打开String的源码,类注释中有这么一段话“Strings are constant; their
values cannot be changed after they are created. String buffers support
mutable strings.Because String objects are immutable they can be
shared.”。这句话总结归纳了String的一个最重要的特点:String是值不可变(immutable)的常量,是线程安全的(can
be shared)。

接下来,String类使用了final修饰符,表明了String类的第二个特点:String类是不可继承的。

下面是String类的成员变量定义,从类的实现上阐明了String值是不可变的(immutable)。

private final char value[];
private final int count; 

因此,我们看String类的concat方法。实现该方法第一步要做的肯定是扩大成员变量value的容量,扩容的方法重新定义一个大容量的字符数组buf。第二步就是把原来value中的字符copy到buf中来,再把需要concat的字符串值也copy到buf中来,这样子,buf中就包含了concat之后的字符串值。下面就是问题的关键了,如果value不是final的,直接让value指向buf,然后返回this,则大功告成,没有必要返回一个新的String对象。但是。。。可惜。。。由于value是final型的,所以无法指向新定义的大容量数组buf,那怎么办呢?“return
new String(0, count + otherLen,
buf);”,这是String类concat实现方法的最后一条语句,重新new一个String对象返回。这下真相大白了吧!

总结:String实质是字符数组,两个特点:1、该类不可被继承;2、不可变性(immutable)。

澳门新葡萄京官网首页 6

 
 
2、String类型在执行连接操作时,效率为什么会比StringBuffer或者StringBuilder低?StringBuffer和StringBuilder有什么联系和区别?

2.代码中的字符串常量在编译的过程中收集并放在class文件的常量区中,如”123″、”123″+”456″等,含有变量的表达式不会收录,如”123″+a。

2、String的定义方法

在讨论String的定义方法之前,先了解一下常量池的概念,前面在介绍方法区的时候已经提到过了。下面稍微正式的给一个定义吧。

常量池(constant
pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。常量池还具备动态性,运行期间可以将新的常量放入池中,String类的intern()方法是这一特性的典型应用。不懂吗?后面会介绍intern方法的。虚拟机为每个被装载的类型维护一个常量池,池中为该类型所用常量的一个有序集合,包括直接常量(string、integer和float常量)和对其他类型、字段和方法的符号引用(与对象引用的区别?读者可以自己去了解)。

String的定义方法归纳起来总共为三种方式:

  • 使用关键字new,如:String s1 = new String(“myString”);
  • 直接定义,如:String s1 = “myString”;
  • 串联生成,如:String s1 = “my” +
    “String”;这种方式比较复杂,这里就不赘述了,请参见java–String常量池问题的几个例子。

第一种方式通过关键字new定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“myString”常量,节省内存空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“s1”,存放的值为堆中String实例的内存地址,这个过程就是将引用s1指向new出来的String实例。各位,最模糊的地方到了!堆中new出来的实例和常量池中的“myString”是什么关系呢?等我们分析完了第二种定义方式之后再回头分析这个问题。

第二种方式直接定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间。然后在栈中开辟一块空间,命名为“s1”,存放的值为常量池中“myString”的内存地址。常量池中的字符串常量与堆中的String对象有什么区别呢?为什么直接定义的字符串同样可以调用String对象的各种方法呢?

带着诸多疑问,我和大家一起探讨一下堆中String对象和常量池中String常量的关系,请大家记住,仅仅是探讨,因为本人对这块也比较模糊。

第一种猜想:因为直接定义的字符串也可以调用String对象的各种方法,那么可以认为其实在常量池中创建的也是一个String实例(对象)。String
s1 = new
String(“myString”);先在编译期的时候在常量池创建了一个String实例,然后clone了一个String实例存储在堆中,引用s1指向堆中的这个实例。此时,池中的实例没有被引用。当接着执行String
s1 =
“myString”;时,因为池中已经存在“myString”的实例对象,则s1直接指向池中的实例对象;否则,在池中先创建一个实例对象,s1再指向它。如下图所示:

这种猜想认为:常量池中的字符串常量实质上是一个String实例,与堆中的String实例是克隆关系。

第二种猜想也是目前网上阐述的最多的,但是思路都不清晰,有些问题解释不通。下面引用《JAVA
String对象和字符串常量的关系解析》一段内容。

在解析阶段,虚拟机发现字符串常量”myString”,它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对(
[myString], s1 )保存到内部字符串常量列表中。如下图所示:

澳门新葡萄京官网首页 7

如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。

例如,String s2 =
“myString”,运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。

这个猜想有一个比较明显的问题,红色字体标示的地方就是问题的所在。证明方式很简单,下面这段代码的执行结果,javaer都应该知道。

String s1 = new
String(“myString”);
String s2 = “myString”;

System.out.println(s1 == s2); 
//按照上面的推测逻辑,那么打印的结果为true;而实际上真实的结果是false,因为s1指向的是堆中String对象,而s2指向的是常量池中的String常量。

澳门新葡萄京官网首页 8

虽然这段内容不那么有说服力,但是文章提到了一个东西——字符串常量列表,它可能是解释这个问题的关键。

文中提到的三个问题,本文仅仅给出了猜想,请知道真正内幕的高手帮忙分析分析,谢谢!

  •  堆中new出来的实例和常量池中的“myString”是什么关系呢?
  • 常量池中的字符串常量与堆中的String对象有什么区别呢?
  • 为什么直接定义的字符串同样可以调用String对象的各种方法呢?

 
  3、java中常量是指什么?String s = “s” 和 String s = new String(“s”)
有什么不一样?

3.JVM在加载类的时候,根据常量区中的字符串生成常量池,每个字符序列如”123″会生成一个实例放在常量池里,这个实例是不在堆里的,也不会被GC,这个实例的value属性从源码的构造函数看应该是用new创建数组置入123的,所以按我的理解此时value存放的字符数组地址是在堆里,如果有误的话欢迎大家指正。

3、String、StringBuffer、StringBuilder的联系与区别

上面已经分析了String的本质了,下面简单说说StringBuffer和StringBuilder。

StringBuffer和StringBuilder都继承了抽象类AbstractStringBuilder,这个抽象类和String一样也定义了char[]
value和int
count,但是与String类不同的是,它们没有final修饰符。因此得出结论:String、StringBuffer和StringBuilder在本质上都是字符数组,不同的是,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以这就是为什么在进行大量字符串连接运算时,不推荐使用String,而推荐StringBuffer和StringBuilder。那么,哪种情况使用StringBuffe?哪种情况使用StringBuilder呢?

关于StringBuffer和StringBuilder的区别,翻开它们的源码,下面贴出append()方法的实现。

澳门新葡萄京官网首页 9

澳门新葡萄京官网首页 10

面第一张图是StringBuffer中append()方法的实现,第二张图为StringBuilder对append()的实现。区别应该一目了然,StringBuffer在方法前加了一个synchronized修饰,起到同步的作用,可以在多线程环境使用。为此付出的代价就是降低了执行效率。因此,如果在多线程环境可以使用StringBuffer进行字符串连接操作,单线程环境使用StringBuilder,它的效率更高。

    本文经多方资料的收集整理和归纳,最终撰写成文,如果有错误之处,请多多指教!澳门新葡萄京官网首页 11

4.使用String不一定创建对象

在执行到双引号包含字符串的语句时,如String a =
“123”,JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。如果是
String a = “123” + b
(假设b是”456″),前半部分”123″还是走常量池的路线,但是这个+操作符其实是转换成[SringBuffer].Appad()来实现的,所以最终a得到是一个新的实例引用,而且a的value存放的是一个新申请的字符数组内存空间的地址(存放着”123456″),而此时”123456″在常量池中是未必存在的。

要注意: 我们在使用诸如String str =
“abc”;的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象

二、java内存分配

5.使用new String,一定创建对象

在执行String a = new
String(“123”)的时候,首先走常量池的路线取到一个实例的引用,然后在堆上创建一个新的String实例,走以下构造函数给value属性赋值,然后把实例引用赋值给a:

  1 public String(String original) {
  2     int size = original.count;
  3     char[] originalValue = original.value;
  4     char[] v;
  5       if (originalValue.length > size) {
  6          // The array representing the String is bigger than the new
  7          // String itself.  Perhaps this constructor is being called
  8          // in order to trim the baggage, so make a copy of the array.
  9             int off = original.offset;
 10             v = Arrays.copyOfRange(originalValue, off, off+size);
 11      } else {
 12          // The array representing the String is the same
 13          // size as the String, so no point in making a copy.
 14         v = originalValue;
 15      }
 16     this.offset = 0;
 17     this.count = size;
 18     this.value = v;
 19     }

 

从中我们可以看到,虽然是新创建了一个String的实例,但是value是等于常量池中的实例的value,即是说没有new一个新的字符数组来存放”123″。

如果是String a = new
String(“123″+b)的情况,首先看回第4点,”123″+b得到一个实例后,再按上面的构造函数执行。

 
  1、JVM简介
      Java虚拟机(Java
Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java
最具吸引力的特性之一。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
            一个运行时的Java虚拟机实例的天职是:负责运行一个java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
        如下图所示,JVM的体系结构包含几个主要的子系统和内存区:
         
   垃圾回收器(Garbage
Collection):
负责回收堆内存(Heap)中没有被使用的对象,即这些对象已经没有被引用了。
         
   类装载子系统(Classloader Sub-System):除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。
         
   执行引擎(Execution Engine):负责执行那些包含在被装载类的方法中的指令。
         
   运行时数据区(Java Memory Allocation
Area):
又叫虚拟机内存或者Java内存,虚拟机运行时需要从整个计算机内存划分一块内存区域存储许多东西。例如:字节码、从已装载的class文件中得到的其他信息、程序创建的对象、传递给方法的参数,返回值、局部变量等等。

6.String.intern()

String对象的实例调用intern方法后,可以让JVM检查常量池,如果没有实例的value属性对应的字符串序列比如”123″(注意是检查字符串序列而不是检查实例本身),就将本实例放入常量池,如果有当前实例的value属性对应的字符串序列”123″在常量池中存在,则返回常量池中”123″对应的实例的引用而不是当前实例的引用,即使当前实例的value也是”123″。

  1 public native String intern();

存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的
intern()方法就是扩充常量池的
一个方法;当一个String实例str调用intern()方法时,Java 查找常量池中
是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常
量池中增加一个Unicode等于str的字符串并返回它的引用;看示例就清楚了

  1 public static void main(String[] args) {
  2         String s0 = "kvill";
  3         String s1 = new String("kvill");
  4         String s2 = new String("kvill");
  5         System.out.println( s0 == s1 ); //false
  6         System.out.println( "**********" );
  7         s1.intern(); //虽然执行了s1.intern(),但它的返回值没有赋给s1
  8         s2 = s2.intern(); //把常量池中"kvill"的引用赋给s2 
  9         System.out.println( s0 == s1); //flase
 10         System.out.println( s0 == s1.intern() ); //true//说明s1.intern()返回的是常量池中"kvill"的引用
 11         System.out.println( s0 == s2 ); //true
 12     }

最后我再破除一个错误的理解:有人说,“使用 String.intern()
方法则可以将一个 String 类的保存到一个全局 String 表中
,如果具有相同值的 Unicode
字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中”如果我把他说的这个全局的
String
表理解为常量池的话,他的最后一句话,”如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:

  1 public static void main(String[] args) {
  2         String s1 = new String("kvill");
  3         String s2 = s1.intern();
  4         System.out.println( s1 == s1.intern() ); //false
  5         System.out.println( s1 + "" + s2 ); //kvill kvill
  6         System.out.println( s2 == s1.intern() ); //true
  7     }

 

在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一
个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。
  s1==s1.intern()
为false说明原来的”kvill”仍然存在;s2现在为常量池中”kvill”的地址,所以有s2==s1.intern()为true。

 

澳门新葡萄京官网首页 12

StringBuffer与StringBuilder的区别,它们的应用场景是什么?

jdk的实现中StringBuffer与StringBuilder都继承自AbstractStringBuilder,对于多线程的安全与非安全看到StringBuffer中方法前面的一堆synchronized就大概了解了。

这里随便讲讲AbstractStringBuilder的实现原理:我们知道使用StringBuffer等无非就是为了提高java中字符串连接的效率,因为直接使用+进行字符串连接的话,jvm会创建多个String对象,因此造成一定的开销。AbstractStringBuilder中采用一个char数组来保存需要append的字符串,char数组有一个初始大小,当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,也即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置,因为重新分配内存并拷贝的开销比较大,所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式,这里是2倍

    StringBuffer 始于 JDK 1.0
    StringBuilder 始于 JDK 1.5
    从 JDK 1.5 开始,带有字符串变量的连接操作(+),JVM 内部采用的是
    StringBuilder 来实现的,而之前这个操作是采用 StringBuffer 实现的。

我们通过一个简单的程序来看其执行的流程:

  1 public class Buffer {
  2      public static void main(String[] args) {
  3             String s1 = "aaaaa";
  4             String s2 = "bbbbb";
  5             String r = null;
  6             int i = 3694;
  7             r = s1 + i + s2;
  8 
  9             for(int j=0;i<10;j++){
 10                 r+="23124";
 11             }
 12      }
 13 }

 

使用命令javap -c Buffer查看其字节码实现:

澳门新葡萄京官网首页 13

 

 

将清单1和清单2对应起来看,清单2的字节码中ldc指令即从常量池中加载“aaaaa”字符串到栈顶,istore_1将“aaaaa”存到变量1中,后面的一样,sipush是将一个短整型常量值(-32768~32767)推送至栈顶,这里是常量“3694”,更多的Java指令集请查看另一篇文章“Java指令集”。

让我们直接看到13,13~17是new了一个StringBuffer对象并调用其初始化方法,20~21则是先通过aload_1将变量1压到栈顶,前面说过变量1放的就是字符串常量“aaaaa”,接着通过指令invokevirtual调用StringBuffer的append方法将“aaaaa”拼接起来,后续的24~30同理。最后在33调用StringBuffer的toString函数获得String结果并通过astore存到变量3中。

看到这里可能有人会说,“既然JVM内部采用了StringBuffer来连接字符串了,那么我们自己就不用用StringBuffer,直接用”+“就行了吧!“。是么?当然不是了。俗话说”存在既有它的理由”,让我们继续看后面的循环对应的字节码。

37~42都是进入for循环前的一些准备工作,37,38是将j置为1。44这里通过if_icmpge将j与10进行比较,如果j大于10则直接跳转到73,也即return语句退出函数;否则进入循环,也即47~66的字节码。这里我们只需看47到51就知道为什么我们要在代码中自己使用StringBuffer来处理字符串的连接了,因为每次执行“+”操作时jvm都要new一个StringBuffer对象来处理字符串的连接,这在涉及很多的字符串连接操作时开销会很大。

参考:

java那点事——StringBuffer与StringBuilder原理与区别

 

 

 

内容来自:cnblogs:牛奶、不加糖

 
  2、java内存分区
     
从上节知道,运行时数据区即是java内存,而且数据区要存储的东西比较多,如果不对这块内存区域进行划分管理,会显得比较杂乱无章。程序喜欢有规律的东西,最讨厌杂乱无章的东西。
根据存储数据的不同,java内存通常被划分为5个区域:程序计数器(Program
Count Register)、本地方法栈(Native Stack)、方法区(Methon
Area)、栈(Stack)、堆(Heap)。
      程序计数器(Program Count
Register):
又叫程序寄存器。JVM支持多个线程同时运行,当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)。如果线程正在执行的是一个Java方法(非native),那么PC寄存器的值将总是指向下一条将被执行的指令,如果方法是
native的,程序计数器寄存器的值不会被定义。
JVM的程序计数器寄存器的宽度足够保证可以持有一个返回地址或者native的指针。
            栈(Stack):又叫堆栈。JVM为每个新创建的线程都分配一个栈。也就是说,对于一个Java程序来说,它的运行就是通过对栈的操作来完成的。栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的
Java堆栈里新压入一个帧,这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其他数据。从Java的这种分配机制来看,堆栈又可以这样理解:栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。其相关设置参数:

  • -Xss –设置方法栈的最大值

 
        本地方法栈(Native
Stack):
存储本地方方法的调用状态。

澳门新葡萄京官网首页 14

 
        方法区(Method Area):当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息(包括类信息、常量、静态变量等)放到方法区中,该内存区域被所有线程共享,如下图所示。本地方法区存在一块特殊的内存区域,叫常量池(Constant
Pool),这块内存将与String类型的分析密切相关。

澳门新葡萄京官网首页 15
          堆(Heap):Java堆(Java
Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在此区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存,但是这个对象的引用却是在栈(Stack)中分配。因此,执行String
s = new
String(“s”)时,需要从两个地方分配内存:在堆中为String对象分配内存,在栈中为引用(这个堆对象的内存地址,即指针)分配内存,如下图所示。

澳门新葡萄京官网首页 16

            JAVA虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令,正如你无法用Java代码区明确释放一个对象一样。虚拟机自己负责决定如何以及何时释放不再被运行的程序引用的对象所占据的内存,通常,虚拟机把这个任务交给垃圾收集器(Garbage
Collection)。其相关设置参数:

  • -Xms — 设置堆内存初始大小

  • -Xmx — 设置堆内存最大值

  • -XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数

  • -XX:PretenureSizeThreshold —
    设置超过指定大小的大对象直接分配在旧生代中

     
  Java堆是垃圾收集器管理的主要区域,因此又称为“GC
堆”(Garbage Collectioned
Heap)。现在的垃圾收集器基本都是采用的分代收集算法,所以Java堆还可以细分为:新生代(Young
Generation)和老年代(Old
Generation),如下图所示。分代收集算法的思想:第一种说法,用较高的频率对年轻的对象(young
generation)进行扫描和回收,这种叫做minor
collection,而对老对象(old
generation)的检查回收频率要低很多,称为major
collection。这样就不需要每次GC都将内存中所有对象都检查一遍,以便让出更多的系统资源供应用系统使用;另一种说法,在分配对象遇到内存不足时,先对新生代进行GC(Young
GC);当新生代GC之后仍无法满足内存空间分配需求时,
才会对整个堆空间以及方法区进行GC(Full GC)。

澳门新葡萄京官网首页 17
         

        在这里可能会有读者表示疑问:记得还有一个什么永久代(Permanent
Generation)
的啊,难道它不属于Java堆?亲,你答对了!其实传说中的永久代就是上面所说的方法区,存放的都是jvm初始化时加载器加载的一些类型信息(包括类信息、常量、静态变量等),这些信息的生存周期比较长,GC不会在主程序运行期对PermGen
Space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen
Space错误。其相关设置参数:

  • -XX:PermSize –设置Perm区的初始大小

  • -XX:MaxPermSize –设置Perm区的最大值

     
   新生代(Young Generation**)**又分为:Eden区和Survivor区,Survivor区有分为From
Space和To Space。Eden区是对象最初分配到的地方;默认情况下,From
Space和To Space的区域大小相等。JVM进行Minor
GC时,将Eden中还存活的对象拷贝到Survivor区中,还会将Survivor区中还存活的对象拷贝到Tenured区中。在这种GC模式下,JVM为了提升GC效率,
将Survivor区分为From Space和To
Space,这样就可以将对象回收和对象晋升分离开来。新生代的大小设置有2个相关参数:

  • -Xmn — 设置新生代内存大小。

  • -XX:SurvivorRatio — 设置Eden与Survivor空间的大小比例

            老年代(Old
Generation)
: 当 OLD 区空间不够时, JVM 会在 OLD 区进行 major
collection ;完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”Out
of memory错误”  。

三、String类型的深度解析

    让我们从Java数据类型开始说起吧!Java数据类型通常(分类方法多种多样)从整体上可以分为两大类:基础类型和引用类型,基础类型的变量持有原始值,引用类型的变量通常表示的是对实际对象的引用,其值通常为对象的内存地址。对于基础类型和引用类型的细分,直接上图吧,大家看了一目了然。当然,下图也仅仅只是其中的一种分类方式。
 

 
  针对上面的图,有3点需要说明:

  •  
     
    char类型可以单独出来形成一类,很多基本类型的分类为:数值类型、字符型(char)和bool型。

  •  
     
    returnAddress类型是一个Java虚拟机在内部使用的类型,被用来实现Java程序中的finally语句。

  •  
     
    String类型在上图的什么位置?yes,属于引用类型下面的类类型。下面开始对String类型的挖掘!

 

    1、String的本质
           打开String的源码,类注释中有这么一段话“Strings
are constant; their values cannot be changed after they are created.
String buffers support mutable strings.Because String objects are
immutable they
can be shared.”。这句话总结归纳了String的一个最重要的特点:String是值不可变(immutable)的常量,是线程安全的(can
be shared)。
     
 接下来,String类使用了final修饰符,表明了String类的第二个特点:String类是不可继承的。
     
 下面是String类的成员变量定义,从类的实现上阐明了String值是不可变的(immutable)。
            private
final char value[];
            private
final int count; 
     
 因此,我们看String类的concat方法。实现该方法第一步要做的肯定是扩大成员变量value的容量,扩容的方法重新定义一个大容量的字符数组buf。第二步就是把原来value中的字符copy到buf中来,再把需要concat的字符串值也copy到buf中来,这样子,buf中就包含了concat之后的字符串值。下面就是问题的关键了,如果value不是final的,直接让value指向buf,然后返回this,则大功告成,没有必要返回一个新的String对象。但是。。。可惜。。。由于value是final型的,所以无法指向新定义的大容量数组buf,那怎么办呢?“return
new String(0, count + otherLen, buf);”,这是String类concat实现方法的最后一条语句,重新new一个String对象返回。这下真相大白了吧!

     
 总结:**String实质是字符数组,两个特点:1、该类不可被继承;2、不可变性(immutable)**。

 

    2、String的定义方法
       在讨论String的定义方法之前,先了解一下常量池的概念,前面在介绍方法区的时候已经提到过了。下面稍微正式的给一个定义吧。
       常量池(constant
pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。常量池还具备动态性,运行期间可以将新的常量放入池中,String类的intern()方法是这一特性的典型应用。不懂吗?后面会介绍intern方法的。虚拟机为每个被装载的类型维护一个常量池,池中为该类型所用常量的一个有序集合,包括直接常量(string、integer和float常量)和对其他类型、字段和方法的符号引用(与对象引用的区别?读者可以自己去了解)。

     
 String的定义方法归纳起来总共为三种方式:

  •  
         使用关键字new,如:String s1 = new String(“myString”);

  •  
         直接定义,如:String s1 = “myString”;

  •  
         串联生成,如:String s1 = “my” +
    “String”;这种方式比较复杂,这里就不赘述了,请参见java–String常量池问题的几个例子。 

 
     第一种方式通过关键字new定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“myString”常量,节省内存空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“s1”,存放的值为堆中String实例的内存地址,这个过程就是将引用s1指向new出来的String实例。各位,最模糊的地方到了!堆中new出来的实例和常量池中的“myString”是什么关系呢?等我们分析完了第二种定义方式之后再回头分析这个问题。

     
 第二种方式直接定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间。然后在栈中开辟一块空间,命名为“s1”,存放的值为常量池中“myString”的内存地址。常量池中的字符串常量与堆中的String对象有什么区别呢?为什么直接定义的字符串同样可以调用String对象的各种方法呢?

     
 带着诸多疑问,我和大家一起探讨一下堆中String对象和常量池中String常量的关系,请大家记住,仅仅是探讨,因为本人对这块也比较模糊。
     
 第一种猜想:因为直接定义的字符串也可以调用String对象的各种方法,那么可以认为其实在常量池中创建的也是一个**String**实例(对象)。String
s1 = new String(“myString”);先在编译期的时候在常量池创建了一个String实例,然后clone了一个String实例存储在堆中,引用s1指向堆中的这个实例。此时,池中的实例没有被引用。当接着执行String
s1 = “myString”;时,因为池中已经存在“myString”的实例对象,则s1直接指向池中的实例对象;否则,在池中先创建一个实例对象,s1再指向它。如下图所示:
澳门新葡萄京官网首页 18
       这种猜想认为:常量池中的字符串常量实质上是一个String实例,与堆中的String实例是克隆关系。

     
 第二种猜想也是目前网上阐述的最多的,但是思路都不清晰,有些问题解释不通。下面引用《JAVA
String对象和字符串常量的关系解析》一段内容。
       在解析阶段,虚拟机发现字符串常量”myString”,它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对(
[myString], s1
)保存到内部字符串常量列表中。如下图所示:

澳门新葡萄京官网首页 19

            如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。**
           例如,String
s2 = “myString”,运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。
         
 
这个猜想有一个比较明显的问题,红色字体标示的地方就是问题的所在。证明方式很简单,下面这段代码的执行结果,javaer都应该知道。
               String s1 = new String(“myString”);
               String s2 = “myString”;
               System.out.println(s1 == s2); 
//按照上面的推测逻辑,那么打印的结果为true;而实际上真实的结果是false,因为s1指向的是堆中String对象,而s2指向的是常量池中的String常量。
澳门新葡萄京官网首页 20

         
 虽然这段内容不那么有说服力,但是文章提到了一个东西——字符串常量列表,它可能是解释这个问题的关键。

         
 文中提到的三个问题,本文仅仅给出了猜想,请知道真正内幕的高手帮忙分析分析,谢谢!

  •  
             堆中new出来的实例和常量池中的“myString”是什么关系呢?

  •  
             常量池中的字符串常量与堆中的String对象有什么区别呢?

  •  
             为什么直接定义的字符串同样可以调用String对象的各种方法呢?      

    3、String、StringBuffer、StringBuilder的联系与区别 
        上面已经分析了String的本质了,下面简单说说StringBuffer和StringBuilder。

     StringBuffer和StringBuilder都继承了抽象类AbstractStringBuilder,这个抽象类和String一样也定义了char[]
value和int
count,但是与String类不同的是,它们没有final修饰符。因此得出结论:String、StringBuffer和StringBuilder在本质上都是字符数组,不同的是,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以这就是为什么在进行大量字符串连接运算时,不推荐使用String,而推荐StringBuffer和StringBuilder。那么,哪种情况使用StringBuffe?哪种情况使用StringBuilder呢?         

     关于StringBuffer和StringBuilder的区别,翻开它们的源码,下面贴出append()方法的实现。 澳门新葡萄京官网首页 21  澳门新葡萄京官网首页 22 
     
     

        上面第一张图是StringBuffer中append()方法的实现,第二张图为StringBuilder对append()的实现。区别应该一目了然,StringBuffer在方法前加了一个synchronized修饰,起到同步的作用,可以在多线程环境使用。为此付出的代价就是降低了执行效率。因此,如果在多线程环境可以使用StringBuffer进行字符串连接操作,单线程环境使用StringBuilder,它的效率更高。

四、参考文献

Java虚拟机体系结构 
Java内存管理基础篇-Java内存分配 
Java堆内存设置优化 
Java内存管理和垃圾回收 
Java堆内存的转换和回收 
Java虚拟机的JVM垃圾回收机制 

浅谈设置JVM内存分配的几个妙招
深入Java字符串
Java性能优化之String篇
java字符串常量池知识
Java内存分配及String类型详解
Java String的内存机制
Java之内存分析和String对象

String类学习总结

发表评论

电子邮件地址不会被公开。 必填项已用*标注