03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据
03 | 字符串性能优化不容小觑,百M内存轻松存储几十G数据
讲述:李良
时长15:08大小13.84M
String 对象是如何实现的?
String 对象的不可变性
String 对象的优化
1. 如何构建超大字符串?
2. 如何使用 String.intern 节省内存?
3. 如何使用字符串的分割方法?
总结
思考题
互动时刻
赞 43
提建议
精选留言(130)
- KL32019-05-25老师,能解释下, “String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。” 共享char数组可能导致内存泄露问题?
作者回复: 你好 KL3,在Java6中substring方法会调用new string构造函数,此时会复用原来的char数组,而如果我们仅仅是用substring获取一小段字符,而原本string字符串非常大的情况下,substring的对象如果一直被引用,由于substring的里面的char数组仍然指向原字符串,此时string字符串也无法回收,从而导致内存泄露。 试想下,如果有大量这种通过substring获取超大字符串中一小段字符串的操作,会因为内存泄露而导致内存溢出。
共 2 条评论131 - 扫地僧2019-05-25答案是false,false,true。背后的原理是: 1、String str1 = "abc";通过字面量的方式创建,abc存储于字符串常量池中; 2、String str2 = new String("abc");通过new对象的方式创建字符串对象,引用地址存放在堆内存中,abc则存放在字符串常量池中;所以str1 == str2?显然是false 3、String str3 = str2.intern();由于str2调用了intern()方法,会返回常量池中的数据,地址直接指向常量池,所以str1 == str3;而str2和str3地址值不等所以也是false(str2指向堆空间,str3直接指向字符串常量池)。不知道这样理解有木有问题展开
作者回复: 答案非常正确,理解了这个题目基本理解了string的特性了。
共 11 条评论96 - 快乐的五五开2019-05-25自学一年居然不知道有String.intern这个方法😓😓 不过从Java8开始(大概) String.split() 传入长度为1字符串的时候并不会使用正则,这种情况还是可以用
作者回复: 非常感谢Geek的补充,我在这里也再补充一个小点,split有两种情况不会使用正则表达式: 第一种为传入的参数长度为1,且不包含“.$|()[{^?*+\\”regex元字符的情况下,不会使用正则表达式; 第二种为传入的参数长度为2,第一个字符是反斜杠,并且第二个字符不是ASCII数字或ASCII字母的情况下,不会使用正则表达式。
共 3 条评论71 - 风轻扬2019-08-01老师好,诚心请教一个问题 string s1 = new string(“1”)+new string(“1”); s1.intern; string s2=“11”; s1==s2为什么是true呢,我理解s1指向的对象,s2指向的常量池地址才对啊? 然后 string s1 = new string(“1”); s1.intern; string s2=“11”; s1==s2又是false了,区别在哪? 老师,周董提的这个问题,我都琢磨一晚上了。您的回答看了好多遍,确实是看不懂,您能再解释一下吗?目前的回答,咋看也看不懂。。。。。。展开
作者回复: 如果看不太懂,建议先熟悉下JVM这块的知识点。我们知道,JVM从逻辑分区可以分为堆、JVM栈、本地方法栈、方法区、程序计数器,方法区中,在JDK1.8之后,包含了元空间、静态常量池、运行时常量池。 对于字符串常量,在类加载时,会将字符串放入方法区中的静态常量池,包括字符串的字面量和字符引用。而在初始化或运行时,会将字符引用转为直接引用,存放在运行时常量池。 如果是运行时动态生成的字符串对象调用intern方法,如果字符串的引用在运行时常量池不存在,则会在常量池中创建一个引用。 所以第一个通过加动态生成的“11”字符串由于在运行时常量中没有该字符串的引用,所以会在调用s1.intern时,在运行时常量池中生成一个s1的引用,当s2再次引用该字符串时,发现运行时常量池中存在相同值的字符串的引用,就直接返回s1的引用。所以s1==s2是返回的true。这也仅限于JDK1.7之后的版本。 而第二种,用于"11"在类加载时,已经存在静态常量池中,在new string(“11”)时,会在运行时常量池中创建一个“11”字符串的直接引用。而s1指向的并不是该引用,而是new string这个对象的引用。当s2=“11”时,返回的是运行时常量池中的引用。所以s1==s2返回false。
共 16 条评论38 - 周董2019-08-01老师,还有一个问题网上众说纷纭,jdk1.8版本,字符串常量池和运行时常量池分别在内存哪个区?您文中的常量池是什么常量池?调用intern后字符串是在哪个常量池生成引用或者对象?麻烦老师抽空解答下,这个困扰很久了。
作者回复: 严格来说,是静态常量池和运行时常量池,静态常量池是存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。 运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。 这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。 我文中说的是两个常量池,没有具体区分,在初次加载时,是字面量是加载到了静态常量池中,解析之后会将引用加载到运行时常量池。 intern方法生成的引用或对象是在运行时常量池中。
共 2 条评论29 - Teanmy2019-06-02老师好,有一点始终想不明白,请老师解惑,非常感谢! 老师先帮忙看看关于这两行代码,我的分析是否正确: str1 = "abc"; str2 = new String("abc") str1 = "abc"; 1.str1,首先是在字符串常量池中寻找"abc",找到则取其地址,找不到则创建并返回其地址 2.将该地址赋值给栈中的str1 str2 = new String("abc") 1.在堆中创建String对象,我查阅了String构造方法源码,实际值取的是"abc"的(此时"abc"已经存在字符串常量池中)引用,也就是说,str2还是指向常量池,并没有创建新的"abc"。 public String(String original) { this.value = original.value; this.hash = original.hash; } 2.堆中创建完String对象,将该对象的地址赋值给栈变量str2 疑问: 既然不管是以上哪种方式,最终实际引用的还是常量池中的"abc",str2 = new String("abc")只是增加了一个堆中String的“空壳”对象而已(因为实际上char[]指向的还是常量池中的"abc"),这个空壳对象并不会占用过多内存。而.intern的实质只是减少了这个中间的String空壳对象,那何来twitter通过.intern减少大量内存?展开
作者回复: 你好 teanmy。运行时创建的字符串对象只会在堆中创建一个对象。在这个前提下,如果有相同值的对象创建,使用intern可以减少重复字符串的创建。例如,有广东省/深圳市/南山区,如果有千万个人发布消息,创建了地址对象,这样导致千万个“广东省”对象在堆内存中创建,如果长时间引用,这些对象都没法释放,使用intern将“广东省”放到常量池中,其他对象引用常量池中的同一个“广东省”字符串,而堆中的千万个对象将被回收。 如果有疑问,请继续留言。
共 4 条评论22 - 失火的夏天2019-05-25开头题目答案是false false true str1是建立在常量池中的“abc”,str2是new出来,在堆内存里的,所以str1!=str2, str3是通过str2..intern()出来的,str1在常量池中已经建立了"abc",这个时候str3是从常量池里取出来的,和str1指向的是同一个对象,自然也就有了st1==str3,str3!=str2了
作者回复: 这里我纠正下,str3是intern返回的引用,intern而不是创建出来的。 你的答案是正确的!
21 - Only now2019-05-29看了本篇几乎全部留言, 感觉包括老师在内, 对于 "字符串常量池" 和 "常量池", 这俩概念用的很混。 对于jdk7 以及之前的jvm版本不再去深究了, 它的字符串常量池存在于方法区, 但是jdk8以后, 它存在于Java堆中, 唯一, 且由java.lang.String类维护, 它和类文件常量池, 运行时常量池没有半毛钱的关系。 最后我有个疑问问老师, 字符串常量池中的对象, 在失去了所有外部引用之后, 会被gc掉吗?展开
作者回复: 非常感谢only now的总结,这一讲中没有详细去区分常量池,而是在强调字符串的使用,后面我们在JVM中可以再一起研究下常量池。 JVM文献中提到方法区是存在垃圾回收。我们可以通过intern方法来验证这个gc问题,通过大量请求请求某个接口,传入参数创建字符串对象,之后通过intern方法在常量池中生成字符串对象,之后失去引用,观察gc情况。
16 - Zend2019-05-26“在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。” 比如: 是从常量池中复制到堆内存,这时常量池中字符串与堆内存字符串是完全独立的,内部也不存在引用关系?展开
作者回复: 你好 Zend,具体的复制过程是先将常量池中的字符串压入栈中,在使用string的构造方法时,会拿到栈中的字符串作为构造方法的参数。这里我纠正一点,今天我查看了下这个构造函数,String的构造函数是一个char数组赋值过程,不是new char[]重新创建,所以是引用了常量池中的字符串对象,存在引用关系。
14 - 六维2019-05-25使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担。 像国家地区是有边界的。像其他情况,怎么把握这个度呢?
作者回复: 如果对空间要求高于时间要求,且存在大量重复字符串时,可以考虑使用常量池存储。 如果对查询速度要求很高,且存储字符串数量很大,重复率很低的情况下,不建议存储在常量池中。 具体可以通过模拟测试自己的场景,对比两种存储方式的性能,通过数据来给自己答案。
共 3 条评论13 - Eric2019-05-25对于您文中 “在一开始创建 a 变量时,会在堆内存中创建一个对象,同时在常量池中创建一个字符串对象” 这句话 我认为前部分没有问题 分歧点在后面那部分 我觉得abc常量早就在运行时常量池就存在了 可以理解使用这个类之前 就已经构造好了运行时常量池 而运行时常量池中就包括“abc”常量 至于使用new String(“abc”) 我觉得它应该只会在堆中创建String对象 并将运行时常量池中已经存在的“abc”常量的引用作为构造函数的参数而已
作者回复: 你理解的分歧点是对的,这个构造是在加载类时,就已经在常量池中构造好常量。
共 2 条评论11 - 周董2019-07-26老师好,诚心请教一个问题 string s1 = new string(“1”)+new string(“1”); s1.intern; string s2=“11”; s1==s2为什么是true呢,我理解s1指向的对象,s2指向的常量池地址才对啊? 然后 string s1 = new string(“1”); s1.intern; string s2=“11”; s1==s2又是false了,区别在哪?展开
作者回复: String s1 = new String("1") + new String("1")会在堆中组合一个新的字符串对象"11",在s1.intern()之后,由于常量池中没有该字符串的引用(只有字符串常量"11"),所以常量池中生成一个堆中字符串"11"的引用,此时String s2= "11"返回的是堆字符串"11"的引用,所以s1==s2。 在JDK1.7版本以及之后的版本运行以下代码,你会发现结果为true,在JDK1.6版本运行的结果却为false: String s1 = new String("1") + new String("1"); System.out.println( s1.intern()==s1); 而String s1 = new String("11")首先会在常量池中创建字符串"11"的引用,而s1则是返回的堆中的new String("11")对象的引用,此时s1.intern()返回的是常量池字符串常量"11"的引用,而非堆中的。而String s2="11"又是返回的常量池中常量"11"的引用。所以s1==s2为false。 总结:常量池中同时存在字符串常量和字符串引用,在JDK1.7版本之后的intern()方法只会尝试对象的引用放入常量池,而在之前的版本中,intern()方法会复制字符串常量到常量池中,并返回字符串引用。
共 4 条评论7 - -W.LI-2019-05-26老师好!第一个问题没有描述清楚。String a = ”abc”, String b =new String("abc"),String c=new String(new char[]{‘a’,‘b’,‘c’})。创建的String对象。我debug时发现这三个String对象的value指向的那个char数组地址值都是一样的。他们是复用了一个char数组么?还是工具显示问题?我用的idea。
作者回复: 你好 W.LI,刚我debug了下,a和b的value是同一个地址,因为a在常量池中创建了"abc",而new String("abc")时,发现常量池存在"abc"字符串对象,不会创建了。这时通过构造函数String(String original)将常量池中的"abc"复制给value,这里的复制是引用,不是创建新的char[]数组,所以是同一个value地址。 而c中的构造函数,是新开辟了一个char[]数组: public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } 所以value的地址不一样。 可以再试试,有问题留言。
7 - Eric2019-05-25String s1 = new String("abc").intern() Code: 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String abc 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: invokevirtual #5 // Method java/lang/String.intern:()Ljava/lang/String; 12: astore_1 13: return 9:invokevirtual的时候 常量池里面应该早就有了”abc“这个字符串常量了吧 为什么文中说的是先去堆中创建一个String对象 然后再去常量池创建一个字符串常量? 我理解错误了吗?展开
作者回复: 我们可以看到0 new,即是生成了一个对象,这个对象是在堆内存用创建的,之后4 Idc则是将常量池中创建的字符串abc压入栈中,invokespecial调用构造方法复制abc字符串到对象中,invokevirtual调用intern本地方法,返回常量池中的对象引用给s1。 new String("abc")是会创建两个对象的,一个是堆对象,一个是常量池中的对象,intern会去判断常量池中是否有,这个时候是有的,所以不会创建,而是改变s1的引用。 不知道这样是否更好理解?
7 - 建国2019-05-25在实际编码中我们应该使用什么方式创建字符传呢? A.String str= "abcdef"; B.String str= new String("abcdef"); C.String str= new String("abcdef"). intern(); D.String str1=str.intern();
作者回复: 实际编码中,我们要结合实际场景来选择创建字符串的方式,例如,在创建局部变量以及常量时,我们一般使用A的这种方式;如果我们要区别一个字符串创建两个不同的对象来使用时,会选择B;intern一般使用的比较少,例如我们平时会创建很多一样的字符串的对象时,且对象会保存在内存中,我们可以考虑使用intern方法来减少过多重复对象占用内存空间。
共 3 条评论7 - benben2019-06-26请教最后一张图第三列的意思是对象成员变量是string的话不会放到常量池是吗?
作者回复: 是的,运行时动态创建是在堆内存中直接创建的,调用intern方法,会反倒常量池中。
4 - 水月2019-07-09老师请教一个问题,通过抽离出单独类SharedLocation,存储减到了20G,麻烦解析下原理?
作者回复: 共享一个类,减少在不同的类中重复创建location的信息。
3 - Hammy2019-06-21老师您好,我这里有一个疑问。在听您说完,对象的string属性实质上在运行中是在堆内存中创建而不是引用常量池的时候如雷贯耳一般,觉得自己之前根本没思考过这个问题,完全没想过用intern进行优化。但是我做了一个实验,public class Person { public String name; public void setName(String name) { this.name = name; } public String getName() { return name; } public static void main(String[] args) { Person person1 = new Person(); person1.setName("张三"); Person person2 = new Person(); person2.setName("张三"); System.out.println(person1.name==person2.name); } 这段代码中,我理解如果string是在运行过程中在堆内存生成对象,那么结果应该是false,但是返回的结果是true。这是我的一个疑惑,劳烦老师帮忙看一下我的测试代码哪里不对,还是有理解错误的地方。展开
作者回复: "张三"是常量,而不是一个对象,所以会有问题。我们可以使用外部传值的方式试试。
共 4 条评论3 - 西门吹水之城2019-06-04老师您好,看下面的留言,您看我这这么理解对吗? String b=new String(“abc”); for(int i=0;i<10;i++){ String c=new String(i+“”); } 上面的代码中,b和c是不同的,b在编译的时候会将abc放入常量池中,b引用的堆内存,堆内存引用常量池。c在编译时候没有字符串,在运行的时候,会直接存入内存中,不会将字符串放入常量池。这样解释可以吗?展开
作者回复: b是在类加载时,放入到常量池中。其他地方理解没问题。
3 - ° BugMaker2019-05-31刘老师您好!"使用 intern 方法需要注意的一点是,一定要结合实际场景。因为常量池的实现是类似于一个 HashTable 的实现方式,HashTable 存储的数据越大,遍历的时间复杂度就会增加。如果数据过大,会增加整个字符串常量池的负担",那这个Twitter 工程师在 QCon 全球软件开发大会上的演讲的那个 intern 方法是如何做到遍历这么多常量池的数据,同时保证性能的呢?
作者回复: 你好,如果我们的数据对查询速度没有这么高要求,可以考虑使用。
3