总结:《JVM虚拟机类加载过程与类初始化顺序》
总结:《JVM虚拟机类加载过程与类初始化顺序》
一·触发类加载与类初始化的条件操作:
-
第一次创建类的实例(第一次使用
new
关键字)。 -
第一次访问类的静态变量或者为静态变量赋值(但 final 常量除外,它们在编译期就已经被赋值了)。
-
第一次调用类的静态方法。
-
第一次使用反射 API 对类进行反射调用。
-
第一次初始化一个类的子类(会首先初始化父类)。
注意:
(1)若代码中没有显式调用某个Java类加载器去加载该类,则在触发该类初始化时,jvm底层会自动找到该类适应的类加载器,进行相应类加载与类初始化
(2)类加载过程一般都只会在第一次引用该类的时候触发,若已经加载过该类到虚拟机内存里面,即使满足上面条件操作也不会再次执行类加载
(3)类初始化是类加载的最后阶段部分,却也是两个独立的执行过程。即,可以实现只加载类到内存但不初始化类,但要初始化类必须先加载类到内存里面
(4)如果该类有父类,则类加载器会先去查找其父类,且依次类推直到最顶级的类,然后再顺序往下执行每个类的类加载具体过程
(5)一个类加载过程是包括加载、验证、准备、解析、初始化这五个小阶段
(6)即,只有等父类的类加载5个阶段执行完,才会执行子类的类加载5个阶段
二·类加载实际分为五个阶段:
1. 加载(Loading):
当 JVM 首次遇到需要使用某个类的时候,它会通过类加载器(ClassLoader)找到该类的 .class
文件。类加载器负责从指定的位置(如文件系统、网络、jar 包等)读取字节码数据到内存里面。
注意:
(1)此阶段会将类中的静态方法、非静态方法都会被加载到内存方法区中进行存储,方便后续调用,什么时候调用则什么时候触发方法操作
2. 验证(Verification):
加载完字节码后,JVM 会对字节码进行验证,确保其符合 Java 虚拟机规范,没有安全方面的问题。验证包括语法校验、元数据验证、字节码验证和符号引用验证等步骤。
3. 准备(Preparation):
在这个阶段,JVM 会为类的静态变量分配内存,并将其初始化为默认值,这个阶段又称隐式初始化。例如,对于整型静态变量,默认值为0;对于对象引用类型的静态变量,默认值为 null
。
注意:
(1)此阶段只会对静态变量进行内存空间分配以及赋默认值;非静态变量则是在调用构造方法,触发对象实例化时,进行内存空间分配与赋默认值
(2)若是final修饰的静态变量,则此阶段只会对该静态变量分配内存空间,但不会赋默认值;会在下面的显示初始化阶段进行一次性赋值。
final修饰的变量只能被赋一次值,且必须在显示初始化结束之前。
4. 解析(Resolution):
解析阶段主要是将常量池中的符号引用转换为直接引用。符号引用是对类、接口、字段和方法的抽象表示,而直接引用是内存中实际的地址或偏移量。
5. 初始化(Initialization):
这个阶段又被称为显示初始化,如果该类还没有被初始化,那么 JVM 会在适当的时候触发类的初始化(参考上文触发类初始化操作)。类的初始化主要包括以下步骤:
- 调用类的
<clinit>
方法(由编译器自动生成,用于初始化静态变量和执行静态初始化块的代码)。
注意:
(1)连续多次触发同一个类的初始化操作,jvm底层只会在第一次类加载进行类初始化,后面就不会再进行类初始化了
(2)静态变量、静态代码块,会按照它们在类中的声明顺序依次执行
(3)此阶段也只会对静态方法、静态变量、静态代码块进行操作;非静态的各种属性显式初始化不在此阶段,而是在后续调用类构造方法,对象实例化阶段进行显式初始化
(4)虽然静态变量在准备阶段已经进行隐式初始化,但是由于jvm机制原因,静态代码块中只能引用定义在其之前的静态变量;
定义在其之后的静态变量,该静态代码块中只可以对该静态变量进行赋值,却不能进行非赋值操作,否则就会报 "java: 非法前向引用" 异常。
(5)这个机制有助于提高代码的可读性和可维护性,因为在阅读代码时,开发人员可以信任变量在使用之前已经被正确地初始化。
这有助于减少代码中的错误,并使代码更容易理解和调试。
三·当一个子类具有父类时,类加载子类时,执行步骤顺序如下:
- 父类静态成员隐式初始化
- 父类静态成员显示初始化
- 子类静态成员隐式初始化
- 子类静态成员显示初始化
- 其他子孙类以此类推
四·触发类对象实例化的条件操作:
- 通过new关键字调用一个类的构造方法,创建类对象实例
五·类对象实例化分为三个阶段:
1. 隐式初始化:
非静态成员变量按照它们在类中的声明顺序,进行内存空间分配以及自动赋予变量类型对应的默认值
2. 显示初始化:
非静态成员变量、普通代码块,会按照它们在类中的声明顺序依次执行
注意:
(1)虽然非静态成员变量在前面已经进行隐式初始化,但是由于jvm机制原因,普通代码块中只能引用定义在其之前的非静态变量;
定义在其之后的非静态变量,该普通代码块中只可以对该成员变量进行赋值,却不能进行非赋值操作,否则就会报 "java: 非法前向引用" 异常。
(2)这个机制有助于提高代码的可读性和可维护性,因为在阅读代码时,开发人员可以信任变量在使用之前已经被正确地初始化。
这有助于减少代码中的错误,并使代码更容易理解和调试。
(3)类对象实例化过程中完全可以调用类的各种静态成员,因为类静态成员已经完成类初始化了
3. 构造方法:
执行实际构造方法的代码逻辑
注意:
(1)虽然在源代码逻辑中是直接new一个对象,看似直接调用了某个类的构造方法,但是从调用这个构造方法开始,jvm底层会判断该类是否初次加载,
否就会先触发类加载以及类初始化(有父类先加载且初始化父类),接着再执行对象实例化(有父类先实例化父类),最后才会执行真正的构造方法代码逻辑。
(2)类初始化与类对象实例化是两个完全独立的过程,可以分开触发,但是类初始化必须在类对象实例化之前进行
(3)当然也可以在类初始化的过程中,手动触发对象实例化,这样是可以的但是不符合规范,不建议这么做
六·当一个子类具有父类,且子类父类都已经执行过类加载,则实例化子类对象时,执行步骤顺序如下:
- 父类非静态成员隐式初始化
- 父类非静态成员显示初始化
- 父类构造方法
- 子类非静态成员隐式初始化
- 子类非静态成员显示初始化
- 子类构造方法
- 其他子孙类依次类推
七·当一个子类具有父类时,且子类父类都没执行过类加载,则实例化子类对象时,完整执行步骤如下:
- 父类静态成员隐式初始化
- 父类静态成员显示初始化
- 子类静态成员隐式初始化
- 子类静态成员显示初始化
- 调用父类构造器(执行父类实例成员初始化):实际执行下面三个顺序步骤
1. 父类非静态成员隐式初始化
2. 父类非静态成员显示初始化
3. 父类构造方法
- 调用子类构造器(执行子类实例成员初始化):实际执行下面三个顺序步骤
1. 子类非静态成员隐式初始化
5. 子类非静态成员显示初始化
6. 子类构造方法
八·演示上述执行逻辑过程的完整代码:
注意:可以试着注释其中某些代码来测试各种情况,建议读者多多体会这两个示例代码的执行结果,从而验证上诉逻辑执行顺序是否正确
1.Father类
/**
* @Description TODO
* <p>
* Copyright @ 2022 Shanghai Mise Co. Ltd.
* All right reserved.
* <p>
* @Author LiuMingFu
* @Date 2024/1/3 09:47
*/
public class Father {
public static void main(String[] args) {
System.out.println("父类静态main方法");
Father father = new Father();
System.out.println("weight=" + father.getWeight());
System.out.println("===================================再次触发对象实例化过程===================================");
Father father2 = new Father();
System.out.println("===================================再次触发对象实例化过程===================================");
Father father3 = new Father();
}
public Father() {
System.out.println("父类构造方法!");
}
String job;
{
System.out.println("父类普通代码块-1,job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
job = getJob();
}
static int age;
static String name;
static {
// Father father = new Father();
System.out.println("父类静态代码块-1,age=" + age + ",name=" + name);
age = 20;
name = "张三";
}
Double weight = 99.0d;
{
System.out.println("父类普通代码块-2,weight=" + weight + ",job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
weight = getWeight();
}
static String sex = getSex();
static {
System.out.println("父类静态代码块-2,sex=" + sex + "age=" + age + ",name=" + name);
}
static Double height = getHeight();
static {
System.out.println("父类静态代码块-3,height=" + height + "sex=" + sex + "age=" + age + ",name=" + name);
}
public static String getSex() {
System.out.println("父类静态方法-getSex");
return "男";
}
public static Double getHeight() {
System.out.println("父类静态方法-geHeight");
return 170.0d;
}
public String getJob() {
System.out.println("父类普通方法-getJob");
return "Java工程师";
}
public Double getWeight() {
System.out.println("父类普通方法-getWeight");
return new Double("120.5");
}
}
2.Son类
/**
* @Description TODO
* <p>
* Copyright @ 2022 Shanghai Mise Co. Ltd.
* All right reserved.
* <p>
* @Author LiuMingFu
* @Date 2024/1/3 09:56
*/
public class Son extends Father{
public static void main(String[] args) {
System.out.println("子类静态main方法");
Son father = new Son();
System.out.println("weight=" + father.getWeight());
System.out.println("===================================再次触发对象实例化过程===================================");
Son father2 = new Son();
System.out.println("===================================再次触发对象实例化过程===================================");
Son father3 = new Son();
}
public Son() {
System.out.println("子类构造方法!");
}
String job;
{
System.out.println("子类普通代码块-1,job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
job = getJob();
}
static int age;
static String name;
static {
System.out.println("=========================start-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================");
Father father = new Father();
Son son = new Son();
System.out.println("=========================end-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================");
System.out.println("子类静态代码块-1,age=" + age + ",name=" + name);
age = 20;
name = "李四";
}
Double weight = 99.0d;
{
System.out.println("子类普通代码块-2,weight=" + weight + ",job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
weight = getWeight();
}
static String sex = getSex();
static {
System.out.println("子类静态代码块-2,sex=" + sex + "age=" + age + ",name=" + name);
}
static Double height = getHeight();
static {
System.out.println("子类静态代码块-3,height=" + height + "sex=" + sex + "age=" + age + ",name=" + name);
}
public static String getSex() {
System.out.println("子类静态方法-getSex");
return "女";
}
public static Double getHeight() {
System.out.println("子类静态方法-geHeight");
return 100.0d;
}
public String getJob() {
System.out.println("子类普通方法-getJob");
return "Python工程师";
}
public Double getWeight() {
System.out.println("子类普通方法-getWeight");
return new Double("120.5");
}
}
3.执行Son类的main方法结果:
父类静态代码块-1,age=0,name=null
父类静态方法-getSex
父类静态代码块-2,sex=男age=20,name=张三
父类静态方法-geHeight
父类静态代码块-3,height=170.0sex=男age=20,name=张三
=========================start-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
父类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Java工程师,name=张三,age=20,sex=男,height=170.0
父类普通方法-getWeight
父类构造方法!
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=null,age=0,sex=null,height=null
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=null,age=0,sex=null,height=null
子类普通方法-getWeight
子类构造方法!
=========================end-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================
子类静态代码块-1,age=0,name=null
子类静态方法-getSex
子类静态代码块-2,sex=女age=20,name=李四
子类静态方法-geHeight
子类静态代码块-3,height=100.0sex=女age=20,name=李四
子类静态main方法
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=李四,age=20,sex=女,height=100.0
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=李四,age=20,sex=女,height=100.0
子类普通方法-getWeight
子类构造方法!
子类普通方法-getWeight
weight=120.5
===================================再次触发对象实例化过程===================================
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=李四,age=20,sex=女,height=100.0
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=李四,age=20,sex=女,height=100.0
子类普通方法-getWeight
子类构造方法!
===================================再次触发对象实例化过程===================================
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=李四,age=20,sex=女,height=100.0
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=李四,age=20,sex=女,height=100.0
子类普通方法-getWeight
子类构造方法!
注意:可以试着注释其中某些代码来测试各种情况,建议读者多多体会这两个示例代码的执行结果,从而验证上诉逻辑执行顺序是否正确
九·什么时候jvm会进行class卸载?
- 类不再被引用:当一个类的所有实例都变为不可达(即没有任何活的对象引用该类或者其任何静态成员),并且该类的类加载器也可以被垃圾收集时,JVM可能会卸载这个类。
- 类加载器卸载:与类加载器的生命周期相关。每个类都与其加载它的类加载器关联。当类加载器被卸载时,它所加载的所有类也会被卸载。
- 堆内存压力:在运行过程中,如果JVM的堆内存压力增大,为了释放内存,JVM的垃圾收集器可能会更积极地查找并卸载不再使用的类。
- 明确调用System.gc():虽然不推荐,但调用System.gc()会触发垃圾收集,这可能包括类卸载的过程。然而,这并不保证一定会发生类卸载,因为具体的垃圾收集策略和行为取决于JVM实现。
注意:JVM的类卸载是一个复杂的过程,涉及到可达性分析、垃圾收集以及类加载器的管理。而且,类卸载并不频繁发生,因为大多数应用程序在运行时都会稳定在一组核心类上,这些类通常不会被卸载。此外,JVM设计时也考虑到性能和稳定性,因此它可能会保守地处理类卸载,以避免不必要的开销和风险。
十·参考文献:
1.Java非法向前引用变量
https://blog.csdn.net/hsz2568952354/article/details/97496917
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!