`
Javcoder
  • 浏览: 43408 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Java解惑之Final的理解

 
阅读更多

1)代码

public class T2 {
	static final int i = 1;
	static final int j = i * 2;
	static final int k;

	static {
		k = 3;
	}

	final int l = 4;
	final int m;
	final int n;

	{
		m = 5;
	}

	public T2() {
		n = 6;
	}

	public static void main(String[] args) {
		System.out.println(T2.i);
		System.out.println(T2.j);
		System.out.println(T2.k);

		T2 t2 = new T2();
		System.out.println(t2.l);
		System.out.println(t2.m);
		System.out.println(t2.n);
	}
}

2)分析

2.1)Javap查看字节码:

 

<clinit>方法:

 

  <init>方法:

 

main方法:

 

2.2)现象分析

  • 以上代码分为三个部分,分别为:类常量的定义及赋值,实例常量的定义及赋值,以及用于测试的main方法。
  • 类常量的赋值有两种方式:定义常量的同时直接赋值,静态代码块赋值。
  • 实例常量的赋值有三种方式:定义常量的同时直接赋值,非静态代码块赋值,以及构造方法赋值。
  • 类常量和实例常量各自又可分为编译阶段可知常量和编译阶段不可知常量。
  • 编译阶段可知常量为:定义常量的同时直接赋值,并且等号右侧的赋值部分不具有可执行性(例如,直接赋值为数字、字符串等,而不能是random方法),在编译期间,编译器直接把所有引用该常量的地方直接替换为该常量对应的值,等价于符号替换。
  • 编译阶段不可知常量为:如果是定义常量的同时直接赋值,等号右侧的部分要具有可执行性(例如,double d = Math.random()),除此之外的其他情况都为编译阶段不可知常量,从本质上来讲,通过代码执行来完成赋值操作的常量都是编译阶段不可知常量。
  • 由<clinit>方法的字节码截图可知,编译阶段可知类常量不会像类变量一样在对应的Method Area区分配内存以存放常量值。
  • 由<clinit>方法的字节码截图可知,在对类常量k进行赋值操作时(第1行),由于使用的是putstatic字节码指令,所以说明jvm会为类常量k在Method Area区分配内存。
  • 由<init>方法的字节码截图可知,在对实例常量l 进行赋值时(第1行),由于使用的是putfield字节码指令,所以说明jvm会为实例常量l在堆中分配内存。
  • 由<init>方法的字节码截图可知,实例常量(不管是编译阶段可知常量还是编译阶段不可知常量)的赋值是在类实例化阶段中的初始化时期完成的。
  • 由main方法的字节码截图可知,编译器在对引用编译阶段可知常量的类进行编译时,会将遇到的这种常量用其对应的常量值进行替换(第3行、第10行、第39行)。

3)总结

3.1)常量的分类及特点:

  • 常量分为类常量和实例常量,而类常量和实例常量又都可进一步细分为编译期可知常量和编译期不可知常量。
  • 对于编译期不可知常量(和变量类似),由于在编译阶段,编译器无法确定该常量的常量值,所以编译器在对引用这些常量的类进行编译时,不会对遇到的这些常量其进行值替换,编译过后,这些地方持有的还是对这种常量的字段符号引用,具体描述如下:当编译器在编译一个类时,如果该类引用了本类或其他类定义的一个编译期不可知常量,则编译器会首先在该类的常量池中放置一个CONSTANT_Fieldref_info条目,然后使编译后的字节码持有对该条目的引用。该条目中存放的并不是具体的常量值,它存放的是有关该常量值的描述,例如:该常量值的存放位置,jvm在对这个条目进行解析时,它会根据该条目中存放的信息,到相应类(定义该常量的类)的Method Area区(编译期不可知的类常量)或堆上分配的该类的实例(编译期不可知的实例常量)中去取相应的常量值,这一过程其实是和变量的解析过程一样的,即:先到常量池中找到对应的条目,然后再根据条目中的信息或是到Method Area中去查找,或是到堆上去查找,总之要分两步做完。
  • 对于编译期可知常量,不管其是类常量还是实例常量,由于在编译阶段,编译器都可确定该常量的常量值,所以任何地方对这种常量的引用,编译器都会将其替换为该常量对应的常量值,具体描述如下: 当编译器在编译一个类时,如果该类引用了本类或其他类定义的一个编译期可知常量,则编译器在编译过程中,会将遇到的这种常量替换为该常量对应的常量值,而在做这种替换操作时,编译器又会根据常量类型和常量值范围做不同的处理,即:或是用字节码隐含表示该常量的值,或是将常量值跟在字节码后面,作为字节码流的一部分,或是在引用该常量的类的常量池中放置一个条目,在该条目中存放对应的常量值,字节码直接引用该条目。而不管做何种替换处理,替换的最终结果都是jvm不用再像上面那样,分两步去找这个常量值了,因为常量值要么隐含再字节码中,jvm看到字节码就可知道常量值,要么存在字节码流中,jvm可以直接读取使用,要么在本类的常量池中,jvm直接去该条目中取出该值使用,所以编译期可知常量的替换行为可以认为是编译器对字节码指令流的一种优化,因为不管是上面的哪种替换情况,它都使jvm经过最多一步的查找就可获得常量值。

3.2)常量的内存使用及赋值时期:

  • 编译期可知的类常量:常量值在编译阶段确定,不会像类变量一样为其在对应的Method Area区分配内存,没有赋值过程。
  • 编译期不可知的类常量:常量值在类的初始化阶段确定,会像类变量一样为其在对应的Method Area区分配内存(类准备阶段),常量的赋值过程发成在类的初始化阶段(调用<clinit>方法)。
  • 编译期可知的实例常量:常量值在编译阶段确定,会像实例变量一样为其在堆中分配内存,常量的赋值过程发成在类实例化阶段中的初始化时期(调用<init>方法)。
  • 编译期不可知的实例常量:常量值在类实例化阶段中的初始化时期确定,会像实例变量一样为其在堆中分配内存,常量的赋值过程发成在类实例化阶段中的初始化时期(调用<init>方法)。

4)备注

 

  • 类的生命周期:编译、加载、链接(验证、准备、解析)、初始化、实例化。
  • 链接中的准备阶段负责为类变量在对应的Method Area区分配内存,并根据类变量的类型为其赋初值。
  • 类的初始化阶段,jvm通过调用编译器自动生成的<clinit>方法,来完成类变量的初始化。
  • 类的实例化过程又可分为三个子时期,分别是:在堆上分配内存、根据实例变量的类型为其赋初值、调用编译器自动生成的<init>方法完成实例变量的初始化。

 

 

  • 大小: 6.7 KB
  • 大小: 28 KB
  • 大小: 113.6 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics