Java中一種字符串的內存管理方法
Java[1]語(yǔ)言為字符串操作提供了豐富的支持,它將字符串封裝在三個(gè)類(lèi)中并提供多種字符串操作接口。在Java應用程序中,由于對字符串的使用量比較高,從而使得其需要消耗較大的堆空間。例如在J2EE應用服務(wù)器運行過(guò)程中,約40%的活躍堆空間被用來(lái)保存字符串數據[2]。
通過(guò)對Java中字符串操作接口的分析可以發(fā)現,隨著(zhù)這些操作的運行會(huì )產(chǎn)生較多的無(wú)用字符串,它們不再被Java類(lèi)封裝并且也不被任何變量引用。這些無(wú)用字符串數據將一直停留在活躍堆中,直到Java虛擬機啟動(dòng)垃圾收集將其回收。而由于字符串數據具有單個(gè)對象占用空間較小但總體數量很大的特征,大量的無(wú)用字符串數據不僅會(huì )影響堆空間的利用率,并且對Java虛擬機垃圾收集的性能有較大影響。
當前對Java中字符串的內存管理優(yōu)化方案主要關(guān)注于字符串的使用效率上,如消除常量重復、延遲分配等技術(shù)[2],通過(guò)修改Java虛擬機對字符串分配回收的支持來(lái)提高堆中字符串的使用效率。然而這些方案無(wú)法處理堆中已經(jīng)成為無(wú)用字符串的數據,只能等待垃圾收集來(lái)處理。
近期編譯時(shí)的獨立對象回收策略[3]則專(zhuān)注于在編譯階段對應用程序做分析并插入回收指令以回收無(wú)用對象空間,但是該方案對Java庫函數只做保守分析從而無(wú)法回收這些無(wú)用字符串。為此,本文從對字符串操作接口的分析出發(fā),識別各類(lèi)操作對字符串的改變情況以利用獨立對象回收策略中的指令插樁技術(shù)來(lái)主動(dòng)回收無(wú)用字符串對象,以提高堆空間的利用率、減低垃圾回收的負擔、改善Java虛擬機的性能。
1 Java中字符串的支持與分析
1.1 Java中字符串的支持
Java語(yǔ)言將字符串的表示和操作都封裝在StringBuilder、StringBuffer和String三個(gè)類(lèi)中。其中前兩個(gè)類(lèi)指向的字符串是可變的,String類(lèi)指向的字符串是不變的。這三個(gè)類(lèi)的內部結構基本上一致,以StringBuilder為例,StringBuilder在Java中的結構如圖1所示。
從圖1可以看出,字符串數據由StringBuilder對象指向的value域保存,在內存空間上反映為兩個(gè)對象:StringBuilder對象通過(guò)value域指向字符串對象。由于該類(lèi)提供常用的可變字符串操作接口且相對另一個(gè)類(lèi)StringBuffer具有較高的執行效率,對字符串數據的操作在Java虛擬機中一般會(huì )將其轉換為StringBuilder對象再做處理。下面以一個(gè)語(yǔ)句示例來(lái)說(shuō)明這一點(diǎn):
String s=new String(‘aa’+’bb’+’cc’);
該語(yǔ)句的語(yǔ)義是將三個(gè)字符串連接在一起并生成一個(gè)String對象,在Java語(yǔ)言的源程序級別上不會(huì )出現StringBuilder對象,但是經(jīng)過(guò)編譯器優(yōu)化之后,這條語(yǔ)句實(shí)際被翻譯為下面的字節碼形式(為簡(jiǎn)化描述,本文以源語(yǔ)言來(lái)表示字節碼的操作):
StringBuilder t=new StringBuilder(‘aa’);
t.append(‘bb’);
t.append(‘cc’);
String s=t.toString();
即Java編譯器會(huì )首先創(chuàng )建一個(gè)StringBuilder對象,完成字符串的連接工作之后再將其轉變?yōu)镾tring對象。由于類(lèi)似于這種情況的字符串操作較多地出現在輸出方法和字符串創(chuàng )建方法中,所以可推斷出StringBuilder有著(zhù)較大的使用頻率,故將以其為代表分析其提供的接口對字符串的影響。
1.2 無(wú)用字符串的產(chǎn)生
在上節的示例中,StringBuilder類(lèi)提供的append()接口將會(huì )改變value域所指向的字符串,其做法是:新建長(cháng)度為連接后字符串長(cháng)度之和的字符數組,分段復制之后使其成為value域指向的新數組,而value域指向的原數組將被丟棄成為無(wú)用字符串。
圖2為示例語(yǔ)句中append()接口引起的value域指向字符串變化圖。
從圖2可以看出,在append()接口執行過(guò)后,對象的字符數組將指向新建的字符串’aabb’,原有字符串’aa’將不被任何變量指向而成為無(wú)用字符串。
由于StringBuilder類(lèi)的value域指向的字符串是可變的,在其提供的接口中存在大量類(lèi)似append()可能對value域做出改變的接口,如insert()、replace()等。而在Java虛擬機對這些接口的調用頻率較多,表1是基準測試程序Jolden[4]的4個(gè)子程序中字符串操作接口調用次數以及可能對value域做出改變的接口調用次數對比。
由表1可以看出,可能對value域做出改變的調用次數占字符串操作接口調用次數的22.6%~45.7%,占有不可忽視的比例。下面將深入分析這些可能改變value域的操作接口的具體實(shí)現。
1.3 字符串操作接口分析
可能對value域做出改變的操作接口有一個(gè)共同點(diǎn),即this對象不會(huì )發(fā)生變化,只是其value域指向一個(gè)新建的字符串。對字符串的操作接口做深入分析后可知,在append()等可能改變value域指向的操作接口的實(shí)現中,存在兩條改變分支:例如在接口append(s)中,如果s為null或者s的value域指向一個(gè)空字符串,則該接口不會(huì )改變this對象的value域指向;否則才會(huì )新建一個(gè)字符串以被this對象的value域指向。
可以將這兩條改變分支表現為下面的形式:
分支1:不做任何改變。
分支2:新建字符串,使其被this對象的value域指向,原有字符串成為無(wú)用字符串。
下面將給出根據本節的分析給出的無(wú)用字符串回收方案。
2 字符串的回收方案
對無(wú)用字符串的回收存在兩個(gè)難點(diǎn):(1)不可深入改變Java的庫函數實(shí)現。因為回收方案需要具有較強的通用性和靈活性;(2)由于操作接口具體實(shí)現中對value域的改變存在分支,并且只能在應用程序的運行階段判斷究竟執行的是哪個(gè)分支。
本文采用獨立對象顯式回收策略中的指令插樁技術(shù)來(lái)解決上述兩個(gè)難題:在可能發(fā)生改變的字符串操作接口調用點(diǎn)處插入判定語(yǔ)句來(lái)對操作接口執行的分支做判斷,然后根據結果來(lái)實(shí)施字符串的回收方案。由于這些語(yǔ)句都插樁在用戶(hù)程序中,不會(huì )改變Java庫函數的實(shí)現,而且這些語(yǔ)句會(huì )隨著(zhù)字符串操作接口的執行而執行,所收集的信息屬于運行時(shí)信息,故可以很好地判斷運行時(shí)分支的情況。
由于兩條分支的不同之處表現為操作接口執行完畢之后,this對象的value域指向是否發(fā)生了變化,故可以采取接口調用前后value域比較的方式來(lái)判斷具體執行的分支。本文使用指令插樁技術(shù),在Java虛擬機重編譯Java字節碼時(shí)對其做指令插樁工作,其處理流程為:
(1)在Java虛擬機處理應用程序指令時(shí)判斷其是否為可能引起字符串變化的操作接口調用指令。
(2)如果是則實(shí)施步驟(3)~(5)的指令插樁工作。
(3)在調用指令之前插入this對象的value域引用保存指令。
(4)在調用指令之后插入this對象的value域引用保存指令。
(5)安插兩個(gè)引用的對比指令,如果不同,則插入回收指令以回收調用之前保存的引用;否則將不做處理。
本方案用到了獨立對象回收技術(shù)中的回收指令,需要在Java虛擬機的內存管理模塊支持這個(gè)回收指令。由于對回收指令的支持對原有的分配和回收方案影響很小,故其實(shí)現較簡(jiǎn)單并且具有一定的通用性。
以圖3為例來(lái)說(shuō)明本文的字符串回收方案。由于該方案處理的為Java字節碼,為了方便理解,將以實(shí)際代碼的形式體現:由圖3可看出,在調用點(diǎn)前后加入了value域引用保存指令記錄了調用點(diǎn)執行前后的value域的引用信息,然后將兩者做對比處理來(lái)判斷調用點(diǎn)是否對value域的引用做出了改變,如果引用信息有了變化,則之前的value域引用成為了無(wú)用字符串,可以插入回收指令將其回收。
可以將該無(wú)用字符串回收方案應用到其他可能對對象內部value域指向的字符串做出改變的接口調用點(diǎn),即可以在運行時(shí)回收由這些接口運行而出現的無(wú)用字符串數據。
3 實(shí)驗結果及分析
在A(yíng)pache的開(kāi)源Java SE(Standard Edition)平臺Harmony[5]上實(shí)現了針對字符串的回收方案,并做了相關(guān)的實(shí)驗測試。測試平臺的操作系統是Windows 7 Ultimate,CPU為Intel Pentium Dual E2200,主頻為2.2 GHz,內存為2 GB。測試用例為Jolden中的4個(gè)基準程序。
在實(shí)現了無(wú)用字符串的顯式回收之后,可以在運行中主動(dòng)回收一些無(wú)用字符串以提高活躍堆空間的利用率和降低Java虛擬機垃圾回收的開(kāi)銷(xiāo)。表2給出了主動(dòng)回收的無(wú)用字符串大小和總分配大小的比較情況。
由表2可以看出,本文的回收方案主動(dòng)回收的無(wú)用字符串占應用程序總分配空間的5%~18%,對堆空間的利用率有較大的提升。對無(wú)用字符串的主動(dòng)回收也帶來(lái)了Java虛擬機的性能提升,因為可以減輕垃圾收集的負擔。表3給出了實(shí)現無(wú)用字符串回收方案前后測試程序在Java虛擬機中的執行時(shí)間對比。
由表3可以看出,經(jīng)過(guò)對無(wú)用字符串的主動(dòng)回收處理,Java虛擬機對應用程序的執行效率也有了改善,分別減少了1%~5%。這說(shuō)明無(wú)用字符串的回收可以提升Java虛擬機的性能。
本文提出一種對Java應用程序中無(wú)用字符串進(jìn)行顯式回收的方案,以指令插樁的形式收集應用程序運行時(shí)信息并主動(dòng)回收堆中無(wú)用字符串,以提高堆空間的利用率。實(shí)驗結果表明,該方案可以有效提高Java虛擬機的性能和Java應用程序的執行效率。
評論