JVM深入学习(十二)-从jvm角度看String

简介: 从jvm角度看String,分析String的特性,优化,垃圾回收

1. String的特性

  1. 定义方式
  1. String str = "hello";
  2. String str = "hello";
  1. String声明为final,不可被继承
  2. String实现了Serializable接口和Comparable接口
  3. String的底层存储结构在jdk1.8之后从char[]变为了byte[]
  4. String不可变,String一旦定义,不可修改,修改=重新定义
  5. String常量池String pool中不会出现相同的内容,String pool本质上是一个hashtable,jdk6中长度未1009,jdk7以后为60013(解决hash冲突),该长度可以通过-XX:StringTableSize指定, jdk8之后此值最小1009

2. 内存分配

使用字面量的方式创建的String对象直接存放在常量池中

String hello = "hello";存放在常量池中

jdk6: 常量池位于永久代中

jdk7: 常量池挪到了堆

jdk8: 堆

为什么挪到了堆?

  1. 永久代空间较小
  2. 垃圾回收不易,fullGC频率极低

3. 从内存角度看字符串拼接

String hello = "hel"+"lo";

  1. 常量与常量的结果放在常量池,原理是编译期优化

这是常量+常量

  1. 如果其中一个是变量,则结果就在堆中(此堆区别于常量池所在堆),原理是StringBuilder拼接
  2. 如果变量+常量的拼接结果 调用了intern()方法,如果此结果在常量池中不存在,就会将该结果放入常量池中

常见问题:

Strings1="hello";
Strings2="hello";
System.out.println(s1==s2);// true 内存地址均为常量池中的helloStrings3=s1+"world";
Strings4=s2+"world";
System.out.println(s3==s4); // false 每个对象的内存地址都是不一样的Strings5= (s1+"world").intern(); 
System.out.println(s4==s5); // true 将结果放入到了常量池,所以是true



3.1 字符串变量拼接

从字节码看字符串拼接的本质

/*** @Author: Zy* @Date: 2021/12/7 18:50* 测试字符串拼接底层内存结构*/publicclassStringConcatTest {
publicstaticvoidmain(String[] args) {
Strings1="hello";
Strings2="world";
Strings3=s1+s2;
    }
}


可以看到两个变量拼接本质上就是StringBuilder的append;

3.2 字符串变量拼接测试

既然说拼接底层使用的是StringBuilder,为什么还说他比较慢

代码测试如下:

packagecom.zy.study12;
/*** @Author: Zy* @Date: 2021/12/7 21:39* 测试字符串拼接和使用StringBuilder.append方法的时间*/publicclassStringBuilderTest {
/*** 使用拼接的方式拼接100000个字符串* @author Zy* @date 2021/12/7* @param* @return void*/publicstaticvoidstringConcat(){
longstart=System.currentTimeMillis();
Stringresult="";
for (inti=0; i<100000; i++) {
result+="test";
        }
longend=System.currentTimeMillis();
System.out.println(end-start);
    }
/*** 创建一个StringBuilder对象,进行append* @author Zy* @date 2021/12/7* @param* @return void*/publicstaticvoidstringBuilderAppend(){
longstart=System.currentTimeMillis();
StringBuilderbuilder=newStringBuilder();
for (inti=0; i<100000; i++) {
builder.append("test");
        }
longend=System.currentTimeMillis();
System.out.println(end-start);
    }
publicstaticvoidmain(String[] args) {
// 字符串拼接StringBuilderTest.stringConcat();
// append拼接StringBuilderTest.stringBuilderAppend();
    }
}


结果差距巨大:


这个结果应当是显而易见的,但是我们要思考的是为什么?

原因:

  1. 字符串拼接每次拼接都会创建一个StringBuilder对象,而且在返回的时候会再次新建一个String对象(StringBuilder.toString()方法),每次都创建了两个对象,耗时严重
  2. 每次循环创建两个对象占用内存巨大
  3. 内存占用过大,GC的次数也会增加,GC是最耗时的操作

即使是StringBuilder单一对象append,也可以进行优化:

如果确定了StringBuilder的次数,那么就可以在创建StringBuilder的时候指定他的容量,从而减少StringBuilder的扩容操作,扩容操作也是非常耗时的.

/*** 指定StringBuilder容量*/publicstaticvoidstringBuilderAppend(){
longstart=System.currentTimeMillis();
StringBuilderbuilder=newStringBuilder(100000);
for (inti=0; i<100000; i++) {
builder.append("test");
        }
longend=System.currentTimeMillis();
System.out.println(end-start);
    }


4. String的intern方法

此方法在上文用过,如果调用此方法的字符串不在常量池中,就将此字符串放到常量池中,如果此字符串在常量池中,那么就返回常量池中的字符串

该方法为native方法,即本地方法

源码注释:

Returnsacanonicalrepresentationforthestringobject.
Apoolofstrings, initiallyempty, ismaintainedprivatelybytheclassString.
Whentheinternmethodisinvoked, ifthepoolalreadycontainsastringequaltothisStringobjectasdeterminedbytheequals(Object) method, thenthestringfromthepoolisreturned. Otherwise, thisStringobjectisaddedtothepoolandareferencetothisStringobjectisreturned.
Itfollowsthatforanytwostringssandt, s.intern() ==t.intern() istrueifandonlyifs.equals(t) istrue.
Allliteralstringsandstring-valuedconstantexpressionsareinterned. Stringliteralsaredefinedinsection3.10.5oftheTheJava™LanguageSpecification.
Returns:
astringthathasthesamecontentsasthisstring, butisguaranteedtobefromapoolofuniquestrings.
publicnativeStringintern();


翻译:

返回字符串对象的规范表示。
字符串池最初是空的,由 String 类私下维护。
当调用 intern 方法时,如果池中已经包含一个等于该 String 对象的字符串(由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中并返回对此 String 对象的引用。
因此,对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为真时,s.intern() == t.intern() 为真。
所有文字字符串和字符串值常量表达式都被实习。字符串文字在 Java™ 语言规范的第 3.10.5 节中定义。
返回:
与此字符串具有相同内容的字符串,但保证来自唯一字符串池。


例子:

("a"+"b"+"c").intern() == "abc";

intern() 方法在不同的jdk版本有着不一样的表现

jdk6: 调用intern()方法,如果该字符串不在常量池中存在,就直接在永久代的字符串常量池中创建该字符串对象

jdk7/8及以后: 调用intern()方法,如果该字符串不在常量池中,那么不会再元空间常量池中创建一个新的对象,而是直接使用堆空间中的String对象的地址作为字符串常量池的地址.

Stringtest=newString("hello") +newString("world");
Stringintern=test.intern();
Stringtest1="helloworld";
System.out.println(intern==test); // jdk6中为false  jdk7/8中为true


4.1 intern()的用处

如上文所说,intern()方法可以获取常量池中的对象,如果对象不存在会新建一个,那么当系统中有大量重复字符串存储的时候就可以使用intern()方法

好处:

避免在堆中创建大量对象,占用堆空间; 因为一个字符串在常量池中维护过一次以后,下次再使用intern()方法就会获取常量池中对象的引用,而不是再在堆中创建一个新对象

但是要酌情使用,非频繁的字符串对象不要放入常量池,因为常量池位于方法区,不易gc

5. 题目理解String底层

5.1 new String("ab")创建了几个对象

答案: 两个

  1. new 创建的String 对象
  2. "ab" 字面量创建的字符串常量池对象

证明: 通过字节码指令看


5.2 字符串对象拼接创建了多少对象

// 创建了多少个对象Stringtest=newString("hello") +newString("world");


从字节码上看应该是创建了五个对象

但是如果再往深层看, StringBuilder的toString()方法也是创建了一个新的String对象的.

6. StringTable的垃圾回收

String也是有垃圾回收的

证明:

packagecom.zy.study12;
/*** @Author: Zy* @Date: 2021/12/13 21:37* 测试String的垃圾回收* -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails*/publicclassStringGc {
publicstaticvoidmain(String[] args) {
for (inti=0; i<100000; i++) {
String.valueOf(i).intern();
        }
    }
}
-Xms15m-Xmx15m-XX:+PrintStringTableStatistics -XX:+PrintGCDetails
堆内存15m 打印StringTable的统计数据 打印gc详细信息


输出:

gc信息,发生了一次YGC,回收了一次eden区

StringTable统计信息,可以看到到6w多后,就不再增大,等待垃圾回收后再次创建.

6.1 扩展: G1中对String的去重操作

jdk8中默认的垃圾回收期G1在回收的时候会对String进行去重操作

  1. 去重去的是堆中的对象,而不是常量池
  2. jdk1.8中String底层的存储是char[],去重也是针对此char[]
  3. 去重的操作: 分析每一个可能存在重复的char[],放入一个不重复的hashtable中,之后有另外一个队列会查询此hashtable,如果存在一个一样的char[],那么就会对两个char[]进行去重,释放其中一个char[]的引用,如果此hashtable中不存在该char[],那么就将此char[]放入hasttable中.


jvm参数开启去重(默认不开启):

UseStringDeduplication 开启去重 true/false
PrintStringDeduplicationStatistics 打印去重统计信息 true/false
StringDeduplicationAgeThreshold 设置去重候选对象的年龄,如果达到此值,被认为可能是重复对象


目录
相关文章
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
77 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
39 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
2月前
|
Java 应用服务中间件 程序员
JVM知识体系学习八:OOM的案例(承接上篇博文,可以作为面试中的案例)
这篇文章通过多个案例深入探讨了Java虚拟机(JVM)中的内存溢出问题,涵盖了堆内存、方法区、直接内存和栈内存溢出的原因、诊断方法和解决方案,并讨论了不同JDK版本垃圾回收器的变化。
36 4
|
2月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
68 3
|
2月前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
35 3
|
2月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
65 3
|
6月前
|
缓存 Java
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
55 0
|
2月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
59 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
2月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
58 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
4月前
|
Go
Go - 学习 grpc.Dial(target string, opts …DialOption) 的写法
Go - 学习 grpc.Dial(target string, opts …DialOption) 的写法
65 12