java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > StringTable详解

Java的字符串常量池StringTable详解

作者:安然望川海

这篇文章主要介绍了Java的字符串常量池StringTable详解,JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化,为 了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,需要的朋友可以参考下

什么是字符串常量池

字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为 了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中, 就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并放到池中。

Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突 进行共享

string的string Pool是一个圉定大小的Hashtable,默认值大小长度是1009。如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。

参数设置

使用-xX :StringTablesize可设置stringTable的长度

在jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTablesize设置没有要求在jdk7中,stringTable的长度默认值是60013 jdk8开始,1009是可设置的最小值

字符串常量池中是不会存储相同内容的字符串的(equals方法返回为true,即字面量相等)

常量池位置的调整

Java 6及以前,字符串常量池存放在永久代。 Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。

所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了

字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7 中使用string.intern()。

Java8元空间,字符串常量在堆内存中

为什么要从永久代调整位置到堆中?

因为我们在开发中会创建大量的字符串常量,回收效率低,导致永久代空间不足。放在堆里能及时回收内存

方法原理

常量与常量的拼接结果在常量池,原理是编译期优化

public class StringTest {
    public static final String A = "a";
    public static void main(String[] args) {
        String s = "a"+"b";//两个字符串常量拼接
        String s1 = "ab";
        String s2 = A+"b";//常量引用和字符串常量拼接
        System.out.println(s == s1);
        System.out.println(s == s2);
        System.out.println(s2 == s1);
    }
}

以上三个字符串都存储在常量池中,所以打印结果都是true 因为被final修饰的字符串会在编译期放入常量池

只要其中有一个是变量,结果就在堆中。变量拼接的原理是stringBuilder

		String s1 = "ab";
        String s2 = "a";
        String s3 = s2+"b";

字节码

在这里插入图片描述

可以看到String s3 = s2+"b";的过程等价于

第6行表示new一个StringBuilder对象 (jdk5之前是StringBuffer)

第10行执行构造方法, 14行表示调用append方法拼接“a”

19行同理拼接“b”, 然后第22行表示调用toString方法

StringBuilder sb = new StringBuilder();//6
        sb.append("a");//14
        sb.append("b");//19
        sb.toString();//22

查看StringBuilder源码:oString方法new 了一个区别于常量池地址的新的String 对象

public String toString() {
	        // Create a copy, don't share the array
	        return new String(value, 0, count);
	    }

如果拼接的结果调用intern ()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

 private static void test2() {
        String s1 = "ab";
        String s2 = "a";
        String s3 = s2+"b";
        String s4 = s3.intern();
        System.out.println(s3 == s1);//false
        System.out.println(s4 == s1);//true
    }

intern 方法会先看常量池中是否有“ab”,若有,直接返回地址,若没有,创建“ab”到常量池并返回结果。 所以s4的地址指向常量池中的“ab”;

append和+的效率

我们已经通过字节码看到“a”+“b”会new一个StringBuilder,调用append方法并toString,此时new 了一个StringBuilder,new 了一个String,注意此时常量池不会创建“ab”常量

所以在一个循环里String a += "xxx"的效率是远低于在循环中sb.append(“xxx”)最后toSring的效率的,后者少创建了相当于n-1个StringBuiler对象 再看StringBuiler的构造方法:

/**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }
/**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

默认数组长度是16,如果拼接的操作远大于16,也是会引起频繁扩容的,所以如果知道字符串的最终长度不超过某个值,可以直接将这个值通过构造器传入以提高性能

面试题

new string(“a”)创建了几个对象?

private static void test4(){
        String s1 = new String("a");
    
    }

字节码

0 new #14 <java/lang/String>
 3 dup
 4 ldc #7 <a>
 6 invokespecial #15 <java/lang/String.<init>>
 9 astore_0
10 return

可以看到答案是两个,new一个Stirng对象字节码第0行,方法区放入“a”对象对应字节码第4行(如果之前常量池没有的话)

new string(“a”) + new string(“b”)创建了几个对象?

String s1 = new String("a") + new String("b");

字节码

 0 new #8 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #9 <java/lang/StringBuilder.<init>>
 7 new #14 <java/lang/String>
10 dup
11 ldc #7 <a>
13 invokespecial #15 <java/lang/String.<init>>
16 invokevirtual #10 <java/lang/StringBuilder.append>
19 new #14 <java/lang/String>
22 dup
23 ldc #11 <b>
25 invokespecial #15 <java/lang/String.<init>>
28 invokevirtual #10 <java/lang/StringBuilder.append>
31 invokevirtual #12 <java/lang/StringBuilder.toString>
34 astore_0
35 return

可以看到常量池两个对象常量“a”和“b”(11和23行),两个new的String对象(7和19),还有一个StringBudder对象(0),特别注意31行的toString方法又new了一个“ab”的对象,而不放入字符串常量池,所以答案是6个

intern方法的更改

private static void test5(){
        String s1 = new String("a") ;
        s1.intern();
        String s2 = "a";
        System.out.println(s1 == s2);//false

    }

这里结果为false,很正常,注意的是s1.intern();并没有返回重新赋值,如果修改为s1 = s1.intern();则结果true;

再修改一下:

private static void test5(){
        String s1 = new String("a") + new String ("b");
        s1.intern();
        String s2 = "ab";
        System.out.println(s1 == s2);//false?

    }

我们知道第一段代码并没有在常量池生成“ab”,第二行代码生成了但是没返回,结果是不是false?

很遗憾,jdk6及其之前是false,但是jdk7以后是true,为什么呢,这就是提一下intern在jdk7之后做的改动:如果常量池没有对应的字符串常量------- jdk6以及之前,该方法会在常量池中创建新的对象,这很好理解返回结果是false,但是在jdk7之后,如果堆空间已经有了我们第一行new的“ab”后,只会在常量池创建一个指向前者的指针,所以返回是true;更进一步,去掉代码s1.intern();则结果就是false了,因为这时候常量池中是新的对象,而非第一个对象的指针

如果你现在回头看上一个案例,为应该发现的是重点是String s1 = new String(“a”) ;和String s1 = new String(“a”) + new String (“b”);的区别:后者没有在常量池生成“ab”对象.

到此这篇关于Java的字符串常量池StringTable详解的文章就介绍到这了,更多相关StringTable详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文