字符串常量池
JVM为了字符串的服用,减少字符串对象的重复创建,特别维护了一个常量池。
jdk1.7之前的版本,常量池存放在方法区,方法区和JAVA堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态常量、JIT编译后的代码等。在java虚拟机规范中将方法区描述为堆的一个逻辑部分,也被叫做Non-Heap(非堆)。经常遇到的一个错误:java.lang.OutOfMemoryError: PermGen space,这里的PermGenspace也就是我们经常提到的永久代,方法区可以理解为java虚拟机的规范,而永久代则看作是规范的一种实现。而在jdk1.7中,常量池从永久代移到到了堆区,为移除永久代工作做准备。于是我们看到的常量池内存溢出变成了java.lang.OutOfMemoryError: Java heap space。
到了jdk1.8,永久代彻底移除,元空间(Meta space)出现,可以看作是对JVM规范中方法区的另一种实现。不过元空间与永久代之间最大的区别在于:元空间并不在JVM中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,把类的元数据扔到了元空间,而常量池仍然放在java heap中。
元空间取代永久代可能是因为,字符串存于永久代,占用堆区内存,太小可能导致永久代溢出,太大导致老年代溢出,为GC也带来不必要的复杂度,并且回收效率也偏低。当然以上内容仅针对HotSpot 。
String
String str1 = "123";
String str2 = new String("123");
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
首先是String的两种不同的初始化写法。第一种字面量形式的写法,会直接在字符串常量池中查找是否存在值 123,若存在直接返回这个值的引用,若不存在创建一个字符串123存入字符串常量池中。而使用 new 关键字,则会直接在堆上生成一个新的 String 对象,并不会理会常量池中是否有这个值。所以本质上 str1 和 str2 指向的内存地址是不一样的,str1 指向常量池,str2指向堆,而对于“==”比较的是地址,所以第一句是返回false,而第二句equals比较值(对于没有复写equals的对象,equals仍然比较的是地址),当然是返回true。
String对象不可变是因为它用了final修饰。在同一个方法内,当final修饰一个基本数据类型时,表示该基本数据类型的值在初始化后便不能发生变化;如果final修饰一个引用类型,则对其初始化之后便不能再改变值,但栈上的引用可以改变。
上栗子1:
String s = "123";
System.out.println("s = " + s);
s = "456";
System.out.println("s = " + s);
打印结果为
s = 123
s = 456
s的值变了,那123这个对象改变了吗,其实“123”这个对象还是存在,只不过生成了一个新的”456“对象,并把这个s指向了“456”,如下图所示。
在进一步讨论讨论String的不可变性之前,看下面的一个栗子:
public static void main(String args[]) {
String str = "hello";
change(str);
System.out.println(str);
}
private static void change(String str) {
str = "word";
}
输出结果:
hello
为什么这个执行结果是hello,而不像上一个栗子,str指向了新的对象“word”呢?这个涉及到java函数调用时的参数传递策略,java到底是值传递还是引用传递。
先理解几个基础概念:
形参:用来接收调用该方法时传递的参数。只有在被调用的时候才分配内存空间,一旦调用结束,就释放内存空间。因此仅仅在方法内有效,也就是change函数里面的这个str。
实参:传递给被调用方法的值,预先创建并赋予确定值,对应main函数里面的str。
值传递:方法调用时,实参把它的副本传递给对应的形参。
引用传递:方法调用时,将实参的引用地址直接传给形参,函数接收到原参数的内存地址。
值传递和引用传递都是属于函数调用时参数的求值策略(Evaluation Strategy),这是对调用函数时,求值和传值的方式的描述,而非传递的内容的类型,引用类型和引用传递是不同方法的概念,一个描述内存分配方式,一个描述参数求值策略。
对于引用传递,在函数中可以改变原始对象,这里的改变是指将一个变量指向另一个对象,就像第一个栗子中呈现的那样,而不是仅仅是改变属性或者成员变量之类的,所以Java是值传递。
而这些行为与参数类型是基本类型还是引用类型无关。对于值传递来说,无论参数是基本类型还是引用类型,都会在栈上创建一个副本,但有一点不同,对于值类型,这个副本是整个原始值的复制,而对于引用类型来说,由于对象实例在堆上,复制的只是在栈上的它的一个引用值。
上例代码中,str传入change()函数时,形参str只是原引用str的一个引用副本,它们同时指向了堆中的“hello”对象,也就是同时存在两个不同的str。当执行形参str = "word";时,在内存再创建了一个“word”对象,并把形参str引用指向了这个新创建的对象,但是main函数里面的str的地址并没有改变。
那么再来看这段代码:
public static void main(String args[]) {
final char[] ch = {'h', 'e', 'l', 'l', 'o'};
change(ch);
System.out.println(ch);
}
private static void change(char[] ch) {
ch[1] = 'a';
}
输出结果:
hallo
因为是值传递,所以引用不可变,但是可以改变对象的属性或者成员变量。那怎么改变对象的属性或者成员变量呢?比如上述的数组,直接改变ch[i]的值,要么对象自身提供改变的方法,比如StringBuilder的append方法:
public static void main(String args[]) {
StringBuilder sb = new StringBuilder("hello");
change(sb);
System.out.println(sb.toString());
}
private static void change(StringBuilder sb) {
sb.append(" world");
}
输出结果:
hello world
String对象不可变
在Effective Java中对不可变类的设计提出了以下五点建议:
- 不要提供任何会修改对象状态的方法。
- 保证类不会被扩展。
- 使所有的域都是 final 的。
- 使所有的域都成为私有的。
- 确保对于任何可变性组件的互斥访问。
我们再结合jdk源码(1.8)就可以知道为什么String被称为一个不可变类了,除了在类上标识final保证类不会被扩展,其所有的域都是final且私有的,也不提供改变对象状态的方法(比如setter等)。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** 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;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
那么String真的一定不可改变,发现String的值是用一个char[]数组存储的,结合我们上面提到的栗子是不是可以做到改变呢?
改变String
计算机科学领域的任何问题都可以通过增加一个中间层来解决,那么对于java,通过反射往往可以做到一些普通方法做不到的事。通过反射获取这个value[]数组,然后改变它不就可以了吗。
public static void main(String args[]) throws Exception {
String str = "hello";
change(str);
System.out.println(str);
}
private static void change(String str) throws Exception {
// 1.获取域
Field field = String.class.getDeclaredField("value");
// 2.value是私有域,设置为可访问
field.setAccessible(true);
// 3.找到对应值
char[] value = (char[]) field.get(str);
value[1] = 'a';
}
输出结果:
hallo
很显然,通过反射可以破坏 String 的不可变性。
总结
Java的传参方式是值传递,这个是函数调用时参数的求值策略,与参数是基本类型还是引用类型无关。另外,不可变类的设计在使用过程中不容易出错,天生的线程安全,当然也应该避免不可变设计的滥用。