×
嵌入式开发 > 详情

Java中一种字符串的内存管理方法

发布时间:2020-07-06 发布时间:
|

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所示。

从图1可以看出,字符串数据由StringBuilder对象指向的value域保存,在内存空间上反映为两个对象:StringBuilder对象通过value域指向字符串对象。由于该类提供常用的可变字符串操作接口且相对另一个类StringBuffer具有较高的执行效率,对字符串数据的操作在Java虚拟机中一般会将其转换为StringBuilder对象再做处理。下面以一个语句示例来说明这一点:

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域指向字符串变化图。

从图2可以看出,在append()接口执行过后,对象的字符数组将指向新建的字符串’aabb’,原有字符串’aa’将不被任何变量指向而成为无用字符串。

由于StringBuilder类的value域指向的字符串是可变的,在其提供的接口中存在大量类似append()可能对value域做出改变的接口,如insert()、replace()等。而在Java虚拟机对这些接口的调用频率较多,表1是基准测试程序Jolden[4]的4个子程序中字符串操作接口调用次数以及可能对value域做出改变的接口调用次数对比。

由表1可以看出,可能对value域做出改变的调用次数占字符串操作接口调用次数的22.6%~45.7%,占有不可忽视的比例。下面将深入分析这些可能改变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域引用成为了无用字符串,可以插入回收指令将其回收。

可以将该无用字符串回收方案应用到其他可能对对象内部value域指向的字符串做出改变的接口调用点,即可以在运行时回收由这些接口运行而出现的无用字符串数据。

3实验结果及分析

在Apache的开源Java SE(Standard Edition)平台Harmony[5]上实现了针对字符串的回收方案,并做了相关的实验测试。测试平台的操作系统是Windows 7 Ultimate,CPU为Intel Pentium Dual E2200,主频为2.2 GHz,内存为2 GB。测试用例为Jolden中的4个基准程序。

在实现了无用字符串的显式回收之后,可以在运行中主动回收一些无用字符串以提高活跃堆空间的利用率和降低Java虚拟机垃圾回收的开销。表2给出了主动回收的无用字符串大小和总分配大小的比较情况。

由表2可以看出,本文的回收方案主动回收的无用字符串占应用程序总分配空间的5%~18%,对堆空间的利用率有较大的提升。对无用字符串的主动回收也带来了Java虚拟机的性能提升,因为可以减轻垃圾收集的负担。表3给出了实现无用字符串回收方案前后测试程序在Java虚拟机中的执行时间对比。

由表3可以看出,经过对无用字符串的主动回收处理,Java虚拟机对应用程序的执行效率也有了改善,分别减少了1%~5%。这说明无用字符串的回收可以提升Java虚拟机的性能。

本文提出一种对Java应用程序中无用字符串进行显式回收的方案,以指令插桩的形式收集应用程序运行时信息并主动回收堆中无用字符串,以提高堆空间的利用率。实验结果表明,该方案可以有效提高Java虚拟机的性能和Java应用程序的执行效率。



『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
Recogni:将高端AI芯片推向自动驾驶边缘