常量池主要涉及到常量池里的内容和常量池解析这两块。
在方法区中,每个类型都对应一个常量池,常量池中存储了如:文字字符串,final变量值,类名,方法名等常量。jvm把常量池组织为入口列表的形式,可通过索引来访问常量池中的各个入口,每个常量池入口的第一个字节都是个标志,用这个标志来表示该入口中存储的常量类型,如:CONSTANT_Long表示里面存储的是Long类型的字面值,CONSTANT_Class_Info表示里面存的是某个Class的类型信息(存的可能是个普通的字符串,然后经过常量池解析,则变成指向某个类的引用)。
除了字面常量值以外,常量池还可以容纳其他几种符号引用:类和接口的权限定名,字段名称和描述符,方法名称和描述符。
类和接口的权限定名指的是当前类的权限定名。
字段名称指的是类或接口的实例变量或类变量,字段的描述符是一个指示字段的类型的字符串。如在一个类中有一下形式的生命:A a = null,则a为字段名,A为字段描述符。
方法的描述符也是个字符串,该字符串指示了方法的返回值和参数的数量,顺序,类型。
在运行时,jvm从常量池中获得符号引用,然后在运行时解析成引用项的实际地址,最后通过常量池中的权限定名,方法,字段描述符把当前类或接口中的代码与其他类或接口中的代码联系起来。
常量池解析:常量池解析是非常复杂的,它分多种情况处理。这里,就举个简单的例子,了解常量池解析的大概过程。
public class Animal {
String id = "Animal" ;
public void print () {
System.out .println("this is animal" +id );
}
}
public class Test {
public static void main (String[] args) {
Animal a = new Animal();
}
}
jvm处理Test类中的main()方法的第一条指令过程:
要运行Test程序,jvm找到对应的Test.class文件,并读入到方法区中,通过保存在方法区中的字节码,jvm开始执行main方法,执行时,它会一直指向当前类(Test类)的常量池。main的第一条指令告诉jvm要为常量池中的第一项分配足够的内存,于是jvm使用Test常量池指针找到第一项,发现它是一个对Animal类的符号引用(也就是对字符串“Animal”的引用),于是jvm检查方法区,看Animal类是否被装载了。
如果jvm发现还没被装载过Animal类,它开始查找并装载Animal.class文件。
最后,jvm将一个直接指向方法区Animal类数据的指针来代替常量池第一项(也就是那个字符串“Animal”),以后,则可以通过该指针来快速访问Animal类了,这个就称为常量池解析。
常量池解析在这里所做的处理就是把常量池中的字符串“Animal”解析成一个对类Animal的引用。
String str1 = "abc" ;
String str2 = "abc" ;
String str3 = new String("abc" );
String str4 = new String("abc" );
System.out .println("str1 == str2 ? :" + (str1 == str2)); //true
System.out .println("str2 == str3 ? :" + (str2 == str3)); //false
System.out .println("str3 == str4 ? :" + (str3 == str4)); //false
System.out .println("str3.equals(str4) ? :" + str3.equals(str4)); //true
1.str1 == str2 这个表达式的比较过程,"=="是一个逻辑运算符,对于基础数据类型比较的是两个值是否相等,对于引用数据类型则比较的是两个引用变量所指向在堆内存中的地址值是否一致。String类型是引用数据类型,但是String是一个特殊的引用数据类型,我们在定义一个String类型的变量的时候,在赋值的过程中,Java虚拟机首先会到字符串常量池中检索所要赋的值是否在字符串常量池中存在,如果存在则将该值所在的字符串常量池中的地址赋予该引用变量,如果不存在则在字符串常量池中分配一个内存来存储所赋的值然后将该值所在的地址赋予引用变量。所以在定义str1变量的时候,JVM在字符串常量池中分配了内存给"abc"这个值空间然后将这个值的地址给予str1这个引用变量,而str2在定义的时候JVM检索字符串常量池中存在了"abc"这个值同样将该值的地址给予了str2,所以str1 == str2 的值为true。
2.str2 == str3 这个表达式的比较过程,上面我们说了"=="逻辑运算符,比较String类型的变量比较的是地址,那么有人会问,不是说"abc"这个值已经在字符串常量池中存在了么,可是为什么结果会是false呢?那么我们要说明一下对于String类型变量在定义的时候的方式了,对于String name = "" 这种方式也是最常用的定义方式,该方式的定义过程就是在字符串常量池中分配内存空间并返回值的地址,而String name = new String();这种定义方式则完全不同,使用new关键字定义的对象所存储的空间是在堆内存中而非在常量池中,它的过程是,首先Java虚拟机会在字符串常量池中检索是否存在该值得内存空间,若不存在则先分配空间给该值,若存在则将该值(不是内存地址)复制一份到堆内存中,然后将该值在堆内存中的地址赋予引用变量str3。这样就会导致str2 == str3的值为false。
3.str3 == str4 的值为false,通过2的解释我想大家可以明白str3所引用的对象和str4所引用的对象在堆内存中的地址是不一致的,所有是false。
4.str3.equals(str4)的值为true,也许有的人会问了,equals()这个方法比较的不也是地址么?我的回答很简单,大家可以看一下Object源码一切就会明白了,
public boolean equals(Object obj) { return (this == obj); }
很简单的代码段,要注意用红色框注的部分,这个方法返回的表达式用的是"==",那么上面已经说过了,"=="逻辑运算符在引用变量中比较的是内存中的地址,这样回答可能又会问了,可是答案是true啊?那么我回答你的是,我们比较的是String类型的变量而非是Object类型的变量。Object是Java中所有类(包括自定义类)的超类,所有的类都继承了Object类的元素String类也不例外,但是不同的时候String类对equals()方法进行了重写
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = count; if (n == anotherString.count) { char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n-- != 0) { if (v1[i++] != v2[j++]) return false; } return true; } } return false; }