Java中String揭秘

2019-12-10  本文已影响0人  简书_大叔

String对象是我们日常使用的对象类型,字符串对象或者其等价对象(如char数组),在内存中总是占据了最大的空间块,因此如何高效地处理字符串,是提高系统整体性能的关键。
在此之前,String作为一个对象类型,我们必须清楚Java对象的创建以为对象的内存结构。
\color{blue}{1.对象的创建以及内存结构 }
创建一个对象通常需要使用new关键字,当虚拟机遇到一条new指令的时候,首先会检查这个指令的参数是在常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果是则执行相应的类加载过程。

类加载检查结束之后,虚拟机将为新生对象分配内存,java中为对象分配内存有两种方式,一种是\color{red}{指针碰撞},该方法适用于内存规整的情况,在中间放一个指针作为分界点的指示器,使用过的内存和空闲的内存各放在一边,当需要分配内存的时候只需要将指针移动即可。另一种是\color{red}{空闲列表},如果java堆中的内存不是规整的,虚拟机会维护一张列表,记录哪块内存可用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。采用哪种分配方式是根据java堆是否规整决定的。而java堆是否规整由JVM是否使用带有压缩整理功能的垃圾收集器决定。

另外需要考虑的是内存分配过程中线程安全的情况。有如下两种解决方案;

内存分配完毕之后想,虚拟机需要分配到的内存空间初始化为零值。这一步操作保证了对象的实例字段在java代码中可以不赋初始值就可以使用,接下来虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息等,这些信息存放在对象的对象头中。这些工作完成之后,从JVM的角度来看一个对象已经创建成功了,从java的角度来看还需要执行init方法,将对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

在HotSpot虚拟机中,对象在内存中存储的布局可分为三个部分,即对象头,实例数据和对齐填充。
对象头包括两个部分,第一部分用来存储对象自身运行时的数据,如哈希码,GC分代年龄、线程所持有的锁等,官方称为“Mark Word”。第二个部分为类型 指针,即对象指向它的类元数据的指针,虚拟机通过这个 指针来确定这个对象属于哪个类的实例。

实例数据是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。

对齐填充并不是必须的,仅仅起到占位符的作用,HotSpot虚拟机需要对象起始地址必须是8字节的整数倍,对象部分正好是8字节的整数倍,所以当实例数据部分没有对齐时,需要通过对齐填充来对齐。
\color{blue}{2.String揭秘}
对于String类型,我们首先来看看其JDK内部的成员变量的声明代码:

sss.png

我们会看到它内部维护着一个char数组,而且它是由final关键词修饰的,说明它一旦创建之后不可变。对于String的创建,比较特殊一些,我们来看一下它的具体创建原理:

最常见的String操作莫过于拼接字符串了,在拼接字符串时,我们尽量用+,因为通常编译器会做出优化,如String test="hello "+"world",编译器会将其视为String test="hello world"。所以在拼接国泰字符串时,我们需要尽量使用StringBuffer或者StringBuilder的append方法,这样可以减少构造过多的临时String对象。下面我们来看一个简单的实例来证实:

ddd.png
在String对象中有一个特殊的方法,它是一个本地方法,当调用该方法时,如果池中已经包含了一个等于此String对象的字符串,则返回池中的字符串,否则,将此对象添加到池中,并且返回String对象的引用。

在上面的一个例子中,str1和str4并不是同一个对象引用,因此不相等,那么我们使用intern方法,添加一句,观察运行结果:


sdsds.png

也许很多人想到我们可以使用intern方法来创建对象,避免使用new创建大量的对象,但是这也有一个隐含的问题。

使用String的\color{red}{intern()}方法返回JVM对字符串缓存池里已经存在的字符串引用,从而解决内存性能问题,但是intern方法使用的池是JVM全局的池,很多情况下我们的程序并不需要如此大作用域的缓存,而且,它所使用的是JVM heap中PermGen对应的区域,PermGen通常是用来存放装载类和创建类实例时用到的元数据,因此,使用过多的intern方法会导致PermGen过度增长而最后返回OOM,因此垃圾收集器不会对缓存的String做垃圾回收,因此不建议使用。

实际中,如果需要创建大量的字符串,我们可以自己构建缓存,比如使用HashMap,将需缓存的String作为key和value放在HashMap中,例如下面代码:

public String getCacheString(String key){
    String temp=cacheMap.get(key);
    if(temp!=null){
        return temp;
    }else{
        cacheMap.put(key,key);
        return key;
    }
}

在字符串的使用中,另一个常见的操作是截取字符串,在String内部提供了\color{red}{substring}方法供我们使用,其源码如下(1.8版本):

41.png 42.png 43.png

从上面的源码可以看出,substring方法截取字符串的时候,会将String的原生内容复制到新的子字符串中,从整个方法的调用链来看,它会保存原始String。因此这也引发了下面的问题。

所幸的是,在JDK1.7之后的版本中,将substring的内部实现修改为使用Arrays进行拷贝,不再复用之前的原字符串,因此使其得以回收,所以String内存泄漏的问题也得到了修复。

如果使用了1.7之前的API,也可以使用下面的方法来解决内存泄漏问题。

看一个用例:

public class TestSubString {

    public static void main(String[] args) {
        List<String> list=new ArrayList<String>();
        for(int i=0;i<1000;i++){
            SubString1 str1=new SubString1();
            SubString2 str2=new SubString2();
            list.add(str1.getSubString(1,6));
            list.add(str2.getSubString(1,6));
        }

    }

    public static class SubString1{
        public String str=new String(new char[10000000]);
        public String getSubString(int begin,int end){
            return new String(str.substring(begin, end));      //使用new重新创建字符串
        }
    }

    public static class SubString2{
        public String str=new String(new char[10000000]);
        public String getSubString(int begin,int end){
            return str.substring(begin, end);                   //直接截取返回
        }
    }
}

在这个用例中,原始字符串很大,但是需要截取的却是很小的一段,因此在这种场景下推荐使用SubString1重新new一个字符串来释放原始字符串的方式来截取字符串,这样避免了原始字符串不能被回收,存在内存泄漏的问题

上一篇下一篇

猜你喜欢

热点阅读