String深度解析

文章出处:http://my.oschina.net/xiaohui249/blog/170013

一、引题

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

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

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

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

二、java内存分配

  1、JVM简介

  Java虚拟机(Java Virtual Machine 简称JVM)是运行所有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文件中得到的其他信息、程序创建的对象、传递给方法的参数,返回值、局部变量等等。


  2、java内存分区
  从上节知道,运行时数据区即是java内存,而且数据区要存储的东西比较多,如果不对这块内存区域进行划分管理,会显得比较杂乱无章。根据存储数据的不同,java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。
  程序计数器(Program Count Register):
它是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。每一条JVM线程都有自己的PC寄存器,各条线程之间互不影响,独立存储,这类内存区域被称为“线程私有”内存。在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址,如果该方法是native,那PC寄存器的值是undefined。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

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

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

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

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


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


  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)。

  在这里可能会有读者表示疑问:记得还有一个什么永久代(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 完全垃圾收集后,若SurvivorOLD区仍然无法存放从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)

    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()方法的实现。                

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

原文地址:https://www.cnblogs.com/wxgblogs/p/5510288.html