✅典型解析
创建的对象数应该是1个或者2个。
首先要清楚什么是对象?
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的,在HotSpot虚机中,存储的形式就是oop-klass model,即ava对象模型。我们在Java代码中,使用new创建一个对象的时候,JVM会创建一instanceOopDesc对象,这个对象中包合了两部分信息,对象头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息,元数据其实维护的是指针,指向的是对象所属的类的instanceKlass。
这才叫对象。其他的,一概都不叫对象。
那么不管怎么样,一次new的过程,都会在堆上创建一个对象,那么就是起码有一个对象了。至于另外一个对象到底有没有要看具体情况了。
另外这一个对象就是常量池中的字符串常量,这个字符串其实是类编译阶段就进到Class常量池的,然后在运行期,字符串常量在第一次被调用(准确的说是ldc指令)的时候,进行解析并在字符串池中创建对应的String实例的。
在运行时常量池中,也并不是会立刻被解析成对象,而是会先以VM_CONSTANT_UnresolveString_info的形式驻留在常量池。在后面,该引用第一次被LDC指令执行到的时候,就尝试在堆上创建字符串对象,并将对象的引用驻留在字符串常量池中。
通过看上面的过程,你也能发现,这个过程的触发条件是我们没办法决定的,问题的题干中也没提到。有可能执行这段代码的时候是第一次LDC指令执行,也许在前面就执行过了。
所以,如果是第一次执行,那么就是会同时创建两个对象。一个字符串常量引用指向的对象,一个我们new出来的对象。
如果不是第一次执行,那么就只会创建我们自己new出来的对象。
至于有人说什么在字符串池内还有在栈上还有一个引用对象,你听听这说法,引用就是引用。别往对象上面扯。
✅什么是Class常量池,和运行时常量池关系是什么?
Class常量池可以理解为是Class文件中的咨源仓库。Cass文件中除了包合类的版本,字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Litera)和符号引用Symbolic References)。
Class是用来保存常量的一个媒个场所,并且是一个中间场所。Class文件中的常量池部分的内容,会在运行期被运行时常量池加载进去。
✅ 查看Class常量池
由于不同的Class文件中包合的常量的个数是不固定的,所以在Class文件的常量池入口处会设置两个字节的常量池容量计数器,记录了常量池中常量的个数。
当然,还有一种比较简单的产看Class文件中常量池的方法,那就是通过 javap 命令,对于以上的HelloWorld.class,可以通过
javap -v HelloWorld.class
查看常量池内容如下:
从上图可以看到,反编译后的class文件常量池中共有16个常量。而Class文件中常量计数器的数值是0011,将该16进制数字转化为10进制的结果是17。
原因是与Java的语言习惯不同,常量池计数器是从0开始而不是从1开始的,常量池的个数是10进制的17,这就代表了其中有16个常量,索引值的范围为1-16。
✅字符串常量是什么时候进入到字符串常量池的?
字符串常量池中的常量有两种来源,一种是字面量会在编译期先进入到Class常量池,然后再在运行期进去到字符串池,还有一种就是在运行期通过intern将字符串对象手动添加到字符串常量池中。
那么,Class常量池中的常量,是在什么时候被放进到字符串池的呢?
Java 的类加载过程要经历加载 (Loading) 、链接 (Linking) 、初始化nitializing) 等几个步,在链接这人步骤,又分为验证 (Verification) 、准备 (Preparation) 以及解析(Resolution) 等几个步骤。
在Java 虚拟机规范及Java语言规范中都提到过:
《The Java Virtual Machine Specification》 5.4 Linking ::
For example, a Java Virtual Machine implementation may choose to resolve each symbolic reference ina class or interface individually when it is used (azy" or “late” resolution), or to resolve them all atonce when the class is being verified (“eager” or “static” resolution)
《The Java Language Specification》 12.3 Linking of Classes and Interfaces
For example, an implementation may choose to resolve each symbolic reterence in a class or interfaceindividually, only when it is used (lazy or late resolution), or to resolve them all at once while the classis being verified (static resolution), This means that the resolution process may continue, in someimplementations, after a cass or interface has been initialized.
大致意思差不多,就是说,Java 虚拟机的实现可以选择只有在用到类或者接口中的符号引用时才去逐一解析他(延迟解析),或者在验证类的时候就解析每个引用(预先解析)。这意味着在一些虚拟机实现中,把常量放到常量池的步骤可能是延迟处理的。
对于 HotSpot 虚拟机来说,字符串字面量,和其他基本类型的常量不同,并不会在类加载中的解析阶段填充并驻留在字符串常量池中,而是以特殊的形式存储在运行时常量池中。只有当这个字符串字面量被调用时,才会对其进行解析,开始为他在字符串常量池中创建对应的 String 实例。
通过查看 HotSpotJDK 1.8 的ldc 指令的源代码,也可以验证上面的说法。
ldc 指令表示int、float或String型常量从常量池推送至栈顶
IRT_ENTRY(void,InterpreterRuntime::ldc(JavaThread* thread, bool wide))
//access constant pool
ConstantPool* pool = method(thread)->constants():
int index = wide ? get_index_u2(thread, Bytecodes::_ldc_w) : get_index_u1(thread,Bytecodes :: _ldc);
constantTag tag = pool->tag_at(index);assert (tag.is_unresolved_klass() || tag.is_klass(),"wrong ldc call");
Klass* klass = pool->klass_at(index,CHECK);
oop java_class = klass->java mirror();
thread->set_wm_result(java_class);
IRT_END
所以,字符串常量,是在第一次被调用(准确的说是ldc指令)的时候,进行解析并在字符审池中创建对应的String实例的。
✅字符串常量池是如何实现的?
字符串常量池(String Constant Pool) 是Java中一块特殊的内存区域,用于存储字符串常量。
当程序中出现字符串常量时,Java编译器会将其放入字符串常量池中。字符串常量是不可变的,因此可以共享。如果字符串常量池中已存在相同内容的字符串,编译器会直接引用已存在的字符串常量,而不会创建新的对象。
在HotSpot虚拟机中:
在JDK 1.6及之前的版本,字符串常量池通常被实现为方法区的一部分,即永久代(Permanent Generation)用于存储类信息、常量池、静态变量、即时编译器编译后的代码等数据。
从JDK 1.7开始,字符串常量池的实现方式发生了重大改变。字符串常量池不再位于永久代,而是直接存放在堆(Heap) 中,与其他对象共享堆内存。
之所以要挪到堆内存中,主要原因是因为永久代的 GC 回收效率太低,只有在FulGC的时候才会被执行回收。但是Java中往往会有很多字符串也是朝生夕死的,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
✅字符串常量从哪来的?
字符串常量池中的常量有以下几个来源:
1、字面量常量
在代码中直接使用双引号括起来的字符串字面值(如 String s "Hllis”)会被认为是常量,并且会在编译后进入class文件的常量池,并且在运行阶段,进入字符串常量池。这是最常见的字符串常量来源。
2、intern() 方法
String类提供了一intern0方法,用于将字符电对象手动添加到字符电常量池中。调用intern0方法时,如果字答串常量池中已经存在相同内容的字符串,将会返回常量池中的引用:如果不存在,则会在常量池中创建新的字符串。
✅扩展知识仓
✅字面量和运行时常量池
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JM中创建的字符串的数量,字符串类维护了一个字符串常量池。
在JVM运行时区域的方法区中,有一块区域是运行时常量池,主要用来存储编译期生成的各种字面量和符号引用。
了解Class文件结构或者做过Java代码的反编译的朋友可能都知道,在iava代码被 javac 编译之后,文件结构中是包含 部分 Constant pool 的。比以下代码:
public static void main(string[] args) {String s ="Hollis";
}
经过编译后,常量池内容如下:
上面的Class文件中的常量池中,比较重要的几个内容:
上面几个常量中, s 就是前面提到的符号引用,而 Hollis 就是前面提到的字面量。而Class文件中的常量池部分的内容,会在运行期被运行时常量池加载进去。关于字面量,详情参考Java SE Specifications: Java SE Specifications
✅intern
编译期生成的各种字面量和符号引用是运行时常量池中比较重要的一部分来源,但是并不是全部。那么还有一种情况,可以在运行期向运行时常量池中增加常量。那就是 string 的 intern 方法。
当一个 string 实例调用 intern() 方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用:。
intern()有两个作用,第一个是将字符串字面量放入常量池 (如果池没有的话),第二个是返回这个常量的引用。
✅intern的正确用法
不知道,你有没有发现,在 String s3 = new String(“Hollis”).intern(); 中,其实 intern 是多余的?
因为就算不用 intern ,Hollis作为一个字面量也会被加载到Class文件的常量池,进而加入到运行时常量池中为啥还要多此一举呢? 到底什么场景下才需要使用 intern 呢?
在解释这个之前,我们先来看下以下代码:
String s1 ="Hollis";
String s2 = "Chuang";
String s3 = s1 + s2;
String s4 ="Hollis" + "Chuang";
在经过反编译后,得到代码如下:
String s1 = "Hollis";
String s2 = "Chuang";
String s3 = (new StringBuilder()).append(s1).append(s2).toString();
String s4 ="HollisChuang";
可以发现,同样是字符串拼接,s3和s4在经过编译器编译后的实现方式并不一样。s3被转化成 stringBuilder及 append ,而s4被直接拼接成新的字符串。
如果你感兴趣,你还能发现, String s3 = s + s2;经过编译之后,常量池中是有两个字符串常量的分别是Chuang (其实 Hollis 和 Chuang 是 String s1 =“Hollis”,和 String s2 = “ChuanHollis.g”,定义出来的),拼接结果 HollisChuang 并不在常量池中。
究其原因,是因为常量池要保存的是已确定的字面量值。也就是说,对于字符串的拼接,纯字面量和字面量的拼接,会把拼接结果作为常量保存到字符串池。
如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成 StringBuilder.append这种情况编译器是无法知道其确定值的。只有在运行期才能确定。
那么,有了这个特性了, intern 就有用武之地了。那就是很多时候,我们在程序中得到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。
这时候,对于那种可能经常使用的字符串,使用 intern 进行定义,每次JVM运行到这段代码的时候,就会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。
比如 —— 深入解析String#intern中举的一个例子:
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];public static void main(String[] args) throws Exception {Integer[] DB_DATA = new Integer[10];Random random = new Random(10 * 10000);for (int i = 0; i < DB_DATA.length; i++) {DB_DATA[i] = random.nextInt();}long t = System.currentTimeMillis();for (int i = 0; i < MAX; i++) {//arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();}System.out.println((System.currentTimeMillis() - t) + "ms");System.gc();
}
运行的参数是:-Xmx2g -Xms2g -Xmn1500M 上述代码是一个演示代码,其中有两条语句不一样,一条是使用 intern,一条是未使用 intern。结果如下图:
2160ms
826ms
通过上述结果,我们发现不使用 intern 的代码生成了1000w 个字符串,占用了大约640m 空间。 使用了 intern 的代码生成了1345个字符串,占用总空间 133k 左右。其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。
细心的同学会发现使用了 intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String 后,然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。 毕竟这里使用了1000w次intern 才多出来1秒钟多的时间。
So,我们明确的知道,会有很多相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,只能我们通过 intern 显示的将其加入常量池,这样可以减少很多字符串的重复创建。