JVM常量池

1 篇文章 0 订阅
1 篇文章 0 订阅

在了解JVM常量池前,我们先来看看JVM。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:
 
JVM的每个实例都有一个它自己的方法域和一个堆,运行于JVM内的所有的线程都共享这些区域;当虚拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和 Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态;本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。
Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。
一个JVM实例只存在于一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:
Permanent Space 永久存储区
Young Generation Space 新生区
Tenure generation space养老区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
而新生区又分为伊甸区(Eden space)和幸存者区(Survivor pace)。所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸区的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到幸存1区。那如果1区也满了呢?再移动到养老区。
常量池是方法区的一部分。常量池(constant_pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量和符号引用。运行时常量池是方法区的一部分。
在class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受,再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号,再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个2个字节无符号类型的数据(constant_pool_count)存储常量池容量计数值。常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
类和接口的全限定名
字段名称和描述符
方法名称和描述符
Java中八种基本类型的包装类的大部分都实现了常量池技术,它们是Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类(Float、Double)则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用对象池。看下面代码:
使用javap查看生成的字节码:javap -verbose Test
minor version:次版本号  (minor:二流的)
major version:主版本号  (major:主修)
  bipush指令意思是将单字节的常量值(-128~127)推送到栈顶
sipush指令意思是将短型的常量值(-32768~32767)推送到栈顶
invokestatic指令意思是调用静态方法。


其它封装类如下:
Integer num1=10;
Integer num2=10;
Integer num3=128;
Integer num4=128;


System.out.println(num1==num2); //输出true
System.out.println(num3==num4); //输出false


Boolean bool1=true;
Boolean bool2=true;
System.out.println(bool1==bool2); //输出true


//浮点类型的包装类没有实现常量池技术
Double d1=1.0;
Double d2=1.0;
System.out.println(d1==d2); //输出false 
     
String s =  new  String( "xyz" );  在运行时涉及几个String实例?
答案是:如果是第一次运行则应该是两个,一个是字符串字面量"xyz"所对应的驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"xyz"相同的实例。


String中的final用法和理解:
final只对引用的"值"(即内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。
final StringBuffer a = new StringBuffer("yc");
final StringBuffer b = new StringBuffer("ycInfo");
a=b;//此句编译不通过 


final StringBuffer sbf = new StringBuffer("yc");
sbf.append("info");//编译通过  


案例1:
String str1 = "yc1";
String str2 = "yc" + 1;
System.out.println( str1==str2 ); //true


String str3 = "ycinfo";
String str4 = "yc" + "info";
System.out.println( str3==str4 ); //true
String str5 = "yc3.14";
String str6 = "yc" + 3.14;
System.out.println( str5==str6 ); //true    
JVM对于字符串常量的"+"连接符在程序编译期就会将常量字符串的"+"连接优化为连接后的值,拿"yc" + 1来说,经编译器优化后在class中就已经是yc1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。


 案例2:
String str1 = "ycinfo";
String str2="yc";
String str3 = str2+"info";


System.out.println( str1==str3 ); //false   
JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即str2+"info"  无法被编译器优化,只有在程序运行期,来动态分配并将连接后的新地址赋给str3。所以上面程序的结果也就为false。


案例3:
String str1 = "ycinfo";
final String str2="yc";
String str3 = str2+"info";


System.out.println( str1==str3 ); //true
和上面唯一不同的是"yc"字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的str2 + "info"和"yc" + "info"效果是一样的。故上面程序的结果为true。


 案例4:
public static void main(String[] args) {
String str1 = "ycinfo";
final String info= getInfo();
String str2 = "yc" + info;
System.out.println( str1==str2 ); //false
}


private static String getInfo() {
return "info";
JVM对于字符串引用info,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"yc"来动态连接并分配地址为str2,故上面程序的结果为false。
 
通过上面4个例子可以得出得知:
String str1 = "yc" + "in" + "fo";  等价于
String str2 = "ycinfo";
int num1 = 1+2+3; 等价于
int num2 = 6;


由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因了,形如这样的代码:
public class Test {
    public static void main(String args[]) {
        String str = null;
        for(int i = 0; i < 100; i++) {
            str += "yc";
        }
    }
每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。 如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行 append操作。
 
String.intern()分析
Java语言并不要求常量一定只能在编译期产生,运行时也可能将新的常量放入常量池中,这种特性用的最多的就是String.intern()方法。
String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用。
String str1= "yc";
String str2=new String("yc");
String str3=new String("yc");


System.out.println( str1==str2 ); //false
str2.intern(); //intern:实习生  拘留 软禁
str3=str3.intern(); //把常量池中"yc"的引用赋给str3
System.out.println( str1==str2 ); //false 虽然执行了str2.intern(),但它的返回值没有赋给str2 
System.out.println( str1==str2.intern() ); //true 说明str2.intern()返回的是常量池中”yc”的引用 
System.out.println( str1==str3 ); //true 
有人说,使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中。如果我把他说的这个全局的String 表理解为常量池的话,他的最后一句话:如果在表中没有相同值的字符串,则将自己的地址注册到表中是错的:
String str1=new String("ycinfo");
String str2=str1.intern();
System.out.println( str1==str1.intern() ); //false
System.out.println(str1==str2); //fals
System.out.println( str2==str1.intern() ); //true
来源:http://chenzehe.iteye.com/blog/1727062

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值