Java字符串常量池相关问题

首先,我们来熟悉字符串常量池:

字符串在java程序中被大量使用,为了避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建做了一定的优化,有一块专门的区域用来存储字符串常量,该区域就是字符串常量池。

字符串常量池涉及到一个设计模式,叫享元模式,所谓享元模式是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素。

需要注意的是:
在Java jdk 1.6,常量池是放在Java JVM的方法区中的;
在Java jdk 1.7后,常量池被放入到堆空间中。

官网说明:

Area: HotSpot
Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.
RFE: 6962931

问题1:String s=new String(“abc”)创建了几个对象?
把String s=new String(“abc”)这代码拆成String str、=、”abc”和new String()四部分来。String str定义了一个名为str的String类型的变量,但是对象还没有被创建;=是对变量str进行初始化,将某个对象的引用赋值给它,显然也没有创建对象;那么创建了多少对象的关键就在于”abc”和new String()这两部分了。

我们使用new语句创建对象,调用了String类的构造器,该构造器如下:

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

可以看到,该构造器的参数也是一个String对象,这个对象就是“abc”,然后将其复制到堆中,并将堆中的引用赋值给s。

当常量池中有“abc”时,该语句创建了一个对象,因为“abc”对象直接取自常量池便可。

当常量池中没有“abc”时,在常量池中创建了一个“abd”对象,然后再堆中创建了“abc”对象,所以该语句创建了两个对象。

问题2:String name=new String(“java”+”hello”)创建了几个对象?

该问题跟问题1差不多,重点在于”java”+”hello”这里有多少个对象?
使用反编译工具看看产生的Java字节码:图1

相应命令的含义:

new:创建一个对象,并将新对象的引用推到操作数堆栈。

new is used to create object instances. 

new takes a single parameter, <class>, the name of the class of object you want to create. <class> is resolved into a Java class (see Chapter 7 for a discussion of how classes are resolved). Then new determines the size in bytes of instances of the given class and allocates memory for the new instance from the garbage collected heap. The fields of the instance are set to the initial value 0 (for numeric and boolean fields), or null (for reference fields). Next, a reference to the new object is pushed onto the operand stack.

Note that the new object is initialize uninitialized - before the new object can be used, one of its <init> methods must be called using invokespecial, as shown in the example below.

dup:复制栈顶的值,再次压栈。

This pops the top single-word value off the operand stack, and then pushes that value twice - i.e. it makes an extra copy of the top item on the stack.

lcd:把数值常量或String常量值从常量池中推送至栈顶

ldc pushes a one-word constant onto the operand stack. ldc takes a single parameter, <value>, which is the value to push. The following Java types can be pushed using ldc:

int float String

Pushing a String causes a reference to a java.lang.String object to be constructed and pushed onto the operand stack. Pusing an int or a float causes a primitive value to be pushed onto the stack.

invokespecial:调用类的某个方法

invokespecial first looks at the descriptor given in <method-spec>, and determines how many argument words the method takes (this may be zero). It pops these arguments off the operand stack. Next it pops objectref (a reference to an object) off the operand stack. objectref must be an instance of the class named in <method-spec>, or one of its subclasses. The interpreter searches the list of methods defined by the class named in <method-spec>, looking for a method called methodname whose descriptor is descriptor. This search is not based on the runtime type of objectref, but on the compile time type given in <method-spec>.

astore_n:弹出栈上的一个引用并将其存储在本地变量中。

Pops objectref (a reference to an object or array) off the stack and stores it in local variable <n>, where <n> is 0, 1, 2 or 3. <n> must be a valid local variable number in the current frame.

'astore_n' is functionally equivalent to 'astore <n>', although it is typically more efficient and also takes fewer bytes in the bytecode.

0:new new一个String对象(在堆内存中开辟空间),并将其引用入栈;
3:dup 复制栈顶的刚刚放入的引用,再次压栈,这时栈里有两个重复的内容,深度为2这是所有new对象都会做的事情,因为jvm要做
4:ldc 从常量池取出“javahello”这个字符串(对象引用)压栈,此时栈深度为3
6 invokespecial 弹出栈顶的两个对象,调用弹出的第二个对象的方法,栈深度为1;
9 将此时栈顶引用弹出并存储到局部变量中(slot 1),此时栈就清空了,深度0;

从字节码的层面分析代码,可以看到程序直接从常量池中取出javahello字符串的引用压栈,所以得出的结论是:

java在编译期间会自己优化,将”java”+”hello”合并成一个对象”javahello”。剩下的就跟问题1没有什么区别了,所以答案还是一个或两个。

关于Java字节码,推荐阅读一篇文章:大话+图说:Java字节码指令——只为让你懂

问题3:

String str = "abc"+"def";   //将"abcdef"放入字符串池中  
String str2 = "abcdef";   //从字符串池中取出"abcdef"字符串的引用赋值给str2 
System.out.println(str == str2);  

上面的代码打印的是true。

请看下面代码,会打印出什么结果?

String str1 = "abc";  
String str2 = "def";  
String str3 = str1 + str2;  
String str4 = "abcdef";   
System.out.println(str3 == str4); 

关键看String str3 = str1 + str2进行了什么操作?

看一下反编译出来的字节码:

图2

可以看到创建了一个StringBuilder对象,使用append方法进行了字符串的拼接。所以经过JVM的处理,这段代码类似于以下写法:

String str1 = "abc";  
String str2 = "def";  
String str3 = new StringBuilder(str1).append(str2).toString();
String str4 = "abcdef";   
System.out.println(str3 == str4); 

这里写图片描述

str3指向的是堆中的”abcdef”对象,而str4指向的是常量池中的对象,所以打印false。

那么,下面这段代码又会打印什么呢?

String str1 = "abc";  
String str2 = "def";  
String str3 = str1 + str2;  
str3.intern();
String str4 = "abcdef";   
System.out.println(str3 == str4);

先看intern方法的作用:

一个初始时为空的字符串池,它由类 String 私有地维护。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。 它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

但在Java jdk 1.7,常量池被放入到堆空间中后,intern方法也产生了变化:

之前的做法是如果字符串是首次出现,那么将字符串复制到常量池中,然后返回常量池中的引用;JDK1.7的做法是如果是首次出现,那么在常量池中记下这个字符串的引用(该字符串对象在堆中的引用),然后直接返回这个引用。

所以执行str3.intern()时,常量池中存储的是str3的引用,当用到字符串”abcdef”时,直接返回了str3的引用,所以打印true。

那么再看以下代码:

String str1 = new StringBuilder("abc").toString();
System.out.println(str1 == str1.intern()); 

“abc”在常量池中,str1肯定是指向的堆中的对象,s1.intern()得到的是常量池中的,不是同一个,所以是false。

final String baseFinalStr = "baseStr";
String str1 = "baseStr01";
String str2 = baseFinalStr+"01";
System.out.println(str1 == str2);//#5

因为final变量在编译后会直接替换成对应的值,所以String str2 = baseFinalStr+”01”等价于String str2 = “baseStr”+”01”,所以打印true。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值