类加载机制&面试题分析

https://blog.csdn.net/x1032019725/article/details/81301614

JVM的类加载

我们平时在编译java代码之后,会生成.class文件——字节码文件,JVM会执行字节码文件,JVM执行字节码文件的过程为下面7个阶段
1. 加载
2. 验证
3. 准备
4. 解析
5. 初始化
6. 使用
7. 卸载

加载

简单的说,就是把字节码文件加载到内存中

验证

当JVM加载完Class字节码文件,并在方法区创建对应的Class对象之后,JVM会启动对改字节码流的校验,只有符合JVM字节码规范的文件才能被JVM正确执行,校验过程主要为如下:

  1. JVM规范校验:JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理等
  2. 代码逻辑校验:JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。比如传参、返参是否正确等

准备

当完成字节码的校验之后,jvm为类变量分配内存以及初始化,但是有下面的点需要注意

内存分配的对象

java中的变量有2种类型,分别为类变量类成员变量
- 类变量:被static修饰的变量
- 类成员变量:除了类变量的其他变量

在准备阶段,jvm只会为类变量分配内存,而不会为类成员变量分配内存,到初始化阶段才会为类成员变量分配内存

初始化的类型

在准备阶段,jvm会为类变量分配内存,并为其初始化,但是这里的初始化和我们平时理解的初始化不太一样,这里的初始化是指为变量赋其类型在java中的零值,而不是用户代码里面的初始值,但是如果一个变量被static final修饰的话,则会被赋予用户所希望的值

示例:

//在准备阶段,jvm会为其赋值为0,而不是1
public static int test = 1;

//在准备阶段,jvm会为其赋值1
public static final int test2 = 1;

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

为什么同样是被static修饰,但是结果不一样,我们可以这么理解,在java中,被final修饰代表不可变,如果被static fianl修饰,在准备阶段被赋值为0,这样和java里面final的约定不一致,但是如果是赋值为1,那么就很合乎情理。没有被final修饰的变量,其可能在初始化阶段或者运行阶段发生一系列变化,所以就没有必要在准备阶段赋予用户所要的值。

解析

当通过准备阶段之后,jvm 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

初始化

初始化阶段,用户定义的Java代码才真正开始执行,jvm会根据语句的执行顺序对类对象初始化,下面几种情况会触发初始化

  • jvm遇到new、getstatic、putstatic、invokestatic这4条字节码指令,如果类没有初始化过,就会先初始化,生成这几条指令一般是如下场景: - new:使用new关键字实例化对象

  • getstatic/putstatic:读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)
  • invokestatic调用一个类的静态方法
  • 使用java.lang.reflect对类进行反射调用的时候,如果类没初始化要先初始化

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 使用

    jvm初始化完之后,就会从方法入口,执行用户的代码

    卸载

    当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存

    总结

    上面对jvm的类加载机制进行了一个大概的了解,知道类加载机制之后可能你会问有什么用,我们平时用不到呀,虽然我平时也用不到这些,但是既然是学习java开发的,我觉得有必要把这些东西了解一下,毕竟我们每天都在用java,怎么能不去了解一下他是怎么运行的呢,除此之外,面试的出场也是非常高的

    例题1

    请看下面的题目,下面代码输出的结果是什么?

    
    public class Student {
    
        static int age = 22;
    
        public Student(){
            System.out.println("无参构造方法 age = "+ age);
        }
    
        {
    
            System.out.println("普通代码块");
        }
    
        static {
            System.out.println("静态代码块");
        }
    
        public static void main(String[] args) {
           Student student = new Student(); System.out.println("main方法");
        }
    }
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    例题1分析

    输出结果如下

    静态代码块
    普通代码块
    无参构造方法 age = 22
    main方法
      
      
    • 1
    • 2
    • 3
    • 4

    当我们的程序要执行main方法的时候,要先对该类进行初始化,在上面的类中有两个方法,一个构造方法和一个main方法,在java类编译成字节码文件的时候,字节码中只有类初始方法和对象初始化方法这两个概念,我们可以根据以下来区分

    1. 类初始化方法:顾名思义,类初始化方法会在类初始化的时候执行,IDE会按顺序手机变量的赋值语句,静态代码块,组合成类初始化方法

    如上面的Student类,下面两个语句将组成类初始化方法

     static int age = 22;
     static {
            System.out.println("静态代码块");
        }
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 对象初始化方法:在对象实例化的时候才会执行,IDE会按顺序手机成员变量的赋值语句,普通代码块,最后收集构造函数代码组成对象初始化方法

    例题2

    
    package jvmlearn.basetype;
    
    
    class A {
        static {
            System.out.println("A的静态块");
        }
    }
    
    class B extends A {
        public static int  bbb = 10;
        static {
            System.out.println("B的静态块");
        }
        public  B(){
            System.out.println("B的构造方法");
        }
    }
    
    class  C extends B{
        static {
            System.out.println("C的静态代码块");
        }
        public C(){
            System.out.println("C的构造方法");
        }
    }
    
    public class LoadDemo {
        public static void main(String[] args) {
            System.out.println("B的参数bbb="+C.bbb);
        }
    }
    
    
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    例题2分析

    输出结果如下

    A的静态块
    B的静态块
    B的参数bbb=10
      
      
    • 1
    • 2
    • 3

    你可能会疑惑为什么C的静态代码块这句话没输出,这里要注明一下,jvm在加载类的过程中,对于静态字段只有直接定义这个字段的类才会被初始化,而不会触发子类的初始化(执行类初始化方法)

    具体过程可以描述如下:
    1. main方法调用C.bbb
    2. bbb在B类定义,初始化B类,不初始化C类
    3. 初始化B类的时候发现A类没有被初始化,去初始化A类
    4. 初始化A类,输出A的静态代码块
    5. 初始化完A类后,继续初始化B类,输出B的静态代码块
    6. 执行main方法,输出B的参数bbb=10

    例题3

    package jvmlearn.basetype;
    
    
    class A {
        static {
            System.out.println("A的静态块");
        }
        public A(){
            System.out.println("A的构造方法");
        }
    }
    
    class B extends A {
        public static int  bbb = 10;
        static {
            System.out.println("B的静态块");
        }
        public  B(){
            System.out.println("B的构造方法");
        }
    }
    
    class  C extends B{
        static {
            System.out.println("C的静态代码块");
        }
        public C(){
            System.out.println("C的构造方法");
        }
    }
    
    public class LoadDemo {
        public static void main(String[] args) {
           new C();
        }
    }
    
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    例题3分析

    结果输出

    A的静态块
    B的静态块
    C的静态代码块
    A的构造方法
    B的构造方法
    C的构造方法
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    加载过程可以描述为如下:
    1. 执行main方法,构造C对象的实例
    2. 初始化C,发现B没初始化,去初始化B
    3. 初始化B,发现A没初始化,去初始化A
    4. 输出A的静态块
    5. A初始化完成,初始化B,输出B的静态块
    6. B初始化完成,初始化C,输出C的静态代码块
    7. 执行C的构造方法,调用父类B的构造方法
    8. 执行B的构造方法,调用A的构造方法
    9. 执行A的构造方法,调用object的构造方法,然后输出A的构造方法
    10. 执行B的构造方法,输出B的构造方法
    11. 执行C的构造方法,输出C的构造方法

    例题4

    
    class A {
    
    
       static A a = new A();
    
       static {
           System.out.println("A的静态块");
       }
    
        {
            System.out.println("A的普通代码块");
        }
        public  A(){
            System.out.println("A的构造方法");
            System.out.println("aaa = "+aaa +" ; bbb = "+bbb);
        }
        public static void staticMethod(){
            System.out.println("A的静态方法");
        }
        int aaa = 1;
        static int bbb = 2;
    
    
    }
    
    public class LoadDemo {
        public static void main(String[] args) {
            A.staticMethod();
        }
    }
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    例题4分析

    输出结果

    A的普通代码块
    A的构造方法
    aaa = 1 ; bbb = 0
    A的静态块
    A的静态方法
      
      
    • 1
    • 2
    • 3
    • 4
    • 5

    过程分析

    这一题我们可以这么来分析,上面我们说过,java虚拟机会把字节码解析成初始化方法和实例化方法,本例中的初始化方法和示例方法分别如下

    • 类初始化方法
    
        static A a = new A();
        static {
           System.out.println("A的静态块");
        }
        static int bbb = 2;  
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 类实例化方法
    
       {
            System.out.println("A的普通代码块");
        }
        int aaa = 1;
        public  A(){//构造方法在最后
            System.out.println("A的构造方法");
            System.out.println("aaa = "+aaa +" ; bbb = "+bbb);
        }
    
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    执行步骤

    1. 入口是main方法,对A类进行了实例化
    2. 初始化A类,执行类初始化方法
    3. 类初始化第一句为static A a = new A(),A被实例化,jvm会去执行A的实例化方法
    4. 执行普通代码块,输出A的普通代码块
    5. 初始化aaa = 1
    6. 执行A的构造方法,输出A的构造方法,bbb的值在准备阶段初始化为0,aaa被初始化为1,输出aaa = 1 ; bbb = 0;
    7. 实例化方法执行完毕,继续执行类初始化方法
    8. 类初始化方法继续往下执行,下一行为执行静态代码块,输出A的静态代码块
    9. 然后为bbb赋值2
    10. 类的初始化完成,执行main中的方法,输出A的静态方法

    例题5

    
    
    class X{
        Y y=new Y();
        public X(){
            System.out.print("X");
        }
    }
    class Y{
        public Y(){
            System.out.print("Y");
        }
    }
    public class Z extends X{
        Y y=new Y();
        public Z(){
            System.out.print("Z");
        }
        public static void main(String[] args) {
            new Z();
        }
    }
    
    
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    例题5分析

    输出结果

    YXYZ
      
      
    • 1

    过程分析

    执行步骤

    1. 入口是main方法,对Z类进行了实例化
    2. 初始化Z类,执行类的初始化方法,发现X方法没有初始化,去执行X初始化方法,本例子中没有使用static修饰符,所以类初始化方法可以看成是空
    3. 由于main用的是new方法,会对Z的父类进行构造
    4. 执行X的实例化方法,先执行Y y=new Y();,输出Y
    5. 执行X的构造函数,输出X
    6. 执行Z的实例化方法,先执行Y y=new Y();,输出Y
    7. 再执行Z的构造方法,输出Z
    8. 把输出结果合成,最后输出字符串为YXYZ

    例题总结

    从上面的例子可以看出类的执行顺序如下

    1. 确定类变量的初始值:在类初始化的时候,在准备阶段jvm会为类变量(static修饰的变量)初始化,如对象会被初始化为null,int被初始化为0
    2. 找到入口执行:找到main方法,开始执行,在执行之前会对main方法所在类进行初始化
    3. 执行类初始化方法:jvm 会按顺序收集类变量的赋值语句、静态代码块,组成类初始化方法去执行
    4. 执行对象实例化方法:JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,组成对象实例化方法去执行

    猜你喜欢

    转载自blog.csdn.net/qq_32534441/article/details/89591788