`
BrokenDreams
  • 浏览: 249065 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
68ec41aa-0ce6-3f83-961b-5aa541d59e48
Java并发包源码解析
浏览量:97973
社区版块
存档分类
最新评论

关于Java String的一些总结

阅读更多

关于Java String的一些总结

作者:大飞

 

  • 不变模式设计
       String是典型的不变模式设计。主要体现在两点:
       1.String Class由final修饰,保证了无法产生可改变String语义的子类。
       2.一些有"修改"行为的方法都会产生新的String实例,如concat、replace等方法。
 
  • ==相关的问题
       看一些常见的判断String相等的问题,给一段代码:
public class StringEqualsTest {
	
	public static final String s1 = "5";
	public static String s2 = "5";
	
	public static String getS(){
		return "5";
	}
	public static void main(String[] args) {
		String a1 = "12";
		String a2 = "12";
		String a3 = new String("12");
		String a4 = new String("12");
		System.out.println(a1 == a2); //1
		System.out.println(a3 == a4); //2
		System.out.println(a1 == a3); //3
		System.out.println("============");
		//==============================
		String b1 = "34";
		String b2 = "3" + "4";
		String b3 = 3 + "4";
		String b4 = "3" + 4;
		System.out.println(b1 == b2); //4
		System.out.println(b1 == b3); //5
		System.out.println(b1 == b4); //6
		System.out.println("============");
		//==============================
		String c1 = "56";
		String c2 = "5";
		String c3 = "6";
		String c4 = c2 + "6";
		String c5 = c2 + c3;
		final String c6 = "5";
		String c7 = c6 + "6";
		String c8 = s1 + "6";
		String c9 = s2 + "6";
		String c0 = getS() + "6";
		System.out.println(c1 == c4); //7
		System.out.println(c1 == c5); //8
		System.out.println(c1 == c7); //9
		System.out.println(c1 == c8); //10
		System.out.println(c1 == c9); //11
		System.out.println(c1 == c0); //12
	}
	
}
       输出如下:(运行环境JDK1.8) 
true
false
false
============
true
true
true
============
false
false
true
true
false
false
       分析:
       情况1:a1和a2都来自字符串池,所以为true。
       情况2:a3和a4分别是堆内存里不同的对象,所以为false。
       情况3:a1来自字符串池,a3是堆内存新建的对象,所以为false。
 
       情况4:b1来自字符串池,b2由于编译器优化,变成"34",从字符串池中获取,所以为true。
       情况5:b1来自字符串池,b3由于编译器优化,变成"34",从字符串池中获取,所以为true。
       情况6:b1来自字符串池,b4由于编译器优化,变成"34",从字符串池中获取,所以为true。
 
       情况7:c1来自字符串池,c4编译后变成StringBuilder的拼接(append),最后由StringBuilder的toString方法返回一个新的String实例,所以为false。
       情况8:c1来自字符串池,c5同c4,所以为false。
       情况9:c1来自字符串池,c7由于编译器优化(c6由final修饰,不会变了,可以优化),变成"56",所以为true。
       情况10:c1来自字符串池,c8由于编译器优化(s1为常量,不会变了,可以优化),变成"56",所以为true。
       情况11:c1来自字符串池,c9同c4(s2为静态变量,但编译器无法保证s2是否会发生变化,所以无法直接优化成"56"),所以为false。
       情况12:c1来自字符串池,c0同c4(编译器无法保证getS方法返回"56"且不变,所以无法直接优化成"56"),所以为false。
 
  • substring的一些历史
       在jdk1.6,String的substring方法有一些问题,可能会导致内存泄露,看下源码: 
    public String substring(int beginIndex, int endIndex) {
	    if (beginIndex < 0) {
	        throw new StringIndexOutOfBoundsException(beginIndex);
	    }
	    if (endIndex > count) {
	        throw new StringIndexOutOfBoundsException(endIndex);
	    }
	    if (beginIndex > endIndex) {
	        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
	    }
	    return ((beginIndex == 0) && (endIndex == count)) ? this :
	        new String(offset + beginIndex, endIndex - beginIndex, value);
    }
    String(int offset, int count, char value[]) {
	    this.value = value;
	    this.offset = offset;
	    this.count = count;
    }
       可见,substring后,返回的新字符串会和旧字符串共享内部的value(char数组),这样看似合理共享利用的内存,但可能会带来一些问题:如果程序从外部读到了一个大字符串,然后只需要大字符串的一部分,做了substring,但实际上还是会hold住整个大字符串的内容在内存里面。如果这样的情况很多,就可能会出现问题。
 
       在jdk1.7后,String类做了调整,去掉了offset和count域,substring也做了调整,看下源码:
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 substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
       可见,jdk1.7后,substring内部返回的新字符串不会共享旧字符串的内部字符数组,而是会按需拷贝一个新的数组。
 
  • 字符串池相关
       前面提到了字符串池的概念,究竟什么是字符串池呢?我们从String的一个特殊方法intern入手简单分析下:
    public native String intern();
       这个方法的意思是:如果当前字符串存在于字符串池中,就返回池中的字符串对象的引用;否则添加当前字符串对象到字符串池中,然后返回当前字符串对象的引用。
       举个栗子:
	public static void main(String[] args) {
		String s = new String(new char[]{'1','4','7'});
		s.intern();
		System.out.println(s == "147");
		String s2 = new String("258");
		s2.intern();
		System.out.println(s2 == "258");
	}
       输出如下:
true
false
       你猜对了吗?修正:jdk版本为1.8
        
 
 
       来看下intern内部怎么实现的。
       打开openjdk代码,找到openjdk\jdk\src\share\native\java\lang目录,找到String.c代码如下:
#include "jvm.h"
#include "java_lang_String.h"
JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}
       然后找到实现,在hotspot\src\share\vm\prims\jvm.cpp中:
// String support ///////////////////////////////////////////////////////////////////////////
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper("JVM_InternString");
  JvmtiVMObjectAllocEventCollector oam;
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END
       可见,这里是调用了StringTable的intern方法,看下StringTable的定义和相关实现,定义在hotspot\src\share\vm\classfile\symbolTable.hpp中:
class StringTable : public RehashableHashtable<oop, mtSymbol> {
  friend class VMStructs;
private:
  // The string table
  static StringTable* _the_table;
...
public:
  // The string table
  static StringTable* the_table() { return _the_table; }
  // Size of one bucket in the string table.  Used when checking for rollover.
  static uint bucket_size() { return sizeof(HashtableBucket<mtSymbol>); }
  static void create_table() {
    assert(_the_table == NULL, "One string table allowed.");
    _the_table = new StringTable();
  }
       StringTable就是一个hash表。
       再看下相关的实现,实现在hotspot\src\share\vm\classfile\symbolTable.cpp中:
oop StringTable::intern(oop string, TRAPS)
{
  if (string == NULL) return NULL;
  ResourceMark rm(THREAD);
  int length;
  Handle h_string (THREAD, string);
  jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL);
  oop result = intern(h_string, chars, length, CHECK_NULL);
  return result;
}
oop StringTable::intern(Handle string_or_null, jchar* name,
                        int len, TRAPS) {
  unsigned int hashValue = hash_string(name, len);
  int index = the_table()->hash_to_index(hashValue);
  oop found_string = the_table()->lookup(index, name, len, hashValue);
  // Found
  if (found_string != NULL) {
    ensure_string_alive(found_string);
    return found_string;
  }
  debug_only(StableMemoryChecker smc(name, len * sizeof(name[0])));
  assert(!Universe::heap()->is_in_reserved(name),
         "proposed name of symbol must be stable");
  Handle string;
  // try to reuse the string if possible
  if (!string_or_null.is_null()) {
    string = string_or_null;
  } else {
    string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
  }
#if INCLUDE_ALL_GCS
  if (G1StringDedup::is_enabled()) {
    // Deduplicate the string before it is interned. Note that we should never
    // deduplicate a string after it has been interned. Doing so will counteract
    // compiler optimizations done on e.g. interned string literals.
    G1StringDedup::deduplicate(string());
  }
#endif
  // Grab the StringTable_lock before getting the_table() because it could
  // change at safepoint.
  oop added_or_found;
  {
    MutexLocker ml(StringTable_lock, THREAD);
    // Otherwise, add to symbol to table
    added_or_found = the_table()->basic_add(index, string, name, len,
                                  hashValue, CHECK_NULL);
  }
  ensure_string_alive(added_or_found);
  return added_or_found;
}
       逻辑很简单,首先从StringTable中找给定的字符串对象,找到的话就直接返回,找不到就创建一个字符串对象,然后添加到StringTable中,然后返回其引用。
 
 
       jdk1.6与jdk1.7、1.8的字符串池实现的一点区别:
 
       1.6的字符串池实现在PermGen区中而1.7和1.8的字符串池实现在堆内存里面。相对于1.6来说,在1.7或1.8下使用字符串池,受限制的堆内存的大小,而不是相对较小的PermGen的大小。
 
       1.7和1.8也提供了相关JVM参数来调整和观察StringTable,参数有:
       -XX:StringTableSize 设置StringTable的size。
       -XX:+PrintStringTableStatistics 在程序结束时打印StringTable的一些使用情况。
 
       看个栗子:
	public static void main(String[] args) {
		int n = 0;
		for(int i=0;i<10000000;i++){
			String s = ""+n++;
			s.intern();
		}
		System.out.println("over");
	}
       运行时jvm参数为:-XX:+PrintStringTableStatistics    
       输出如下:
over
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     27283 =    654792 bytes, avg  24.000
Number of literals      :     27283 =   1166312 bytes, avg  42.749
Total footprint         :           =   1981192 bytes
Average bucket size     :     1.363
Variance of bucket size :     1.344
Std. dev. of bucket size:     1.159
Maximum bucket size     :         8
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :    815780 =  19578720 bytes, avg  24.000
Number of literals      :    815780 =  45819984 bytes, avg  56.167
Total footprint         :           =  65878808 bytes
Average bucket size     :    13.593
Variance of bucket size :     7.484
Std. dev. of bucket size:     2.736
Maximum bucket size     :        23
       我们可以看到StringTable的使用情况。同时也能看到默认的StringTableSize是60013(运行的jdk版本是jdk1.8.0_25)。
 
       再加上-XX:StringTableSize=120013参数跑一下,输出如下:
over
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     27273 =    654552 bytes, avg  24.000
Number of literals      :     27273 =   1165864 bytes, avg  42.748
Total footprint         :           =   1980504 bytes
Average bucket size     :     1.363
Variance of bucket size :     1.343
Std. dev. of bucket size:     1.159
Maximum bucket size     :         8
StringTable statistics:
Number of buckets       :    120013 =    960104 bytes, avg   8.000
Number of entries       :    796602 =  19118448 bytes, avg  24.000
Number of literals      :    796602 =  44744552 bytes, avg  56.169
Total footprint         :           =  64823104 bytes
Average bucket size     :     6.638
Variance of bucket size :     4.038
Std. dev. of bucket size:     2.010
Maximum bucket size     :        15
            
       2016年1月8号补充=================================================
       今天看到一个讨论,下面的代码在jdk1.6和jdk1.7之后版本,结果不同:
       (这里的代码并非讨论中的代码,但说的是同一件事。为了和上面的例子承接)
	public static void main(String[] args) {
	    String s = new String(new char[]{'1','4','7'});  
	    s.intern();  
	    String s2 = "147";
	    System.out.println(s == s2);  
	    
	    String s3 = "258";  
	    s3.intern();  
	    String s4 = "258";
	    System.out.println(s3 == s4); 
	}
  
       经过测试上面的程序在jdk1.6下输出false true;在jdk1.8下输出true、true。
       所以我前面那个“让你猜”的例子是不够严谨的,当时没有重点说明运行版本。
       下面看下为什么会这样:
       在1.6版本的openjdk代码里,StringTable的oop StringTable::intern(Handle string_or_null, jchar* name,int len, TRAPS)方法实现中有这么一行:
  // try to reuse the string if possible
  if (!string_or_null.is_null() && (!JavaObjectsInPerm || string_or_null()->is_perm())) {
    string = string_or_null;
  } else {
    string = java_lang_String::create_tenured_from_unicode(name, len, CHECK_NULL);
  }
       而在1.8版本的openjdk代码里,这行是这样的:
  // try to reuse the string if possible
  if (!string_or_null.is_null()) {
    string = string_or_null;
  } else {
    string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
  }
  
       我们重点关注里面的条件语句,1.6下会多出这个条件
       (!JavaObjectsInPerm || string_or_null()->is_perm())
       这个JavaObjectsInPerm没太深究,但猜测这里显然是true,如果是false的话这句条件就没意义了。接下来会判断一下string_or_null是不是perm中的对象,这句比较关键:
       如果string_or_null是perm中的对象,那么if条件成立,最终会将这个string_or_null放入StringTable中;反之,if条件不成立,最终会再次创建一个String对象,然后放入StringTable中。
       so,再回头看看刚给出的代码,在1.6版本下,开始创建一个String s = new String(new char[]{'1','4','7'})的对象,然后调用intern方法,由于并未出现"147"这种字面量,所以接下来的intern方法中,会在StringTable中找不到对象,然后走上面提到的逻辑,那么接下来会判断s是否在perm里,显然不是,所以intern中新建了一个String对象,放到StringTable中,下一句String s2 = "147" 是从StringTable中拿的对象,所以s和s2当然不是同一个对象了。
       如果是1.8版本,只要走了上面的逻辑,就会将String对象加入StringTable,没有是否存在于perm的判断了。所以s和s2还是同一个对象。
 
       OK!关于Java String的一些总结就到这里。
       相关源码:
 
 
分享到:
评论
10 楼 BrokenDreams 2016-01-07  
浩_亿 写道
BrokenDreams 写道
浩_亿 写道
StringTable有个unlink方法,GC的时候会调用这个方法。该方法会清理掉StringTable中的stale对象,也就是没有外部对其进行引用的对象。如果你一方面保留了外部引用,一方面又不断的往StringTable里放对象,那肯定会溢出的。

感谢!解决了我的疑惑。

你还在那上班?


恩,还在坚持。对了,你说的“StringTable有个unlink方法”和JUC的源码地址能发一个,是在openJDK下载的?还有里面这么多好的文章应该多多在其它地方分享一下的,我就在开发者头条中分享了你将关于“java并发”的相关文章地址的,介意这样做?

StringTable源码可以在openjdk里面找到,openjdk里面jdk6的相关代码地址:http://hg.openjdk.java.net/jdk6/。StringTable的是 http://hg.openjdk.java.net/jdk6/jdk6/hotspot/file/9732f3600a48/src/share/vm/classfile/symbolTable.hpp

JUC源码就是java.util.concurrent包,jdk源码就自带的,安装下源码包就可以了。

分享当然不介意啊,我还得感谢你的支持和宣传呢哈哈~~
9 楼 浩_亿 2016-01-07  
BrokenDreams 写道
浩_亿 写道
StringTable有个unlink方法,GC的时候会调用这个方法。该方法会清理掉StringTable中的stale对象,也就是没有外部对其进行引用的对象。如果你一方面保留了外部引用,一方面又不断的往StringTable里放对象,那肯定会溢出的。

感谢!解决了我的疑惑。

你还在那上班?


恩,还在坚持。对了,你说的“StringTable有个unlink方法”和JUC的源码地址能发一个,是在openJDK下载的?还有里面这么多好的文章应该多多在其它地方分享一下的,我就在开发者头条中分享了你将关于“java并发”的相关文章地址的,介意这样做?
8 楼 BrokenDreams 2016-01-06  
浩_亿 写道
StringTable有个unlink方法,GC的时候会调用这个方法。该方法会清理掉StringTable中的stale对象,也就是没有外部对其进行引用的对象。如果你一方面保留了外部引用,一方面又不断的往StringTable里放对象,那肯定会溢出的。

感谢!解决了我的疑惑。

你还在那上班?
7 楼 浩_亿 2016-01-06  
StringTable有个unlink方法,GC的时候会调用这个方法。该方法会清理掉StringTable中的stale对象,也就是没有外部对其进行引用的对象。如果你一方面保留了外部引用,一方面又不断的往StringTable里放对象,那肯定会溢出的。

感谢!解决了我的疑惑。
6 楼 BrokenDreams 2016-01-06  
浩_亿 写道
public void compute(Person person){
//此处的ID为32为UUID
String id = person.getId().intern();
synchronized(id){
      dosomething();
}
}
如上述方法,系统不同的person对象会存在几百万个,如果这样person.getId().intern();调用都会加载到字符串常量池中,会不会导致jvm内存溢出?或者说jvm会在什么情况下回收这部分内存?

StringTable有个unlink方法,GC的时候会调用这个方法。该方法会清理掉StringTable中的stale对象,也就是没有外部对其进行引用的对象。如果你一方面保留了外部引用,一方面又不断的往StringTable里放对象,那肯定会溢出的。
5 楼 浩_亿 2016-01-06  
public void compute(Person person){
//此处的ID为32为UUID
String id = person.getId().intern();
synchronized(id){
      dosomething();
}
}
如上述方法,系统不同的person对象会存在几百万个,如果这样person.getId().intern();调用都会加载到字符串常量池中,会不会导致jvm内存溢出?或者说jvm会在什么情况下回收这部分内存?
4 楼 BrokenDreams 2015-12-27  
pcgreat 写道
哈哈哈 我已经找到了

才看到... javap就可以吧
3 楼 pcgreat 2015-12-24  
哈哈哈 我已经找到了
2 楼 pcgreat 2015-12-24  
或者 好用分析的方法
1 楼 pcgreat 2015-12-24  
求好用的class 分析工具呀

相关推荐

    java的String用法类型总结

    一些有关java的String类型的总结。

    javaString总结共13页.pdf.zip

    javaString总结共13页.pdf.zip

    JAVA string函数总结.docx

    String类函数总结、日期格式化、正则表达式介绍,详细源码和结果

    JAVA中String与StringBuffer的区别 自己的学习笔记总结

    JAVA中String与StringBuffer的区别 JAVA中String与StringBuffer的区别 自己的学习笔记总结

    有关Java String常用方法的总结.docx

    有关Java String常用方法的总结.docx

    javaString.docx

    Java-String类的常用方法总结,String类在java.lang包中,java使用String类创建一个字符串变量,字符串变量属于对象。java把String类声明的final类,不能有类。String类对象创建后不能修改,由0或多个字符组成,包含...

    Java基础知识点总结.docx

    &lt; java.lang &gt;String字符串:★★★☆ 71 &lt; java.lang &gt;StringBuffer字符串缓冲区:★★★☆ 73 &lt; java.lang &gt;StringBuilder字符串缓冲区:★★★☆ 74 基本数据类型对象包装类★★★☆ 75 集合框架:★★★★★,...

    Java中String类中的常用方法.TXT

    简单总结可以下Java中String类中的常用方法

    JAVA常用类总结

    JAVA常用类,java封装类,String类和StringBuffer类......

    Java中String.format的使用方法总结

    主要介绍了Java中String.format的用法总结的相关资料,需要的朋友可以参考下

    java字符串总结

    很全面的总结,十分实用。很全面的介绍了String的用法,很实用,实战总结。

    java重要知识点总结

    public static void main(String[] args){ } 5.public类名必须跟java文件同名,且一个java文件中只能包括一个public类。 6.java注释: // 单行注释 /* */ 多行注释(不能嵌套) //* */ 文档说明注释 ...... ......

    JAVA程序的总结和提升

     public static void main(String[] args) {…}  在Java中,用花括号划分程序的各个部分,任何方法的代码都必须以“{”开始,以“}”结束, 由于编译器忽略空格,所以花括号风格不受限制。  Java中每个语句...

    关于java webservice 技术的总结

    java 调用webservice的各种方法总结  现在webservice加xml技术已经逐渐成熟,但要真正要用起来还需时日!!  由于毕业设计缘故,我看了很多关于webservice方面的知识,今天和大家一起来研究研究webservice的各种...

    个人java总结笔记

    Class 对象由Java虚拟机在使用到该类的时候,自动创建; Class 对象的三种获取方式: 1: Class.forName(“全类名”); 2:类对象名.getClass(); 3:类名.Class(); 通过Class 对象获得类的指定成员,构造方法 构造方法...

    Java中String类方法总结

    获取String字符串中指定下标位置的char类型字符,如果index超出有效范围 StringIndexOutOfBoundsException int indexOf(char ch); int indexOf(String str); int indexOf(char ch, int fromIndex); int indexOf...

    java面试问题总结

    2. StringBuffer与StringBuilder,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,不像String一样创建一些对象进行操作,所以速度就快了。 3. StringBuilder :...

    java 面试题 总结

    java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类 3、int 和 Integer 有什么区别 Java 提供两种不同的类型:引用类型和原始类型(或内置...

    java-API-String类

    java基础,String类基础,自己总结的笔记。

Global site tag (gtag.js) - Google Analytics