在 Java/C# 這樣基於引用語義處理字符串的語言中,作為不可變對象存在的字符串,如果內容相同,則可以通過某種機制實現重用。因為對這類語言來說,指向內存中兩塊內存位置不同內容相同的字符串,與同時指向一個字符串並沒有任何區別。特別是對大量使用字符串的 XML 文件解析類似場合,這樣的優化能夠很大程度上降低程序的內存占用,如 SAX 解析引擎標准中就專門定義了一個 http://xml.org/sax/features/string-interning 特性用於字符串重用。
在語言層面,Java/C# 中都直接提供了 String.Intern 的支持。而對 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");
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")
for (stringTableBucket* bucket = firstBucket(); bucket <= lastBucket(); bucket++) {
for (stringTableEntry** p = bucket->entry_addr(); *p != NULL;) {
stringTableEntry* entry = *p;
assert(entry->literal_string() != NULL, "just checking");
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;)
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.優化策略可選擇,通過定義調度條件,可選擇性執行不同的優化策略