1、JVM相关知识
JVM的体系结构图:
Java栈(线程私有数据区):
每个Java虚拟机线程都有自己的Java虚拟机栈,Java虚拟机栈用来存放栈帧,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
Java堆(线程共享数据区):
在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配。
方法区(线程共享数据区):
方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。在JDK8之前永久代是方法区的一种实现,而JDK8元空间替代了永久代,永久代被移除,也可以理解为元空间是方法区的一种实现。
常量池(线程共享数据区):
常量池常被分为两大类:静态常量池和运行时常量池。
静态常量池也就是Class文件中的常量池,存在于Class文件中。
运行时常量池(Runtime Constant Pool)是方法区的一部分,存放一些运行时常量数据。
重点了解下字符串常量池:
字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。
字符串常量池的存在使JVM提高了性能和减少了内存开销。
使用字符串常量池,每当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。
使用字符串常量池,每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。
2、String源码分析
public final class String
implements java.io.Serializable, Comparable, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
........
}
首先我们来看看String类,String类是用final修饰的,这意味着String不能被继承,而且所有的成员方法都默认为final方法。
接下来看看String类实现的接口:
java.io.Serializable:这个序列化接口仅用于标识序列化的语意。
Comparable:这个compareTo(T 0)接口用于对两个实例化对象比较大小。
CharSequence:这个接口是一个只读的字符序列。包括length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了改接口。
最后看看String的成员属性:
value[] :char数组用于储存String的内容。
offset :存储的第一个索引。
count :字符串中的字符数。
hash :String实例化的hashcode的一个缓存,String的哈希码被频繁使用,将其缓存起来,每次使用就没必要再次去计算,这也是一种性能优化的手段。这也是String被设计为不可变的原因之一。
3、关于equals和==
(1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。
(2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等。
(3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。
例子:
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
输出结果:true,true。
说明:s1和s2都指向字符串常量池中的“abc”对象,故s1与s2地址相同(==比较地址是否相同,比较是否为同一个对象)。字符串重写了equals方法,同一个对象返回true,内容相同,也返回true。
String s3= new String("abc");
String s4 = "abc";
System.out.println(s3 == s4);
System.out.println(s3.equals(s4));
输出结果:false,true。
说明:s3指向堆中的“abc”对象。s4指向常量池中的对象。s3与s4不指向同一个对象,故第一个返回false。s3与s4内容相同,输出结果true。
String s5 = "a"+"b"+"c";
String s6="abc";
System.out.println(s5== s6);
System.out.println(s5.equals(s6));
输出结果:true,true。
说明:s5在编译期间会优化为 s5=”abc”。此时和上面的s1和s2代码逻辑一样。输出结果和s1 s2相同。
String s7 = "ab";
String s8 = "abc";
String s9 = s7+"c";
System.out.println(s9 == s8);
System.out.println(s9.equals(s8));
输出结果:false,true。
说明:s8指向字符串常量池中的“abc”对象,s9语句会在编译期间,优化为stringBuild(s7).append(“c”)。也就是s9最终指向stringBuild.toString()方法返回的“abc”对象(在堆上)。故s9!=s8;内容相同,所以equals返回true。
String s10 = "wenwei";
final String s11 = "wen";
String s12 = s11+"wei";
System.out.println(s10==s12);
输出结果:true。
说明:s10指向常量池中的“wenwei”对象。由于s11字符串被final修饰,也就是不可改变常量,s12语句在编译的时候,会优化成s12=”wen”+”wei”。此时s10和s12和s5 s6指向逻辑相同。
String s13 = new String("zhang");
s13.intern();
String s12 = "zhang";
System.out.println(s13 == s12);
输出结果:false。
说明:intern(),方法在jdk1.7中执行逻辑是:先检查常量池是否有改字符串对象;如果没有则将s13在堆中的对象引用,赋值给常量池中的变量,并返回本引用,如果存在则直接返回引用。s13代码会在类加载解析阶段,在常量池中生成“zhang”对象,s13.intern()方法,检查常量池存在,不会将s13堆中的对象引用赋值到常量池中;s12语句,指向的是常量池中的对象,s13指向堆中新建的对象。故输出结果为false。
String s14 = new String("1") + new String("1");
s14.intern();
String s15 = "11";
System.out.println(s14 == s15);
输出结果:true。
说明:s14语句会在堆中产生一个“11”对象,s14.intern()语句,会将对堆中的“11”对象引用记录的常量池中(jdk1.7以后)。s15语句指向后,s15的值即为常量池中引用的值,也指向堆中的“11”对象。故输输出值为true。
代码优劣
//代码片段1
String result1="";
for(int i =0;i<100;i++){
result1+=i;
}
System.out.println(result1);
//代码片段2
StringBuilder sb = new StringBuilder();
for(int i =0;i<100;i++){
sb.append(i);
}
System.out.println(sb.toString());
说明:
片1中的result+=i语句编译器编译会优化为,reuslt =newStringBuilder(result).append(i).toString()。
这样片段1代码执行100次循环,会创建100个StringBuilder对象,和100个result对象(toString()方法)。代码片段2执行100次循环,就创建了1个StringBuilder和1个String对象。故代码片段2内存占用更少,性能最佳。注意,在java中字符串的+操作,如果连接的不是常量字符串或final修饰的字符变量,编译后都会编译成StringBuilder().append()的形式。
4、String被设计成不可变和不能被继承的原因
String是不可变和不能被继承的(final修饰),这样设计的原因主要是为了设计考虑、效率和安全性。
字符串常量池的需要:
只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段。
String对象缓存HashCode:
上面解析String类的源码的时候已经提到了HashCode。Java中的String对象的哈希码被频繁地使用,字符串的不可变性保证了hash码的唯一性。
安全性
首先String被许多Java类用来当参数,如果字符串可变,那么会引起各种严重错误和安全漏洞。
再者String作为核心类,很多的内部方法的实现都是本地调用的,即调用操作系统本地API,其和操作系统交流频繁,假如这个类被继承重写的话,难免会是操作系统造成巨大的隐患。
最后字符串的不可变性使得同一字符串实例被多个线程共享,所以保障了多线程的安全性。而且类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。
5、关于String的常见面试题解析
public static void testString1(){
//此句代码会产生两个对象,一个在编译类加载(解析)阶段产生,一个在运行时产生。
String s1= new String("weiwei");
//________________________________________
//先在常量池创建对象
String s2 = "zhang";
//创建一个对象
String s3 = new String("zhang");
//________________________________________
//创建一个对象
String s4 = "a"+"b"+"c";
//________________________________________
String s5 = "helloworld";
final String s6 = "hello";
//没有创建对象
String s7 = s6+"world";
//________________________________________
String s8 = "how are you";
String s9 = "how are";
//创建一个对象
String s10 = s7+"you";
}
(1)执行s1语句的时候,会产生两个对象,第一个对象是在类加载的解析阶段,会将字符串“weiwei”在常量池中创建(首先在常量池查找,此处假设其他地方的代码中没有此字符串常量),第二个对象时在代码运行期间,在堆上创建内容为“weiwei”的对象。
(2)执行s2,s3语句,首先s2语句,在加载阶段和s1语句加载相同,常量池不存在,会在常量池中创建对象。s3语句,在类加载解析阶段,由于s2语句已经确保在常量池中存在“zhang”对象,故不会产生对象,在运行期间,new关键字,会在堆上产生一个对象。故s3语句只会生成一个对象。
(3)执行s4语句,首先java文件编译的时候,会将s4语句优化为 String s4 = “abc”;故s4语句,会在类的加载解析阶段,创建一个“abc”对象存放在常量池中,在运行期间,不会创建新的对象,而是将常量池“abc”的引用,赋值给s4。
(4)执行s5,s6,s7语句,首先s5语句,同s4语句相同,会在常量池中产生一个对象。s6,s7语句在编译期间会进行优化,s6直接优化成字符串字面常量,s7会优化成s7=”hello”+”world”也即是 s7=”helloworld”;s7语句不会新建对象。
(5)执行s8,s9,s10,s8和s9同s2语句执行逻辑相同。s10在编译期间,会将+号编译成StringBuild.append(),在装载阶段“you”会在字符串常量池产生一个对象;由于StringBuild会产生一个对象,故s10语句会产生两个对象。
参考文章:
https://blog.csdn.net/qq_34490018/article/details/82110578
https://blog.csdn.net/qq_22494029/article/details/79306182