我们都知道,Java的类分为加载,链接,初始化三个阶段。
1.加载:将Class文件的字节码加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生产一个代表这个类的java.lang.class对象,该对象作为方法区的类的访问入口。
2.链接:将Java类的二进制代码合并到JVM 运行状态之中。
验证:确保加载的类信息符合规范,没有安全方面的问题。
准备:正式为类变量(static)分配内存并设置其的初始值(注意,这里的初始值不是我们编写程序的时候写的初始值,如“static int m=100;”,而是java默认的值,如一个“int”类型的值,其初始值为0),并且为final常量赋值。
解析:虚拟机常量池(下面有解释)内的符号引用(变量名)替换为直接引用(内存)。
3.初始化:由编译期自动收集的所有类变量的赋值动作和静态代码块中的语句合并生成。(子类的初始化会调用父类的初始化)
可能有人不太了解常量池的概念,这边引用一位大佬写的
https://blog.csdn.net/chenkaibsw/article/details/80848069
- 运行时常量池:方法区的一部分,存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储到运行时常量池中。运行时常量池具备动态性,也就是并非预置入Class文件的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中。
- 字符串常量池:本质是一个HashSet,这是一个纯运行时的结构,而且是惰性维护的。注意它只存储String对象的引用,而不存储String对象的内容,根据这个引用可以得到具体的String对象。
- Class常量池:主要存放两大类常量:字面量和符号引用。加载Class文件时,Class文件中String对象会进入字符串常量池(这里的进入是指 放入字符串的引用,字符串本身还是在堆中),别的大都会进入运行时常量池。
字面量比较接近Java语言层面常量的概念,如文本字符串、声明为final的常量值
符号引用属于编译原理的概念:
类和接口的全定限名
字段的名称和描述符
方法的名称和描述符
符号引用将在解析阶段被替换为直接引用。因为Java代码在进行编译时,并不像C那样有"连接"这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,Class文件不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期间 转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
下面是类在何时会初始化的一个测试代码
package ClassLoader_;
public class Loader_ {
static{
System.out.println("Main被初始化了");
}
public static void main(String[] args) throws ClassNotFoundException {
//会产生类的引用(初始化)
//1.主动引用
Son son = new Son();
//Main被初始化了
// Father被初始化了
// Son被初始化了
// 2.反射
Class c1=Class.forName("ClassLoader_.Son");
// Main被初始化了
// Father被初始化了
// Son被初始化了
// 不会产生类的引用
// 1.用子类调用父类的静态变量
System.out.println(Son.b);
// Main被初始化了
// Father被初始化了
// 2
// 2.声明类数组
Son[] sons= new Son[5];
// Main被初始化了
// 3.调用常量
System.out.println(Son.M);
// Main被初始化了
// 1
//原因:常量在常量在链接阶段赋值,static在链接阶段会赋初始值(static的初始值并不是100,而是0),另m=100是在类的初始化阶段做的
}
}
class Father{
static{
System.out.println("Father被初始化了");
}
static int b=2;
}
class Son extends Father{
static{
System.out.println("Son被初始化了");
m=200;
}//在初始化时候运行静态代码块
static int m=100;
static final int M=1; //常量在链接阶段赋值,static在链接阶段会赋初始值,比如在这边m初始值为0
}