类的加载过程
类加载主要分为5个过程:加载、验证、准备、解析、初始化,其中验证、准备、解析统称为连接过程。
加载分为3个阶段:
1、通过类的全限定名获取此类的二进制字节流。
2、将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
验证阶段
验证阶段分成4个验证过程:
1、文件格式验证:class文件格式验证
2、元数据验证:字节码语义分析,分析数据类型,内容是否正确
3、字节码验证:检验方法是否正常
4、符号引用验证:类的常量池中的各种符号引用的信息的匹配性验证
准备阶段
为类的静态变量分配内存并设置初始值。
解析阶段
Java虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化阶段
就是执行类构造器<clinit>()方法的过程。所谓<clinit>()方法的过程就是用户在类中定义的常量值赋值、静态代码块执行过程。在准备阶段已经对常量值设置初始值,在这里就是对常量设置用户定义的值,比如在类中存在如下一行代码:
public static final int i = 10;
在准备阶段是令i=0,而在初始化阶段则是令i=10的过程。这个过程也是静态代码块的执行过程。
1.类什么时候才被初始化
a.创建类的实例,new对象时
b.调用类的静态方法时
c.访问或赋值静态变量时
d.初始化子类时,会先初始化其父类
e.jvm启动时标明的启动类
f.反射
2.类的初始化顺序
a.没有父类时:先初始化静态变量--->静态代码块--->实例变量--->初始化块--->构造器
b.有父类时: 先初始化父类静态变量--->父类静态代码块--->子类静态变量--->子类静态代码块,然后是父类实例变量--->初始化块--->构造器,最后才是子类的。
例外:
当静态变量被final修饰后,变为常量放入常量池,调用该变量时不会初始化类
通过class数组创建对象,也不会被初始化,如Parent[] parents = new Parent[10]
package com.wjj.initclass;
public class NotinitByFinal {
public static void main(String[] args) {
// 常量在编译阶段会存入调用类的常量池,本质上没有没用用到定义常量的类,也就不需要初始化此类
System.out.println(ConstClass.Hello);//输出hello world,从常量池取,不会初始化ConstClass类
}
}
class ConstClass{
static {
System.out.println("const init");
}
public static final String Hello = "hello,world";
}
package com.wjj.initclass;
public class Notinit {
public static void main(String[] args) {
// 通过子类调用父类静态变量,只初始化父类,不初始化子类
System.out.println(Son.value);
// 通过数组定义引用,不触发此类的初始化
Parent[] parents = new Parent[10];
}
}
class Parent{
static {
System.out.println("superClass init");
}
public static int value = 123;
}
class Son extends Parent{
static {
System.out.println("subclass init");
}
}
最后
这几个过程不全是按照以上固定的流程进行的,比如验证阶段的符号引用验证则是在解析阶段中发生。
通过这5个过程,类被使用的准备工作就完成了,接下来才可以直接对类进行使用,比如使用类的静态常量、new一个类的实例等操作。
介绍完类的加载过程,下面我们来简单介绍一下常量池。
常量池常用的有三种:字符串常量池、class常量池和运行时常量池。
class常量池(Class Constant Pool)
每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
每个class文件都有一个class常量池。
可以看到在方法区里的class文件信息包括:
魔数:确定这个class文件能否被虚拟机接受
版本号:保证编译正常运行
常量池:存放字面量和符号引用
访问标志:识别 class 为类还是接口以及一些修饰符,如 public,abstract,final等
类索引和父索引:类索引用来确定这个类的全限定类名,父索引确定继承的父类的全限定类型(java为单继承),除了java.lang.Object 外,所有java类的夫父索引均不为0.
接口索引集合:存放这个类实现的多个接口信息。
字段表集合:存放接口或类中声明的变量,不包括方法内部的局部变量
方法表集合:存放类中的方法
属性表集合:在class文件,字段表,方法表中都可以携带自己的属性表集合
编译与反编译查看 class 文件
javac xxx.java
javap -v xxx.class
运行时常量池( Runntime Constant Pool)
当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?
jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,运行时常量池每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,以保证运行时常量池所引用的字符串与全局字符串常量池中所引用的是一致的。
字符串常量池(String Constant Pool)
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。
详细的字符串常量值介绍可以看看这篇文章:
https://blog.csdn.net/qq_36625806/article/details/105011550