JVM系列之常量池与String

一、常量池

常量池有很多概念,包括运行时常量池、class常量池、字符串常量池。

虚拟机规范只规定以上区域属于方法区,并没有规定虚拟机厂商的实现。

严格来说是静态常量池和运行时常量池:

1、静态常量池       存放字符串字面量、符号引用以及类和方法的信息,而运行时常量池存放的是运行时一些直接引用。
2、运行时常量池   在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。

二、字符串常量池:String的创建分配内存地址

1、String不可变吗?这个不可变到底是什么意思?

记得以前学习java的时候,学到String,就记住了String对象他是不可变。但是还是很蒙蔽的,怎么就不可变了。我运行下面代码输出  222,这不是可变吗?

public class Test {
    public static void main(String[] args) {
        String a = "111";
        a = "222";
        System.out.println(a);
    }
}

JVM系列之常量池与String

a一开始为 111,我改变了一下a的值,赋值a为  222了,打印输出一下  也确实是  222呀,这不是可改变的吗?如果是不可变的,那不就是输出  111吗?带着这样的疑问,我们来好好掰一掰String字符串了。

那么为什么还要这么明目张胆的说String 为什么是不可变的呢?

首先我们查看String类的源码:

JVM系列之常量池与String

可以发现一下几点:

1.String 类是final修饰;

2.String存储内容使用的是char数组,一般我们说的基本数据类型没有包括String,就是因为char才是基本数据类型,String类正好是使用了字符这个基本数据类型的数组形成字符串;

3.char数组是final修饰;

而final关键字的意思就是“最后的,最终的”,也就是说被final修饰的东西就是不可变的,变量就成了常量,这里的不可变指的是这个变量指向的对象引用地址就不能变了(比如String里面的value数组被final修饰,当String a = "111"时,“111”就是赋值给了value数组,一经赋值后,就不可以改变了,所以就有了a="111",a变量指向的“111”字符串常量对象自己的这个成员属性value数组指向的数组对象不可变,String类也就一个属性,这个属性还是常量,就是用来保存字符的。这里要搞清楚一个概念:String a并没有被final修饰,所以a变量保存的String对象引用地址是可以改变的。a = "222",此时并不会去将上一个创建的常量池中的“111”String对象修改为“222”,而是会在常量池重新创建一个“222”对象,并把“222”的引用地址保存在变量a中,此时不再保存常量池中“111”的引用地址了,换成了新的引用地址)

这就解释了上面为什么打印的是 222 呀,因为a变量指向的引用地址都变量,指向“222”了。

JVM系列之常量池与String

其实在JVM的运行中,会单独给一块地分给String,就是我们所说的 “ 字符串常量池 ”。我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用频率非常高的一个类。JVM为了提高性能和减少内存的开销,在实例化字符串对象的时候进行了一些优化:所以说String类在java中生下来就是特殊的。

使用了字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。这里先去JVM给常量池里找,找到了就不用创建对象了,直接把对象的引用地址赋给a(这种方式就减少同一个值的字符串对象的重复创建,节约内存)。找不到会重新创建一个对象,然后把对象的引用地址赋给a。同理a="222";也是先找,找不到就重新创建一个对象,然后把对象的引用地址赋给a。大家有没有发现我上面的描述中“引用地址”。比如说 Object obj = new Object();很多人喜欢成obj为对象,其实obj不是对象,他只是一个变量,然后这个变量里保存一个Object对象的引用地址罢了。

引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。Object obj = new Object(),new object()就是实体,obj就是变量保存了实体的引用地址。就比如说:你是小明,小明就是obj,你本人这个高大威猛的人就是实体对象,而你在世界(内存)有一个唯一对应的身份证号码(引用地址)了。

所以上面String a = “111”;表达的是变量a里保存了“111”这个对象的引用地址。变量是可以变的,不能变的是“111”。你高大威猛的身体本人在这个世界就唯一一份,是不可变的,但是“小明” 这个称呼不一定就非得指的是你呀,这个称呼很多人在用呀,另一个不高大不威猛的人也可以叫小明(那小明这个称呼变量就指向了另一个对象了),所以说小明这个称呼(引用地址)是可以变的。除非你这时候加了final String a = "111",这时的a就唯一指向“111”代表的引用了,不能变了,a成常量了。

三、final关键字

当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

1. final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化。

2. final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值(即地址的值)不发生变化。另外final修饰一个成员变量(属性),必须要显示初始化(不初始化会编译期报错,赋值后再次赋值也会编译期报错)

四、注意事项

JVM系列之常量池与String

JVM系列之常量池与String

无论是concat、replace、substring还是trim方法的操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。

String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何变化性的操作都会生成新的对象。String对象每次有变化性操作的时候,都会从新new一个String对象(这里指的是有变化的情况)。

 

运行下面代码:

public static void main(String[] args) {
        String a = "111";
        String a1 = "111";
        String b = new String("111");
        //对象地址是同一个
        System.out.println(a==a1);
        //对象内容是一样的
        System.out.println(a.equals(a1));
        //对象地址不一样
        System.out.println(a==b);
        //对象内容是一样的
        System.out.println(a.equals(b));
    }

输出:

JVM系列之常量池与String

解释:

1、String a = "111"; 在JVM申请内存存放"111"对应的对象,并将对象保存起来。当String a1="111";的时候,会先去JVM的那块地里寻找是否存在"111",刚好前面保存过,所以找到,然后直接把对象的引用地址给了a1。所以此时的a和a1都保存着同一个引用地址。

2、接触java后都知道可以new一个对象。所以 String b = new String("111");就是创建一个对象然后把对象引用地址赋给变量b。但是这里有个特殊点,那就是(“111”),这里会先去JVM里的那块地里找找,找到了直接存放引用地址。找不到创建一个对象然后把引用地址给String的有参构造方法里。所以第三个中输出false,因为a和b所保存的对象引用是不一样的。

3、最后一个输出true。那是因为两个变量所保存的引用地址中的内容都是“111”。

常见的面试题:

String s = new String("111")创建了几个对象?

答案:如果常量池中存在,则只需创建一个对象,否则需要创建两个对象(堆中间一个String对象,常量池中“111”)。

 

上一篇:AtCoder Regular Contest 111


下一篇:Python pip Retrying 拒绝连接 error 111