类的加载过程
(一)简述类加载过程:
类加载过程: JVM虚拟机把.class文件中类信息加载进内存
.class文件: 通过javac命令将java文件编译成字节码 ,此时生成的字节码文件称为.class文件
类加载的通俗举例: JVM在执行某段代码时,遇到了class A,此时内存中并没有class A的相关信息 ,JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中 。
注: JVM不是一开始就把所有的类都加载进内存中, 而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次
(二)Java虚拟机中类加载的全过程
参考:https://blog.csdn.net/ln152315/article/details/79223441
https://blog.csdn.net/qq_31156277/article/details/80188110
1)加载
1.1、将.class字节码通过类加载器加载到内存
.class字节码来源: 本地路径下编译生成的.class文件 ,从jar包中的.class文件 ,从远程网络,以及动态
代理实时编译。
类加载器的分类:启动类加载器、扩展类加载器 、应用类加载器 、用户的自定义类加载器 。
类加载的三个阶段:
1、通过一个类的全限定名 (例:java/lang/Thread格式,使用 / 相隔) 获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2)验证
确保 Class文件的字节流中包含的 信息符合jvm虚拟机的要求,并且不会危害虚拟机自身的安全
验证的动作:
2.1、文件格式的验证(基于二进制流进行)
验证字节流是否符合Class文件格式的规范 ,保证输入的字节流能正确地解析并存储于方法区之内
注: 验证是基于二进制字节流进行的 ,只有通过了这个阶段的验证后,字节流才能进入内存中的方法
区进行存储。
例:常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
2.2、元数据验证(基于方法区的存储结构 )
对字节码描述的信息进行语义分析 ,使其符合Java语言规范
例:该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
2.3、字节码验证(基于方法区的存储结构 )
对类的方法体进行校验分析 ,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
例:
2.4、符号引用验证(基于方法区的存储结构 )
校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)
是否可被当前类访问
注:验证与加载交替执行,单是加载顺序总是优于验证执行
3)准备
为类变量分配内存并设置类变量初始值 ,这些变量所使用的内存都将在方法区中进行分配
类变量:static修饰的变量
类变量初始值:public static int value=123; 在准备阶段value的初始值为0.而不是123
public static final int value=123;, 当被final修饰事物类变量 准备阶段的初始值为123
4)解析
将常量池内的符号引用替换为直接引用的过程 、
符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息
直接引用:可以理解为一个内存地址,或者一个偏移量
即把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用
解析内容:
- 类或接口
- 字段
- 类方法
- 接口方法
- 方法类型
- 方法句柄
- 调用点限定符
验证、准备、解析统称为连接
5)初始化
执行类构造器
对类变量初始化 ,执行类构造器 ,只对static修饰的变量或语句进行初始化
初始化的顺序:
(三)类的实例创建过程
参考:https://blog.csdn.net/qq_38537709/article/details/88750605
1.当创建子类实例时,先对父类的类变量(satic修饰的变量)和 static{}代码块进行初始化,再对子类的类变量(satic修饰的变量)和 static{}代码块进行初始化
2.对子父类完成初始化后调用构造器,子类的构造器第一行隐式的调用了父的空的构造器,在对父类构造器初始化
时先为成员变量分配内存空间,再对构造器进行初始化
3.成员变量的赋值优先于构造方法里的语句。
class C{
C() {
System.out.println("正执行SuperClass类的构造方法对成员变量初始化,为其成员变量C分配内存空间");
}
}
class SuperClass {
C c = new C();
static{
System.out.println("SuperClass类 正在初始化");
}
SuperClass() {
this("正在调用SuperClass的有参构造方法");
System.out.println("正在执行SuperClass的无参构造方法");
}
SuperClass(String s) {
System.out.println(s);
}
}
public class SubClass extends SuperClass{
C c = new C();
static{
System.out.println("SubClass类 正在初始化");
}
SubClass() {
/*在子类构造方法的第一句,隐式的调用父类的构造方法;*/
System.out.println("正在执行子类SubClass的构造方法");
}
public static void main(String[] args) {
new SubClass();
}
}
执行结果:
SuperClass类 正在初始化
SubClass类 正在初始化
正执行SuperClass类的构造方法对成员变量初始化,为其成员变量C分配内存空间
正在调用SuperClass的有参构造方法
正在执行SuperClass的无参构造方法
正执行SuperClass类的构造方法对成员变量初始化,为其成员变量C分配内存空间
正在执行子类SubClass的构造方法
当子类调用父类的静态字段时只有定义这个字段的类即父类才进行初始化,子类不初始化
父类
public class SuperClass {
public static int value;
public SuperClass() {
}
static {
System.out.println("SuperClass Init");
value = 123;
}
}
子类
public class SubClass extends SuperClass {
public SubClass() {
}
static {
System.out.println("SubClass Init");
}
}
测试:
public class Test {
public Test() {
}
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
结果:
SuperClass Init
123
使用类调用一个static final修饰的常量,该类不会进行初始化
public class ConstantClass {
static{
System.out.println("ConstantClass Init");
}
public static final String HELLOWOELD="hello world";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstantClass.HELLOWOELD);
}
}
输出结果:
hello world
当创建一个类类型的数组时,该类不会进行初始化
public class SupperClass {
static{
System.out.println("SuperClass Init");
}
public static int value=123;
}
public class SubClass {
public static void main(String[] args) {
SupperClass[] supperClasses = new SupperClass[10];
}
}
结果
无输出结果:
总结:
一个类加载过程包含5个过程
一):加载, 首先将编译过的字节码文件通过类加载器加载入内存,将类转为二进制流的形式,将静态时的存储结构转为 方法区运行时的额数据结构,并生成java.lang.class对象
二):连接,连接的过程主要包括验证、准备、解析三个步骤
验证的过程是检验二进制流是否符合java规范,是否对jvm产生伤害,验证分为文件格式验证(验证二进制流是否符合.class文件规范),元数据验证(对语义进行分析),字节码验证(对方法体进行校验分析),符号引用验检验是否能通过类名找到类对象,以及类的修饰符(private、public)
准备的过程是对类变量赋默认初值
解析的过程是将常量池内的符号引用替换为直接引用的过程(将间接引用转为直接引用; 即将变量名称转为对应的地址值或偏移量)
三):初始化,初始化是为类变量赋值+执行静态语句块 static{}的过程
不会引发类初始化的3种情况
1.子类引用父类的类变量,子类不会进行初始化,父类进行初始化
2.一个类引用本类的常量即 static fianl修饰的变量不会进行初始化
3.声明一个类类型数组时也不会进行初始化
子类进行实例化的过程
1.父类先初始化,子类再初始化(先给父类的类变量赋初值+执行静态块static{},再执行子类的)
2.先执行父类的构造方法,在执行子类的构造方法
3.在执行构造方法前先对类的成员变量进行赋值