当前位置: 首页 > 后端技术 > Java

既然String可以做性能调优,我就叫专家

时间:2023-04-02 00:41:02 Java

Code,String还能优化什么?你在陷害我吗?别慌,今天给大家看一个不一样的String,从根到G点。码哥分享了一个例子:通过性能调优,我们可以轻松地在几百兆内存中存储几十G的数据。String对象是我们每天“接触”的对象类型,但我们总是忽略她的性能问题。如果你爱她,你不能只是和她一起玩。你需要深入了解String的内心,做一个“心怀猛虎,细细闻玫瑰花”的暖男。通过以下几点分析,一步步揭开她的衣裳,直达内心深处,提升一个层次,让String直接起飞:String对象的特性;String的不变性;大弦的构造技巧;字符串的保存。记忆;字符串分割技术;Stringbodydecryption如果想深入理解,先从基本组成说起……《StringCreator》对String对象做了很多优化来节省内存,从而提高了String的性能:Java6和以前的数据存储在char[]数组中,String通过offset和count这两个属性定位到char[]数据,得到字符串。这样可以高效快速地定位和共享数组对象,节省内存,但可能会造成内存泄漏。为什么共享字符数组会导致内存泄漏?String(intoffset,intcount,charvalue[]){this.value=value;this.offset=偏移量;this.count=count;}publicStringsubstring(intbeginIndex,intendIndex){//检查边界returnnewString(offset+beginIndex,endIndex-beginIndex,value);}虽然调用substring()时创建了一个新字符串,string的值还是指向内存中同一个数组,如下图:如果我们只用substring获取一小段字符,而原字符串很大,如果substring的对象总是参考。此时String字符串无法回收,导致内存泄漏。如果有大量这样的通过子串获取一个大字符串的一小部分的操作,就会因为内存泄漏而导致内存溢出。JDK7和8去掉了offset和count变量,减少了String对象占用的内存。子字符串源码:publicString(charvalue[],intoffset,intcount){this.value=Arrays.copyOfRange(value,offset,offset+count);}publicStringsubstring(intbeginIndex,intendIndex){intsubLen=endIndex-beginIndex;returnnewString(value,beginIndex,subLen);}substring()通过newString()返回一个新的string对象,创建新对象字符数组时通过Arrays.copyOfRange()深拷贝一个新的。如下图所示:String.substring方法不再共享char[]数组的数据,解决了可能的内存泄露问题。Java9将char[]字段更改为byte[],并添加了一个编码器属性。马哥,你怎么改成这样了?一个char字符占用2个字节,16位。以单字节编码(占用一个字节的字符)存储字符是非常浪费的。为了节省内存空间,使用每字节8位的字节数组来存储字符串。节俭女神,谁不爱。。。新属性编码器的作用是:在计算字符串长度或者使用indexOf()方法的时候,我们需要根据编码来计算字符串的长度类型。coder的值代表不同的编码类型:0:使用Latin-1(单字节编码);1:使用UTF-16。String的不变性了解了String的基本构成后,我发现String还有一个比它的外表更性感的特性。她是用final关键字修饰的,char数组也是。我们知道,一个被final修饰的类,意味着该类不能被继承,而char[]被final+private修饰,意味着String对象不能被改变。String对象一旦创建,就无法更改。final修改的好处Security当你调用其他方法的时候,比如调用一些系统级的操作指令,可能会有一系列的检查。如果它是一个变量类,它的内部值可能会在你验证之后被改变,这可能会导致严重的系统崩溃。高性能缓存String不可变后,可以保证hash值的唯一性,这样类似HashMap的容器就可以实现相应的key-value缓存功能。实现字符串常量池由于其不变性,可以实现字符串常量池。字符串常量池是指在创建字符串时,先到“常量池”中查找“字符串”是否已经创建;如果是,则不会开辟新的空间来创建字符串,而是直接把字符串的引用返回给这个对象。创建字符串的两种方法:Stringstr1="codebyte";Stringstr2=newString("代码字节");当代码使用第一种方法创建字符串对象时,JVM会先检查该对象是否在字符串常量池中,如果在,则返回对象引用。否则将在常量池中创建一个新字符串并返回该引用。这样可以减少重复创建具有相同值的字符串对象,节省内存。创建第二个方法。在编译类文件时,会将“代码字节”字符串放入常量结构中。当加载类时,会在常量池中创建“代码字节”;当调用new时,JVM命令会调用String的构造函数在堆内存中创建一个String对象。同时object指向“常量池”中的“codebyte”字符串,str指向刚刚在堆上创建的String。目的;如下图:什么是对象和对象引用?str属于方法栈的字面值,它指向堆中的String对象,而不是对象本身。object是内存中的内存地址,str是指向这个内存地址的引用。也就是说str不是一个对象,而只是一个对象引用。Code兄,字符串的不可变性到底是什么意思?Stringstr="Java";str="Java,yyds"第一次赋值为"Java",第二次赋值为"Java,yyds"。str的值确实发生了变化。为什么我还是说String对象是不可变的呢?这是因为str只是对String对象的引用,而不是对象本身。真正的对象还在内存中,没有变化。实践中的优化了解了String对象的实现原理和特点之后,就该深入女神的内心,结合实际场景,如何将String对象的使用优化到更高的层次。如何构建一个大字符串既然String对象是不可变的,那么当我们频繁拼接字符串的时候是不是意味着要创建多个对象呢?Stringstr="蛤蟆逗青蛙"+"长丑"+"玩花";你觉得Mr.变成“蟾蜍逗青蛙”的对象,然后生成“蟾蜍逗青蛙丑”对象,最后生成“蟾蜍逗青蛙”青蛙长丑玩花“对象。实际操作中,只有一个对象生成了,这是为什么呢,虽然代码很丑,但是编译器自动优化了代码,再看下面这个例子:Stringstr="littlefrog";for(inti=0;i<1000;i++){str+=i;}上面的代码编译后,可以看到编译器也对这段代码进行了优化,Java在拼接字符串的时候更倾向于使用StringBuilder,这样可以提高程序的效率。for(inti=0;i<1000;i++){str=(newStringBuilder(String.valueOf(str))).append(i).toString();}即便如此,还是在循环内重复创建StringBuilder对象敲黑板所以在做字符串拼接的时候,建议还是显式使用StringBuilder来提高系统性能,如果String对象的拼接涉及多线程编程中的线程安全,可以使用StringBuffer。使用intern节省内存直接看intern()方法的定义和源码:intern()是一个局部方法,它的定义是说当调用intern方法时,如果字符串常量中已经包含了字符串pool,then直接返回这个字符串的引用。否则,将此字符串添加到常量池并返回对该字符串的引用。如果不包含该字符串,则在返回对该对象的引用之前将该字符串添加到常量池中。什么时候适合使用intern()方法?一位Twitter工程师曾经分享过一个使用String.intern()的例子。Twitter每次发布消息状态时,都会生成一个地址信息。根据当时Twitter用户的预估规模,服务器需要20G的内存来存储地址信息。公共类位置{私人字符串城市;私有字符串区域;私有字符串国家代码;私人双经度;privatedoublelatitude;}考虑到很多用户的地址信息有重叠,比如国家,省份,城市等,这部分信息可以单独列在一个类中,减少重复。代码如下:publicclassSharedLocation{privateStringcity;私有字符串区域;privateStringcountryCode;}publicclassLocation{privateSharedLocationsharedLocation;双经度;doublelatitude;}通过优化,数据存储大小减少到20G左右。但是对于内存中存储的数据来说,还是很大的,怎么办?Twitter工程师使用String.intern()将高度重复的地址信息的存储大小从20G减少到数百兆字节,从而优化了String对象的存储。核心代码如下:SharedLocationsharedLocation=newSharedLocation();sharedLocation.setCity(messageInfo.getCity().intern());sharedLocation.setCountryCode(messageInfo.getRegion().intern());sharedLocation.setRegion(messageInfo.getCountryCode().intern());打个简单的例子方便理解:Stringa=newString("abc").intern();Stringb=newString("abc").intern();System.out.print(a==b);输出结果:真。当加载一个类时,会在常量池中创建一个字符串对象,内容为“abc”。在创建局部变量时,调用newSting()会在堆内存中创建一个String对象,String对象中的char数组会引用常量池中的字符串。调用intern方法后,会去常量池中查找是否有等于string对象的引用,有则返回引用。在创建b变量时,调用newSting()会在堆内存中创建一个String对象,String对象中的char数组会引用常量池中的字符串。调用intern方法后,会去常量池中查找是否有等于string对象的引用,如果有则返回局部变量的引用。刚刚在堆内存中的两个对象将被垃圾回收,因为没有指向它们的引用。所以a和b指的是同一个对象。字符串拆分有妙招。Split()方法使用正则表达式来实现其强大的拆分功能,但是正则表达式的性能非常不稳定。使用不当会导致回溯问题,可能导致高CPU。Java正则表达式使用的引擎实现是NFA(NondeterministicFiniteAutomaton,确定性有限自动机)自动机。这个正则表达式引擎在进行字符匹配的时候会回溯,一旦回溯,那么它消耗的时间就会变得很长,可能是几分钟,也可能是几个小时,时间的长短取决于数量和回溯的复杂性。所以我们应该谨慎使用Split()方法。我们可以使用String.indexOf()方法代替Split()方法来拆分字符串。总结与思考我们从String的演变就掌握了它的组成,不断的改变成员变量来节省内存。她的不变性从而实现了字符串常量池,减少重复创建同一个字符串,节省内存。但是也因为这个特性,我们在做长字符串拼接的时候,需要显式的使用StringBuilder来提高字符串拼接的性能。最后,在优化方面,我们还可以使用intern方法,让可变字符串对象可以复用常量池中相同值的对象,从而节省内存。最后,我想问你一个问题。欢迎大家在评论区留言。喜欢的话,就得到马哥赠送的书。通过三种不同的方式创建三个对象,然后成对匹配。每个组中匹配的两个对象是否相等?代码如下:Stringstr1="abc";Stringstr2=newString("abc");Stringstr3=str2.intern();assertSame(str1==str2);assertSame(str2==str3);assertSame(str1==str3)公众号后台回复:“String”获取答案。