从JVM 内部看String 类型的问题

    关于String 类型,在面试题或者实际编程中都会经常遇到,有很多的网友也曾做过大量的分析。在看完深入Java 虚拟机这本书后,对JVM 处理Java 程序的流程有了一个大概的认识,所以总结一下。

   下面的分析从我们遇到的一些问题实例进行,我觉得这样是最好理解的。

 

1、String 类型对象的生成

     

String s=new String("zhxing");
String s1="zhxing";
Object o=new Object();

    下面来对比下这三条代码反编译后生成的字节码:

 

 (1)

   0:   new     #2; //class java/lang/String
   3:   dup
   4:   ldc     #3; //String zhxing
   6:   invokespecial   #4; //Method java/lang/String."<init>":(Ljava/lang/Strin
g;)V
   9:   astore_1

 

(2)

   10:  ldc     #3; //String zhxing
   12:  astore_2

 (3)

   13:  new     #5; //class java/lang/Object
   16:  dup
   17:  invokespecial   #1; //Method java/lang/Object."<init>":()V
   20:  astore_3

 

   涉及到得指令集的解析:

new指令格式:

   new indexbyte1,indexbyte2

 

执行过程:

   要执行new指令,Jvm通过计算(indextype1<<8)|indextype2生成一个指向常量池的无符号16位索引。然后JVM根据计算出的索引查找JVM常量池入口。该索引所指向的常量池入口必须为CONSTANT_Class_info。如果该入口尚不存在,那么JVM将解析这个常量池入口,该入口类型必须是类。JVM从堆中为新对象映像分配足够大的空间,并将对象的实例变量设为默认值。最后JVM将指向新对象的引用objectref压入操作数栈。

 

dup指令格式:

  dup

 

执行过程:

  要执行dup指令,JVM复制了操作数栈顶部一个字长的内容,然后再将复制内容压入栈。本指令能够从操作数栈顶部复制任何单位字长的值。但绝对不要使用它来复制操作数栈顶部任何两个字长(long型或double型)中的一个字长。上面例中,即复制引用objectref,这时在操作数栈存在2个引用。

 

ldc指令格式:

  ldc,index

 

执行过程:

   要执行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的JVM常量池入口,JVM将会查找CONSTANT_Integer_info,CONSTANT_Float_info和CONSTANT_String_info入口。如果还没有这些入口,JVM会解析它们。而对于上面的haha,JVM会找到CONSTANT_String_info入口,同时,将把指向被拘留String对象(由解析该入口的进程产生)的引用压入操作数栈。

 

invokespecial指令格式:
  invokespecial,indextype1,indextype2

 

执行过程:
  对于该类而言,该指令是用来进行实例初始化方法的调用。上面例子中,即通过其中一个引用调用String类的构造器,初始化对象实例,让另一个相同的引用指向这个被初始化的对象实例,然后前一个引用弹出操作数栈。

 

astore_1指令格式:(astore_2、astore_3类似只是局部变量不同
  astore_1

 

astore_1指令过程:
  要执行astore_1指令,JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1。

 

JVM常量池:

   虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM常量池在内存当中是以表的形式存在的,特别要注意的是对于String类型,JVM维护着一张表用来存储文字字符串值(被拘留的字符串值)。--这里有一点不明白的是:到底这张表是保存String 引用还是直接保存String 值(像int 常量池是直接保存值的),这点还不明确。规范上也没有说明,如有大虾知道的话,麻烦短信告诉下。

 

总结:

    看了上面的介绍后,可以知道,String 类型在编码中字面上的对象,是会进行查询String 常量池表的(如果没有就创建一个String 对象,然后再保存在表中)。而new String() 生成的对象是不会查询String 常量池表的,它不管表中是否存在,都会创建一个新的String 对象,所以内存地址是肯定不同的。而如果调用String 方法中的intern()方法,则会在String常量池表中查找,然后把该表中的引用返回。

 

这里有几篇String 相关的文章值得一看的 :

http://blog.csdn.net/ZangXT/archive/2009/08/05/4410246.aspx

http://www.iteye.com/topic/522167

 

 

2、String 类型参数的传递

   记得有篇文章是相关的Java-String类型的参数传递问题 ,(在一年前我还转载过)。

   该文章中的有几点我不大认同:

(1)String的存储实际上通过char[]来实现的。
(2)String就相当于是char[]的包装类。包装类的特质之一就是在对其值进行操作时会体现出其对应的基本类型的性质。

  

  看了JVM规范可以知道,数组在JVM 是一个对象来的,如果改变数组,对其操作是会改变它的值的。所以这和String 作为参数传递不会改变它的值是没有关系的。具体测试看下面:

public class Test {
	public static void main(String[] args) {
		char[] c={'b','a'};
		test(c);
		System.out.println(c[0]);
	}
	
	public static void test(char[] c){
		c[0]='a';
	}

}

 

   那为什么String 作为一个对象引用传递,而不会改变它的值。这是因为String 中成员变量,也就是保存值的char[] 数组是final 类型,而且没有提供任何关于它的修改的方法,当然没办法修改到String 的引用的内容啦。看下面模拟String 的例子:

public class Test {
	public static void main(String[] args) {
		StringTemp s=new StringTemp("bbbb");
		test2(s);
		System.out.println(s);
	}
	public static void test2(StringTemp s1){
		s1=new StringTemp("zhxing");
	}
}
//模拟String类型
public final class StringTemp {
	private final char[] value;
	public StringTemp(String s){
		value=s.toCharArray();
	}
	@Override
	public String toString() {
		return new String(value).toString();
	}
	

}

   在main 方法的s 和test2 方法的参数s1 都只是指向new StringTemp("bbbb"); 这个的引用,当s1 改变时(指向了另外的一个引用new StringTemp("zhxing");),是不会影响到s 的引用的。同理String 类型也是一样,是不会改变的。

  

 

  对于String 类型就了解到这里,不知道还有没相关的应用问题,如果有提出再来补上。。感觉自己写文章讲解有点犯晕,呵呵,文笔不好吧。。

你可能感兴趣的:(jvm,C++,c,虚拟机,面试)