本文主要介绍java种各种变量的加载方式与存储方法
包括:
1、static final修饰的常量;
2、final修饰的成员变量;
3、普通类变量
4、普通成员变量
一、static final修饰的常量,值为基础数据类型、String值或对象
class A{
static final int i1 = 101; /*static final String str1 = "abc"; */
static final String str1 = new String("abc"); /*static final String str1 = "abc"; */
static final float f1 = 101.0f;
public static void main(String args[]){
float f2 = f1+1.0f;
}
}
/* 查看静态常量池 */
Constant pool:
#1 = Methodref #8.#33 // java/lang/Object."<init>":()V
#2 = Class #34 // A
#3 = Float 102.0f
#4 = Class #35 // java/lang/String
#5 = String #36 // abc
#6 = Methodref #4.#37 // java/lang/String."<init>":(Ljava/lang/String;)V
#7 = Fieldref #2.#38 // A.str1:Ljava/lang/String;
#8 = Class #39 // java/lang/Object
#9 = Utf8 i1
#10 = Utf8 I
#11 = Utf8 ConstantValue
#12 = Integer 101
#13 = Utf8 str1
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 f1
#16 = Utf8 F
#17 = Float 101.0f
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 LA;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 f2
#30 = Utf8 <clinit>
#31 = Utf8 SourceFile
#32 = Utf8 A.java
#33 = NameAndType #18:#19 // "<init>":()V
#34 = Utf8 A
#35 = Utf8 java/lang/String
#36 = Utf8 abc
#37 = NameAndType #18:#40 // "<init>":(Ljava/lang/String;)V
#38 = NameAndType #13:#14 // str1:Ljava/lang/String;
#39 = Utf8 java/lang/Object
#40 = Utf8 (Ljava/lang/String;)V
/* 任何类变量和成员变量都必须在Field表中有对应的field_info */
static final int i1;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 101
static final java.lang.String str1;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
static final float f1;
descriptor: F
flags: ACC_STATIC, ACC_FINAL
ConstantValue: float 101.0f
1、final static常量赋值为基础数据类型或String值
final static修饰的变量如果值是基础数据类型或String值,前期编译后就会在字节码文件的字段表中为这个字段添加ConstantValue属性并将值也放在属性表中,可以通过上面javap反编译后看到字段表中所有字段以及有ConstantValue属性和值的字段。
那么常量是在类加载的哪一步被加载到运行时数据区的呢?我们可以倒推出来结果:
(1) 首先明确的就是,基础数据类型常量的值是在常量池中的 (从Constant pool可以很明显的看到),也就是这些基础数据类型的值存放在逻辑方法区内;
(2) 既然是在方法区内,那么一定是 类加载-准备阶段 进行加载的;
注意:准备阶段是对类变量进行加载的阶段,如果是ConstantValue标识则直接在方法区存入值。如果是对象则是在方法区生成引用,引用先指向默认值null,然后在初始化阶段再将引用的值初始化成正确的对象。
观察Javap反编译的结果,可以发现都没有i1、f1对应的CONSTANT_Field_info常量项,也证明了i1和f1并不是作为一个字段引用存在于常量池中,而是直接将值放在静态常量池中(加载后在运行时常量池内),地址映射给变量名,当用到的时候,直接从常量池获取即Float_info常量项即可,而不需要用getstatic Field_info来获取字段常量项(会有专门的分析);
2、final static常量赋值为对象
如果值是new出的对象,那么在前期编译的时候就无法确定对象真实的地址,所以给这种情况的常量赋值则会涉及到两个步骤:
(1) 所以会在类加载的链接 - 解析阶段进行符号引用转化为直接引用的操作;【通用过程,会将常量池中的所有符号引用类型全部替换为直接引用】
(2) 在类加载 - 初始化阶段,将引用str1真正指向一个堆区的对象[ new String(“abc”); ],从javap反编译结果的static{}中的相关指令可以确定我们的这个说法,如下:
static {}; /* static代码块是在 类加载-初始化 阶段用来给字段赋值的,除了ConstantValue的基础数据类型,其他的类变量都是在static{}中真正初始化值的 */
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #4 // class java/lang/String /*创建一个对象,并将其引用值压入栈顶*/
3: dup /*复制栈顶数值并将复制值压入栈顶*/
4: ldc #5 // String abc /* 将int, float或String型常量值从常量池中推送至栈顶,#5就是"utf8=abc" */
6: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V /* 调用超类构造方法,实例初始化方法,私有方法 */
9: putstatic #7 // Field str1:Ljava/lang/String; /* 为指定的类的静态域赋值,就是把栈顶new出来的对象赋值给#7字段 */
12: return /* 从当前方法返回void */
注意:java并不是说一个方法生成一个栈帧,其实准确来说是一个{ }对应一个栈帧,这也是为什么变量作用域会有很严格的区域的原因。所以static{ }本身也是一个独立栈帧的;
二、static修饰的类便俩个,值为基础数据类型或对象
class A{
static int i1 = 101; /*static final String str1 = "abc"; */
static String str1 = new String("abc"); /*static final String str1 = "abc"; */
static float f1 = 101.0f;
public static void main(String args[]){
float f2 = f1+1.0f;
}
}
/* 静态常量池 */
Constant pool:
#1 = Methodref #10.#32 // java/lang/Object."<init>":()V
#2 = Fieldref #9.#33 // A.f1:F
#3 = Fieldref #9.#34 // A.i1:I
#4 = Class #35 // java/lang/String
#5 = String #36 // abc
#6 = Methodref #4.#37 // java/lang/String."<init>":(Ljava/lang/String;)V
#7 = Fieldref #9.#38 // A.str1:Ljava/lang/String;
#8 = Float 101.0f
#9 = Class #39 // A
#10 = Class #40 // java/lang/Object
#11 = Utf8 i1
#12 = Utf8 I
#13 = Utf8 str1
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 f1
#16 = Utf8 F
#17 = Utf8 <init>
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 LocalVariableTable
#22 = Utf8 this
#23 = Utf8 LA;
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 f2
#29 = Utf8 <clinit>
#30 = Utf8 SourceFile
#31 = Utf8 A.java
#32 = NameAndType #17:#18 // "<init>":()V
#33 = NameAndType #15:#16 // f1:F
#34 = NameAndType #11:#12 // i1:I
#35 = Utf8 java/lang/String
#36 = Utf8 abc
#37 = NameAndType #17:#41 // "<init>":(Ljava/lang/String;)V
#38 = NameAndType #13:#14 // str1:Ljava/lang/String;
#39 = Utf8 A
#40 = Utf8 java/lang/Object
#41 = Utf8 (Ljava/lang/String;)V
/* 字段表 */
static int i1;
descriptor: I
flags: ACC_STATIC
static java.lang.String str1;
descriptor: Ljava/lang/String;
flags: ACC_STATIC
static float f1;
descriptor: F
flags: ACC_STATIC
/* static代码块 */
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: bipush 101
2: putstatic #3 // Field i1:I
5: new #4 // class java/lang/String
8: dup
9: ldc #5 // String abc
11: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
14: putstatic #7 // Field str1:Ljava/lang/String;
17: ldc #8 // float 101.0f
19: putstatic #2 // Field f1:F
22: return
1、static修饰类变量赋值为基础数据类型
先对比一下static赋值为基础数据类型和static final赋值基础数据类型的区别:
(1) int类型常量会在常量池中生成对应的Integer常量项,而单独的类变量不会;
(2) static final修饰的常量不需要在静态常量池中声明Field_info的,而类变量则需要有对应的Field_info;
(3) 常量不需要初始化阶段,而类变量需要;
根据上面三点的区别,我们则可以对比和猜测出类变量的加载过程以及存放方式:
1、类变量也是在方法区内,但是不位于