1. public class StringTest { 2.
3. public static void main(String[] args) {
4. String str = new String(\"abc\"); //语句(1) 5. String str1 = \"abc\";//语句(2)
6. String str2 = new String(\"abc\");//语句(3) 7.
8. System.out.println(str == str1);//语句(4) 9. System.out.println(str == str2);//语句(5) 10. System.out.println(str1 == str2);//语句(6) 11.
12. System.out.println(str == str.intern());//语句(7) 13. System.out.println(str1 == str1.intern());//语句(8) 14. System.out.println(str.intern() == str2.intern());//语句(9) 15.
16. String hello = \"hello\";//语句(10) 17. String hel = \"hel\";//语句(11) 18. String lo = \"lo\";//语句(12) 19.
20. System.out.println(hello == \"hel\" + \"lo\");//语句(13) 21. System.out.println(hello == \"hel\" + lo);//语句(14) 22. } 23.}
问题1:当执行完语句(1)时,在内存里面生成几个对象?它们是什么?在什么地方?
解答:当执行完语句(1)时,内存里面生成2个对象,它们的内容分别都是abc,注意:str不是对象,它是对象的地址,它叫做引用 (reference),str指向new...生成的对象。换句话说,在java里面,当我们定义一个类的变量(如:String str;),它永远都是引用,不是对象。那么什么是对象呢?当我们用关键字new时,它生成出来的东西叫做对象。为什么是两个对象呢?首先它生成一个对象 是abc,这个abc对象在什么地方呢?它在一个叫String Pool的字符串池里面,只有String有这样一个String池。String池是一个什么概念呢?我们知道,String类是一个不可变的类,一但 它的内容确定,它就不能去更改了。当你去生成一个字符串对象的时候,它的执行流程是这样的:它首先在你的String Pool里面去找,看有没有一个内容为abc的对象存在,因为tring str = new String(\"abc\")它是main方法的第一个语句,那么在刚开始执行的时候,String Pool里面是没有对象的。它发现String Pool里面没有abc这个对象,那么它首先把new String(\"abc\")的括号里面的abc对象放到String Pool里面,接下来它执行
new ...这行语句, 执行String的构造方法。我们知道new它生成一个对象,这个对象在什么地方呢?在java的堆里面。我们知道java的内存分为2部分,一个叫栈 (Stack),一个叫堆(Heap)。那么new String(\"abc\")时,它在堆(Heap)里面,生成一个内容为abc的这样一个对象。这样就造成了在String Pool里面一个叫abc的对象,堆里面也有一个叫abc的对象。我们这里用的是public String(String original)这个构造方法。jdk api是这样叙述的:
Initializes a newly created String object so that is represents the same sequence of characters as the argument;in other words,the newly created string is a copy of the argument string.Unless an explicit copy of original is needed,use of this constructor is unnecessary since Strings are immutable.(翻译:初始化一个新创建的String对象,表示一个与该参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的一个副 本。由于 String 是不可变的,不必使用该构造方法,除非需要original的显式副本。)
问题2:当执行完语句(2)时,在内存里面一共有几个对象?它们是什么?在什么地方?
解答:当执行完语句(2)时,内存里面一个新的对象都没有生成。为什么这么说?当我们定义语句(2)的时候,如果我们用字符串的常量值(字面值) 给str1赋值的话,那么首先java还是从String Pool里面去查找没有有内容为abc的这样一个对象存在,我们发现当我们执行完语句(1)的时候,StringPool里面已经存在了内容为abc的对 象,那么就不会再在tring Pool里面去生成内容为abc的字符串对象了。而是会使用已经存在String Pool里面的内容为abc的字符串对象,并且会将str2这个引用指向String Pool里面的内容为abc的字符串对象,str2存放的是String Pool里面的内容为abc的字符串对像的地址。也就是说当你使用String str2 = \"abc\",即使用字符串常量(\"abc\")给定义的引用(str2)赋值的话,那么它首先是在String Pool里面去找有没有内容为abc的字符串对象存在,如果有的话,就不用创建新的对象,直接引用String Pool里面已经存在的对象;如果没有的话,就在 String Pool里面去创建一个新的对象,接着将引用指向这个新创建的对象。所以,当执行完语句(2)时内存里面一共有2个对象,它们的内容分别都是abc, 在String Pool里面一个内容abc的对象,在堆里面有一个内容为abc的对象。
问题3:当执行完语句(3)时,在内存里面一共有几个对象?它们是什么?在什么地方?
解答:执行完语句(3)时,执行过程是这样的:它首先在String Pool里面去查找有没有内容为abc的字符串对象存在,发现有这个对象存在,它就不去创建 一个新的对象。接着执行new...,只要在java里面有关键字new存在,就表示它生成一个新的对象,new多少次,就生成多少个对象,而且新生成的 对象都是在堆里面,所以它会在堆里面生成一个内容为abc的对象,并且将它的地址赋给了引用str2,str2就指向刚在堆里面生成的内容为abc的对 象。所以,当执行完语句(3)时,内存里面一共有3个对象,其中包含了一个在String Pool里面内容为abc的字符串对象,另外在堆里面包含了两个内容为abc的字符串对象。
问题4:当执行完语句(4)(5)(6)后,它们的结果分别是什么?
解答:在java里面,对象用\"==\"永远比较的是两个对象的内存地址,换句话说,是比较\"==\"左右两边的两个引用是否指向同一个对象。对于 java里面的8种原生数据类型来说,\"==\"比较的是它们的字面值是不是一样的;对应用类型来说,比较的是它们的内存地址是不是一样的。在语句(1) (2)(3)中,由于str、str1、str2指向不同的对象,它们的内存地址就不一样,因此可以说当执行完语句(4)(5)(6),它们返回的结果都 是false。
问题5:当执行完语句(7)(8)(9)后,它们的结果分别是什么?
解答:jdk api里对方法public String intern()是这样叙述的:
Return a canonical representation for the string object.(翻译:返回字符串对象的标准化表示形式。)A pool of strings,initially empty,is maintained privately by the class String.(翻译:一个初始时为空的字符串池,它由类 String 私有地维护。) When the intern method is invoked,if the pool already contains a string equeal to this String objectas determined by the equals(Object) method,then the string from the pool is returned.Otherwise,this String object is added to the pool and a reference to the String object is returned.(翻译:当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。)If follows that for any two strings s and t,s.intern() == t.intern() is true if and only if s.equals(t) is true.(翻译:它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。)All literal strings and string-valued consrant expressions are interned.String literals and defined in §3.10.5 of the Java Language Specification.(翻译:所有字面值字符串和字符串赋值常量表达式都是内部的。字符串字面值在《Java Language Specification》的 §3.10.5 中已定义。) Returns:a string that has the same contents as this string,but is guaranteed ro be from a pool of unique strings. (翻译:返回一个字符串,内容与此字符串相同,但它保证来自字符串池中。) 当执行语句(7)时,首先,str这个对象指向的是堆中第一次new...生成的对象,根据jdk的api叙述,当调用 intern 方法时,如果String Pool已经包含一个等于此 String 对象的字符串(该对象由equals(Object)方法确定),则返回String Pool中的字符串对象的内存地址。因为String Pool中有内容为abc的对象,所以str.intern()返回的是String Pool中的内容为abc的字符串对象的内存地址,即是str1。而str==str1为false,所以,str == str.intern() 为false,即是语句(7)结果为false。而对于str1.intern(),它还是会首先检查String Pool中是否有内容为abc的对象,发现有,则将String Pool中内容为abc的对象的地址赋给str1.intern()方法的返回值,即str1.intern()的结果为str1。所以,str1 == str1.intern()的结果为true,,即是语句(8)结果为true。对于str.intern(),它首先检查String Pool中是否有内容为abc的对象,发现有,
则将String Pool中内容为abc的对象的赋给str.intern()方法的返回值,即str.intern()的结果为str1。对于 st2r.intern(),首先检查String Pool中是否有内容为abc的对象,发现有,则将String Pool中内容为abc的对象的地址赋给str2.intern()方法的返回值,即str2.intern()的结果为str1。所 以,str.intern() == str2.intern()的结果为true,,即是语句(9)结果为true。因此,当执行完语句(7)(8)(9)后,它们的结果分别是false、 true、true。
问题6:当执行完语句(13)(14)后,它们的结果分别是什么?
解答:执行完语句(13)结果为true,执行完语句(14)结果为false。分析:对于hello == \"hel\" + \"lo\",hello指向的是String Pool里面的内容为hello的字符串对象,对于\"hel\" + \"lo\",当\"+\"两边都是字面值(字符串常量)的时候,在执行完\"+\"之后,它接着去判断String Pool里面有没有内容为hello的字符串对象存在,有的话就直接返回String Pool里面的内容为hello的字符串对象的内存地址,所以,hello == \"hel\" + \"lo\"结果为true;对于hello == \"hel\" + lo,lo不是字面值,当\"+\"两边有一个不是字面值(字符串常量)的时候,那么\"+\"操作后又会在堆里面生成一个新的对象,也就是说hello的引用是 指向String Pool里面的内容为hello的字符串对象,\"hel\" + lo的结果是返回在堆里面生成一个新的对象,一个在String Pool里面,一个在堆里面,当然为false了。
String类的intern方法释疑
相信绝大多数的人不会去用String类的intern方法,打开String类的源码发现这是一个本地方法,定义如下:
public native String intern();
文档告诉我们该方法返回一个字符串对象的内部化引用:由String类维护一个初始为空的字符串的对象池,当intern方法被调用时,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。
从程序的角度上怎么来看这个方法呢,我们假设有两个字符串s1,s2,当s1.equals(s2)时,s1.intern()==s2.intern(),也就是说这两个字符串在内存中使用的是同一个实例。
Java语言规范中定义了字符串文字以及更一般的常量表达式的值的字符串是被内部化的,以便它们共享同一个实例。我们试验一下下面代码
String s1 = \"你好,Java自由人\";String s2 = \"你好,\" + \"Java自由人\";System.out.println(s1==s2);System.out.println(s1.intern()==s2.intern());
这段代码将打印两个true,也就是说字符串s1和s2是共享同一个实例。不过前提是尽管使用了表达式,但是表达式中必须都是常量。
方法intern()可以把String的对象放入到池冲并返回池中的对象。如果我们对s1(String s1 = new String(\"hello\"))调用intern,s1 = s1.intern()这时候,我们再把s1和s3进行\"==\"的判断,你会发现结果返回true! 看下面的例子 public class StringTest {
public static void main(String[] args) {
String s1 = \"hello\";
String s2 = new String(\"hello\"); String s3 = new String(\"hello\");
testString(s1,s2,s3); s2 = s2.intern();
System.out.println(\"after s2.intern\"); testString(s1,s2,s3); }
private static void testString(String s1,String s2,String s3) {
System.out.println(\"s1 = s2 is \"+(s1==s2)); System.out.println(\"s2 = s3 is \"+(s2==s3));
System.out.println(\"s1.equals(s2) is \"+s1.equals(s2)); System.out.println(\"s2.equals(s3) is \"+s2.equals(s3)); } }
输出结果为 s1 = s2 is false s2 = s3 is false s1.equals(s2) is true s2.equals(s3) is true after s2.intern s1 = s2 is true s2 = s3 is false s1.equals(s2) is true
//==================================== public final class DfConstants {
public static final String CONTAINMENT = (new String(\"containment\")).intern(); public static final String ASSEMBLY = (new String(\"assembly\")).intern(); public static final String ROOT = (new String(\"root\")).intern();
public static final String ATTRVALUES = (new String(\"ATTRVALUES\")).intern(); public static final String EMPTY_STRING = (new String(\"\")).intern(); public static final String AMP_CHARREF = (new String(\"&\")).intern(); public static final String GREATERTHAN_CHARREF = (new String(\">\")).intern();
public static final String LESSTHAN_CHARREF = (new String(\"<\")).intern(); public static final String APOS_CHARREF = (new String(\"'\")).intern(); public static final String QUOTE_CHARREF = (new String(\""\")).intern(); public DfConstants() {
if(DfTracer.s_globalTraceEnabled && DfTracer.isEnabled()) {
DfTracer.traceMethodEntrance(\" com.documentum.fc.common.DfConstants.class, this, null); } } } Eclipse 的字符串分区共享优化机制 在 Java/C# 这样基于引用语义处理字符串的语言中,作为不可变对象存在的字符串,如果内容相同,则可以通过某种机制实现重用。因为对这类语言来说,指向内存中两块内存位置不同内容相同的字符串,与同时指向一个字符串并没有任何区别。特别是对大量使用字符串的 XML 文件解析类似场合,这样的优化能够很大程度上降低程序的内存占用,如 SAX 解析引擎标准中就专门定义了一 个 http://xml.org/sax/features/string-interning 特性用于字符串重用。 在语言层面,Java/C# 中都直接提供了 String.Intern 的支持。其中 C# 中优化的相关信息,可以参考我另外一篇文章《CLR中字符串不变性的优化》 而对 Java 来说,实现上的非常类似。由 String.intern 方法,将当前字符串以内容为键,对象引用为值,放入一个全局性的哈希表中。 以下内容为程序代码: // // java/lang/String.java // public final class String { //... public native String intern(); // 使用 JNI 函数实现以保障效率 } // // hotspot/src/share/vm/prims/jvm.cpp // JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str)) JVMWrapper(\"JVM_InternString\"[img]/images/wink.gif[/img]; if (str == NULL) return NULL; oop string = JNIHandles::resolve_non_null(str); // 将引用解析为内部句柄 oop result = StringTable::intern(string, CHECK_0); // 进行实际的字符串 intern 操作 return (jstring) JNIHandles::make_local(env, result); // 获取内部句柄的引用 JVM_END // // hotspot/src/share/vm/memory/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); // 获取实际字符串内容 oop result = intern(h_string, chars, length, CHECK_0); // 完成字符串 intern 操作 return result; } oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) { int hashValue = hash_string(name, len); // 首先根据字符串内容计算 哈希值 stringTableBucket* bucket = bucketFor(hashValue); // 根据哈希值获取目标容器 oop string = bucket->lookup(name, len); // 然后检测字符串是否已经存在 // Found if (string != NULL) return string; // Otherwise, add to symbol to table return basic_add(string_or_null, name, len, hashValue, CHECK_0); // 将字符串放入哈希表 } 对全局字符串表中的字符串,是没有办法显式手动清除的。只能在不使用此字符串后,由垃圾回收线程在进行不可达对象标记时进行分析,并最终调用 StringTable::unlink 方法去遍历清除。 以下内容为程序代码: // // hotspot/src/share/vm/memory/genMarkSweep.cpp // void GenMarkSweep::mark_sweep_phase1(...) { //... StringTable::unlink(); } // // hotspot/src/share/vm/memory/symbolTable.cpp // void StringTable::unlink() { // Readers of the string table are unlocked, so we should only be // removing entries at a safepoint. assert(SafepointSynchronize::is_at_safepoint(), \"must be at safepoint\"[img]/images/wink.gif[/img] for (stringTableBucket* bucket = firstBucket(); bucket <= lastBucket(); bucket++) { for (stringTableEntry** p = bucket->entry_addr(); *p != NULL[img]/images/wink.gif[/img] { stringTableEntry* entry = *p; assert(entry->literal_string() != NULL, \"just checking\"[img]/images/wink.gif[/img]; if (entry->literal_string()->is_gc_marked()) { // 字符串对象是否可达 // Is this one of calls those necessary only for verification? (DLD) entry->oops_do(&MarkSweep::follow_root_closure); p = entry->next_addr(); } else { // 如不可达则将其内存块回收到内存池中 *p = entry->next(); entry->set_next(free_list); free_list = entry; } } } } 通过上面的代码,我们可以直观了解到,对 JVM (Sun JDK 1.4.2) 来说, String.intern 提供的是全局性的基于哈希表的共享支持。这样的实现虽然简单,并能够在最大限度上进行字符串共享;但同时也存在共享粒度太大,优化效果无法度量,大量字符串可能导致全局字符串表性能降低等问题。 为此 Eclipse 舍弃了 JVM 一级的字符串共享优化机制,而通过提供细粒度、完全可控、可测量的字符串分区共享优化机制,一定程度上缓解此问题。Eclipse 核心 的 IStringPoolParticipant 接口由使用者显式实现,在其 shareStrings 方法中提交需要共享的字符串。 以下内容为程序代码: // // org.eclipse.core.runtime.IStringPoolParticipant // public interface IStringPoolParticipant { /** * Instructs this participant to share its strings in the provided * pool. */ public void shareStrings(StringPool pool); } 例如 MarkerInfo 类型实现了 IStringPoolParticipant 接口,在 其 shareStrings 方法中,提交自己需要共享的字符串 type,并通知其下级节点进行相应的提交。 以下内容为程序代码: // // org.eclipse.core.internal.resources.MarkerInfo // public class MarkerInfo implements ..., IStringPoolParticipant { public void shareStrings(StringPool set) { type = set.add(type); Map map = attributes; if (map instanceof IStringPoolParticipant) ((IStringPoolParticipant) map).shareStrings(set); } } 这样一来,只要一个对象树各级节点选择性实现 IStringPoolParticipant 接口,就可以一次性将所有需要共享的字符串,通过递归提交到一个字符串缓冲池中进行复用优化。如 Workspace 就是这样一个字符串共享根入口,其 open 方法在完成工作区打开操作后,将需要进行字符串共享优化的缓存管理对象,加入到全局字符串缓冲区分区优化列表中。 以下内容为程序代码: // // org.eclipse.core.internal.resources // public class Workspace ... { protected SaveManager saveManager; public IStatus open(IProgressMonitor monitor) throws CoreException { // 打开工作空间 // 最终注册一个新的字符串缓冲池分区 InternalPlatform.getDefault().addStringPoolParticipant(saveManager, getRoot()); return Status.OK_STATUS; } } 对需要优化的类型 SaveManager 来说,只需要实现 IStringPoolParticipant 接口,并在被调用的时候提交自己与子元素的需优化字符串即可。其子元素甚至都不需要实现 IStringPoolParticipant 接口,只需将提交行为一级一级传递下去即可,如: 以下内容为程序代码: // // org.eclipse.core.internal.resources.SaveManager // public class SaveManager implements ..., IStringPoolParticipant { protected ElementTree lastSnap; public void shareStrings(StringPool pool) { lastSnap.shareStrings(pool); } } // // org.eclipse.core.internal.watson.ElementTree // public class ElementTree { protected DeltaDataTree tree; public void shareStrings(StringPool set) { tree.storeStrings(set); } } // // org.eclipse.core.internal.dtree.DeltaDataTree // public class DeltaDataTree extends AbstractDataTree { private AbstractDataTreeNode rootNode; private DeltaDataTree parent; public void storeStrings(StringPool set) { //copy field to protect against concurrent changes AbstractDataTreeNode root = rootNode; DeltaDataTree dad = parent; if (root != null) root.storeStrings(set); if (dad != null) dad.storeStrings(set); } } // // org.eclipse.core.internal.dtree.AbstractDataTreeNode // public abstract class AbstractDataTreeNode { protected AbstractDataTreeNode children[]; protected String name; public void storeStrings(StringPool set) { name = set.add(name); //copy children pointer in case of concurrent modification AbstractDataTreeNode[] nodes = children; if (nodes != null) for (int i = nodes.length; --i >= 0[img]/images/wink.gif[/img] nodes[i].storeStrings(set); } } 所有的需优化字符串,都会通过 StringPool.add 方法提交到统一的字符串缓冲池中。而这个缓冲池的左右,与 JVM 级的字符串表略有不同,它只是在进行字符串缓冲分区优化时,起到一个阶段性的整理作用,本身并不作为字符串引用的入口存在。因此在实现上它只是简单的对 HashMap 进行包装,并粗略计算优化能带来的额外空间,以提供优化效果的度量标准。 以下内容为程序代码: // // org.eclipse.core.runtime.StringPool // public final class StringPool { private int savings; private final HashMap map = new HashMap(); public StringPool() { super(); } public String add(String string) { if (string == null) return string; Object result = map.get(string); if (result != null) { if (result != string) savings += 44 + 2 * string.length(); return (String) result; } map.put(string, string); return string; } // 获取优化能节省多少空间的大致估算值 public int getSavedStringCount() { return savings; } } 不过这里的估算值在某些情况下可能并不准确,例如缓冲池中包括字符串 S1,此时提交一个与之内容相同但物理位置不同的字符串 S2,则如果 S2 被提交多次,会导致错误的高估优化效果。当然如果需要得到精确值,也可以对其进行重构,通过一个 Set 跟踪每个字符串优化的过程,获得精确优化度量,但需要损失一定效率。 在了解了需优化字符串的提交流程,以及字符串提交后的优化流程后,我们接着看看 Eclipse 核心是如何将这两者整合到一起的。 前面提到 Workspace.open 方法会调 用 InternalPlatform.addStringPoolParticipant 方法,将一个字符串缓冲池分区的根节点,添加到全局性的优化任务队列中。 以下内容为程序代码: // // org.eclipse.core.internal.runtime.InternalPlatform // public final class InternalPlatform { private StringPoolJob stringPoolJob; public void addStringPoolParticipant(IStringPoolParticipant participant, ISchedulingRule rule) { if (stringPoolJob == null) stringPoolJob = new StringPoolJob(); // Singleton 模式 stringPoolJob.addStringPoolParticipant(participant, rule); } } // // org.eclipse.core.internal.runtime.StringPoolJob // public class StringPoolJob extends Job { private static final long INITIAL_DELAY = 10000;//five seconds private Map participants = Collections.synchronizedMap(new HashMap(10)); public void addStringPoolParticipant(IStringPoolParticipant participant, ISchedulingRule rule) { participants.put(participant, rule); if (sleep()) wakeUp(INITIAL_DELAY); } public void removeStringPoolParticipant(IStringPoolParticipant participant) { participants.remove(participant); } } 此任务将在合适的时候,为每个注册的分区进行共享优化。 StringPoolJob 类型是分区任务的代码所在,其底层实现是通过 Eclipse 的任务调度机制。关于 Eclipse 的任务调度,有兴趣的朋友可以参 考 Michael Valenta (IBM) 的 On the Job: The Eclipse Jobs API 一文。 这里需要了解的是 Job 在 Eclipse 里,被作为一个异步后台任务进行调度,在时间或资源就绪的情况下,通过调用其 Job.run 方法执行。可以说 Job 非常类似一个线程,只不过是基于条件进行调度,可通过后台线程池进行优化罢了。而这里任务被调度的条件,一方面是任务自身的调度时间因素,另一方面是通过 ISchedulingRule 接口提供的任务资源依赖关系。如果一个任务与当前正在运行的任务传统,则将被挂起直到冲突被缓解。而 ISchedulingRule 接口本身可以通过 composite 模式进行组合,描述复杂的任务依赖关系。 在具体完成任务的 StringPoolJob.run 方法中,将对所有字符串缓冲分区的调度条件进行合并,以便在条件允许的情况下,调用 StringPoolJob.shareStrings 方法完成实际工作。 以下内容为程序代码: // // org.eclipse.core.internal.runtime.StringPoolJob // public class StringPoolJob extends Job { private static final long RESCHEDULE_DELAY = 300000;//five minutes protected IStatus run(IProgressMonitor monitor) { //copy current participants to handle concurrent additions and removals to map Map.Entry[] entries = (Map.Entry[]) participants.entrySet().toArray(new Map.Entry[0]); ISchedulingRule[] rules = new ISchedulingRule[entries.length]; IStringPoolParticipant[] toRun = new IStringPoolParticipant[entries.length]; for (int i = 0; i < toRun.length; i++) { toRun[i] = (IStringPoolParticipant) entries[i].getKey(); rules[i] = (ISchedulingRule) entries[i].getValue(); } // 将所有字符串缓冲分区的调度条件进行合并 final ISchedulingRule rule = MultiRule.combine(rules); // 在调度条件允许的情况下调用 shareStrings 方法执行优化 try { Platform.getJobManager().beginRule(rule, monitor); // 阻塞直至调度条件允许 shareStrings(toRun, monitor); } finally { Platform.getJobManager().endRule(rule); } // 重新调度任务自己,以便进行下一次优化 long scheduleDelay = Math.max(RESCHEDULE_DELAY, lastDuration*100); schedule(scheduleDelay); return Status.OK_STATUS; } } StringPoolJob.shareStrings 方法只是简单的遍历所有分区,调用其根节点 的 IStringPoolParticipant.shareStrings 方法,进行前面所述的优化工作,并最终返回分区的优化效果。而缓冲池本身,只是作为一个优化工具,完成后直接被放弃。 以下内容为程序代码: private int shareStrings(IStringPoolParticipant[] toRun, IProgressMonitor monitor) { final StringPool pool = new StringPool(); for (int i = 0; i < toRun.length; i++) { if (monitor.isCanceled()) // 操作是否被取消 break; final IStringPoolParticipant current = toRun[i]; Platform.run(new ISafeRunnable() { // 安全执行 public void handleException(Throwable exception) { //exceptions are already logged, so nothing to do } public void run() { current.shareStrings(pool); // 进行字符串重用优化 } }); } return pool.getSavedStringCount(); // 返回优化效果 } } 通过上面的分析我们可以看到,Eclipse 实现的基于字符串缓冲分区的优化机制,相对于 JVM 的 String.intern() 来说: 1.控制的粒度更细,可以指定要对哪些对象进行优化; 2.优化效果可度量,可以大概估算出优化能节省的空间; 3.不存在性能瓶颈,不存在集中的字符串缓冲池,因此不会因为大量字符串导致性能波动; 4.不会长期占内存,缓冲池只在优化执行时存在,完成后中间结果被抛弃; 5.优化策略可选择,通过定义调度条件,可选择性执行不同的优化策略 ------------------------------------------------ CLR中字符串不变性的优化 自从有编程语言以来,如何处理字符串就一直是一个争论不休的问题。从C/C++用字符数组表示字符串,让用户完全控制其生命周期;到Delphi/VB通过编译器内建支持,使用引用计数自动维护字符串生命周期;再到Java/C#通过不可变字符串以及垃圾回收管理生命周期。不同的策略有着不同的倾向性,也有各自的缺点和优点。这儿我不想评论多种策略之间的优劣,只是想针对C#的实现做一点点较为深入的探讨。 CLR中选择了和Java类似的不可变字符串策略,以简化生命期维护以及多线程同步问题的处理,但同时也付出了一定的效率和空间上的代价,故而不得不通过编译器一级定制来优化。 Chris Brumme和Yun Jin在其BLog上讨论了需要保障字符串不变性(immutability)的原因,并指出通过PInvoke以及unsafe代码直接修改字符串内容可能带来的危害。 Interning Strings & immutability Dangerous PInvokes - string modification 为了提高效率和节约空间,CLR内部实际上维护了一个不可变字符串表。在堆中分配的字符串可以通过String.Intern函数确保其被加入此表;通过String.IsInterned函数判断自己是否在表中。如果在表中,则可以通过引用来直接对字符串进行比较,大大提高字符串比较效率。MSDN上的例子如下 以下为引用: // Sample for String.Intern(String) using System; using System.Text; class Sample { public static void Main() { String s1 = \"MyTest\"; String s2 = new StringBuilder().Append(\"My\").Append(\"Test\").ToString(); String s3 = String.Intern(s2); Console.WriteLine(\"s1 == '{0}'\ Console.WriteLine(\"s2 == '{0}'\ Console.WriteLine(\"s3 == '{0}'\ Console.WriteLine(\"Is s2 the same reference as s1?: {0}\ ct)s1); Console.WriteLine(\"Is s3 the same reference as s1?: {0}\ct)s1); } } /* This example produces the following results: s1 == 'MyTest' s2 == 'MyTest' s3 == 'MyTest' Is s2 the same reference as s1?: False Is s3 the same reference as s1?: True */ 如果熟悉CLR的Metadata文件结构的朋友可能立刻会想到,在Metadata表中实际上本来就有#String流和#US流,分别保存程序中固化的字符串和用户字符串。例如上面的\"MyTest\"字符串就会被放入流中直接载入,而CLR动态维护的字符串表就是在此基础上扩展的。 动态创建的字符串,如前面例子中通过StringBuilder构造的字符串,则缺省放在堆中,只有用户显式调用了String.Intern函数,才会被加入到静态字符串表中。查看Rotor的代码,会发现String.Intern实际上是调用当前线程所在AppDomain的GetOrInternString函数;而进一步调用此AppDomain的字符串映射表的GetInternedString函数。 以下为引用: String.Intern(String str) (bcl\\system\\string.cs:1194) Thread.GetDomain().GetOrInternString(str) AppDomain.GetOrInternString(String str) (bcl\\system\\appdomain.cs:1558) InternalCall BaseDomain::GetOrInternString(STRINGREF *pString) (vm\\appdomain.cpp:856) m_pStringLiteralMap->GetInternedString(pString, ...) AppDomainStringLiteralMap::GetInternedString(...) (vm\\stringliteralmap.cpp:196) 在GetOrInternString函数中:首先会根据字符串的内容计算出其HashCode;然后使用此HashCode在当前AppDomain的字符串映射表(m_StringToEntryHashTable)中搜索;如果没有找到则进一步在CLR的全局字符串映射表 (SystemDomain::GetGlobalStringLiteralMap())中搜索;如果还是没有找到,则根据参数决定是否将此字符串以HashCode为索引加入全局字符串映射表(GetInternedString函数中根据参数bAddIfNotFound判断是否添加);如果当前AppDomain可能被卸载,则还会将此字符串以HashCode为索引加入到当前AppDomain的局部字符串映射表中。伪代码如下: 以下为引用: STRINGREF *AppDomainStringLiteralMap::GetInternedString(STRINGREF *pString, BOOL bAddIfNotFound, BOOL bAppDomainWontUnload) { StringLiteralEntry *Data; DWORD dwHash = m_StringToEntryHashTable->GetHash(字符串数据); if (m_StringToEntryHashTable->GetValue(&StringData, &Data, dwHash)) { return Data->GetStringObject(); } else { StringLiteralEntry *pEntry = SystemDomain::GetGlobalStringLiteralMap()->GetInternedString(pString, dwHash, bAddIfNotFound); if(pEntry) { if (!bAppDomainWontUnload) { m_StringToEntryHashTable->InsertValue(&StringData, (LPVOID)pEntry, FALSE); } } else { return pEntry->GetStringObject(); } } } 另外一个函数String.IsInterned实际上调用路径完全一样,只是在GetInternedString没有在字符串映射表搜索到字符串时不自动加入(bAddIfNotFound = false)。 由此我们可以得出一些结论: 1.Intern String的作用域是整个CLR,虽然每个AppDomain有独立的优先缓存机制。这样既可以保障查询效率,又可以保障在不同级别(如CLR/AppDomain)载入的共享的Assembly中字符串的一致性。 2.Intern String中的内容直接决定其HashCode,进而决定其在字符串表中的存储和索引,直接内容修改可能导致未知问题。直接修改内容后再使用String.IsInterned,就会返回一个和以前完全不同的索引项。 3.Intern String可以通过其引用直接比较。因为在隐式(固化在Metadata的#String或#US流中)或显示(调用String.Intern)将字符串Intern的时候,内容相同的字符串都会被定位到字符串索引表的同一入口,返回相同的对象引用。 因篇幅问题不能全部显示,请点此查看更多更全内容