控制/MCU
Java[1]语言为字符串操作提供了丰富的支持,它将字符串封装在三个类中并提供多种字符串操作接口。在Java应用程序中,由于对字符串的使用量比较高,从而使得其需要消耗较大的堆空间。例如在J2EE应用服务器运行过程中,约40%的活跃堆空间被用来保存字符串数据[2]。
通过对Java中字符串操作接口的分析可以发现,随着这些操作的运行会产生较多的无用字符串,它们不再被Java类封装并且也不被任何变量引用。这些无用字符串数据将一直停留在活跃堆中,直到Java虚拟机启动垃圾收集将其回收。而由于字符串数据具有单个对象占用空间较小但总体数量很大的特征,大量的无用字符串数据不仅会影响堆空间的利用率,并且对Java虚拟机垃圾收集的性能有较大影响。
当前对Java中字符串的内存管理优化方案主要关注于字符串的使用效率上,如消除常量重复、延迟分配等技术[2],通过修改Java虚拟机对字符串分配回收的支持来提高堆中字符串的使用效率。然而这些方案无法处理堆中已经成为无用字符串的数据,只能等待垃圾收集来处理。
近期编译时的独立对象回收策略[3]则专注于在编译阶段对应用程序做分析并插入回收指令以回收无用对象空间,但是该方案对Java库函数只做保守分析从而无法回收这些无用字符串。为此,本文从对字符串操作接口的分析出发,识别各类操作对字符串的改变情况以利用独立对象回收策略中的指令插桩技术来主动回收无用字符串对象,以提高堆空间的利用率、减低垃圾回收的负担、改善Java虚拟机的性能。
1 Java中字符串的支持与分析
1.1 Java中字符串的支持
Java语言将字符串的表示和操作都封装在StringBuilder、StringBuffer和String三个类中。其中前两个类指向的字符串是可变的,String类指向的字符串是不变的。这三个类的内部结构基本上一致,以StringBuilder为例,StringBuilder在Java中的结构如图1所示。
String s=new String(‘aa’+’bb’+’cc’);
该语句的语义是将三个字符串连接在一起并生成一个String对象,在Java语言的源程序级别上不会出现StringBuilder对象,但是经过编译器优化之后,这条语句实际被翻译为下面的字节码形式(为简化描述,本文以源语言来表示字节码的操作):
StringBuilder t=new StringBuilder(‘aa’);
t.append(‘bb’);
t.append(‘cc’);
String s=t.toString();
即Java编译器会首先创建一个StringBuilder对象,完成字符串的连接工作之后再将其转变为String对象。由于类似于这种情况的字符串操作较多地出现在输出方法和字符串创建方法中,所以可推断出StringBuilder有着较大的使用频率,故将以其为代表分析其提供的接口对字符串的影响。
1.2 无用字符串的产生
在上节的示例中,StringBuilder类提供的append()接口将会改变value域所指向的字符串,其做法是:新建长度为连接后字符串长度之和的字符数组,分段复制之后使其成为value域指向的新数组,而value域指向的原数组将被丢弃成为无用字符串。
图2为示例语句中append()接口引起的value域指向字符串变化图。
1.3 字符串操作接口分析
可能对value域做出改变的操作接口有一个共同点,即this对象不会发生变化,只是其value域指向一个新建的字符串。对字符串的操作接口做深入分析后可知,在append()等可能改变value域指向的操作接口的实现中,存在两条改变分支:例如在接口append(s)中,如果s为null或者s的value域指向一个空字符串,则该接口不会改变this对象的value域指向;否则才会新建一个字符串以被this对象的value域指向。
可以将这两条改变分支表现为下面的形式:
分支1:不做任何改变。
分支2:新建字符串,使其被this对象的value域指向,原有字符串成为无用字符串。
下面将给出根据本节的分析给出的无用字符串回收方案。
2 字符串的回收方案
对无用字符串的回收存在两个难点:(1)不可深入改变Java的库函数实现。因为回收方案需要具有较强的通用性和灵活性;(2)由于操作接口具体实现中对value域的改变存在分支,并且只能在应用程序的运行阶段判断究竟执行的是哪个分支。
本文采用独立对象显式回收策略中的指令插桩技术来解决上述两个难题:在可能发生改变的字符串操作接口调用点处插入判定语句来对操作接口执行的分支做判断,然后根据结果来实施字符串的回收方案。由于这些语句都插桩在用户程序中,不会改变Java库函数的实现,而且这些语句会随着字符串操作接口的执行而执行,所收集的信息属于运行时信息,故可以很好地判断运行时分支的情况。
由于两条分支的不同之处表现为操作接口执行完毕之后,this对象的value域指向是否发生了变化,故可以采取接口调用前后value域比较的方式来判断具体执行的分支。本文使用指令插桩技术,在Java虚拟机重编译Java字节码时对其做指令插桩工作,其处理流程为:
(1)在Java虚拟机处理应用程序指令时判断其是否为可能引起字符串变化的操作接口调用指令。
(2)如果是则实施步骤(3)~(5)的指令插桩工作。
(3)在调用指令之前插入this对象的value域引用保存指令。
(4)在调用指令之后插入this对象的value域引用保存指令。
(5)安插两个引用的对比指令,如果不同,则插入回收指令以回收调用之前保存的引用;否则将不做处理。
本方案用到了独立对象回收技术中的回收指令,需要在Java虚拟机的内存管理模块支持这个回收指令。由于对回收指令的支持对原有的分配和回收方案影响很小,故其实现较简单并且具有一定的通用性。
以图3为例来说明本文的字符串回收方案。由于该方案处理的为Java字节码,为了方便理解,将以实际代码的形式体现:由图3可看出,在调用点前后加入了value域引用保存指令记录了调用点执行前后的value域的引用信息,然后将两者做对比处理来判断调用点是否对value域的引用做出了改变,如果引用信息有了变化,则之前的value域引用成为了无用字符串,可以插入回收指令将其回收。
由表3可以看出,经过对无用字符串的主动回收处理,Java虚拟机对应用程序的执行效率也有了改善,分别减少了1%~5%。这说明无用字符串的回收可以提升Java虚拟机的性能。
全部0条评论
快来发表一下你的评论吧 !