java中Class类装载流程是怎样的

发布时间:2021-09-27 17:50 来源:亿速云 阅读:0 作者:柒染 栏目: 开发技术

java中Class类装载流程是怎样的,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

java虚拟机加载class文件过程如下:

1 类装载的条件

class文件只有在必须要使用的时候才会被装载,java虚拟机不会无条件地装载class类型。java虚拟机规定,一个类或接口在初次使用前,必须进行初始化。这里的“使用”是指主动使用,主动使用只有下列几种情况:

  • 当创建一个类的实例时,比如使用new关键字或反射、克隆、反序列化。

  • 当调用类的静态方法时,使用了字节码invokestatic指令。

  • 当使用类或接口的静态字段(final常量除外),比如使用getstatic或者putstatic指令。

  • 当使用java.lang.reflect包中的方法反射类的方法时。

  • 当初始化子类时,要求先初始化父类。

  • 作为启动虚拟机,含有main()方法的那个类。

例1:主动使用
public class Parent {
    static {
        System.out.println("Parent init");
    }
    public static int v = 100;
}

public class Child extends Parent {
    static {
        System.out.println("Child init");
    }
}

public class Demo01 {
    public static void main(String[] args) {
        Child c = new Child();
    }
}

以上代码声明了3个类:ParentChildDemo01ChildParent的子类。若Parent被初始化,由代码中的static语句块可知,将会打印“Parent Init”,若Child被初始化,则会打印“Child Init”,执行Demo01,运行结果:

Parent init
Child init

由此可知,系统首先装载Parent类,接着装载Child类。

例2:被动使用
public class Parent {
    static {
        System.out.println("Parent init");
    }
    public static int v = 100;
}

public class Child extends Parent {
    static {
        System.out.println("Child init");
    }
}

public class Demo02 {
    public static void main(String[] args) {
        System.out.println(Child.v);
    }
}

查看以上代码,Parent中有静态变量v,并且在Demo02中,使用其子类Child调用父类中的变量。运行以上代码,输出结果如下:

Parent init
100

可以看到,虽然在Demo02中直接访问了子类对象,但是Child子类并未被初始化,只有Parent父类被初始化。可见,在使用一个字段时,只有直接定义该字段的类才会被初始化。

注意:虽然Child类没有被初始化,但是此时Child类已经被系统加载,只是没有进入初始化阶段。

使用-XX:+TraceClassLoading参数运行这段代码,就会得到以下日志(删除部分输出):

oaded java.net.Inet6Address from /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.Inet6Address$Inet6AddressHolder from /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded jvm.chapter10.Parent from file:/Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/]
[Loaded jvm.chapter10.Child from file:/Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/]
Parent init
100

2 加载类

加载类处于类装载的第一个阶段。在加载类时,java虚拟机必须完成以下工作:

  • 通过类的全名获取类的二进制数据

  • 解析类的二进制数据流为方法区内的数据结构

  • 创建java.lang.Class类的实例,表示该类型

对于类的二进制数据流,虚拟机可能通过多种途径产生或获得,如:class文件,或者jar、zip等归档数据包,也可以通过网络加载。

在获取类的二进制数据后,java虚拟机就会处理这些数据,并最终转为一个java.lang.Class类提供的实例。

3 验证类

当类加载到系统后,就开始连续操作,验证是连续操作的第一步。它的目的是保存加载的字节码是合法、合理并且符合规范的。验证的步骤比较复杂,实际要验证的项目也繁多,大体上java虚拟机需要做的检查如下:

4 准备

当一个类验证难过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。java虚拟机各类型变量默认值的初始值如下:

注意:java并不支持boolean类型,对于boolean类型,内部实现实际上是int类型,由于int类型的默认值是0,故对应的boolean类型的默认值就是false.

如果类存在常量字段(final修饰的字段),那么常量字段也会在准备阶段被赋值上正确的值,这个赋值属于java虚拟机的行为,属于变量的初始化。

5 解析类

准备阶段后是解析阶段。解析阶段的工作就是将类、接口、字段和方法的符号引用转为直接引用。

符号引用就是一些字面量的引用,和虚拟机的内部数据结构与内存布局无关。在Class类文件中,在大量的符号引用,但在运行时,只有符号引用是不够的,系统需要明确变量、方法等的位置。以方法为例,java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法时,只要知道这个方法在方法表中的偏移量就可以直接调用。通过解析操作,符号引用可以转变为目录方法在类的方法表中的位置,从而使得方法被成功调用。

综上所述,所谓解析,就是将符号引用转为直接引用,也就是得到类或者字段、方法在内存中的指针或者偏移量

6 初始化

类的初始化是类装载的最后一个阶段,如果前面的步骤都没有问题,表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。初始化阶段的重要工作是执行类的初始化方法 <clinit>。方法<clinit>是由编译器自动生成的,它是由静态成员的赋值语句及static语句块共同产生的。

值得一提的是,对于<clinit>方法,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全。也就是说,当多个线程试图初始化同一个类时,只有一个线程可以进入<clinit>方法了(当需要使用这个类时,虚拟机就会直接返回它已经准备好的信息)。

正因为<clinit>方法是带锁线程安全的,所以在多线程环境下进行类初始化时,可能会引起死锁,并且这种死锁是很难发现,因为看起来他们并没有可用的锁信息。

下面的代码展示了在类的初始化时,产生了死锁线程:

package jvm.chapter10;

class StaticA {
    static {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {

        }
        try {
            Class.forName("jvm.chapter10.StaticB");
        } catch (ClassNotFoundException e) {

        }
        System.out.println("StaticA init OK!");
    }
}

class StaticB {
    static {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {

        }
        try {
            Class.forName("jvm.chapter10.StaticA");
        } catch (Exception e) {

        }
        System.out.println("StaticB init OK");
    }
}

public class Demo03 extends Thread {

    private char flag;

    public Demo03(char flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            Class.forName("jvm.chapter10.Static" + flag);
        } catch (Exception e) {

        }
        System.out.println(getName() + "over");
    }

    public static void main(String[] args) throws Exception {
        Demo03 loadA = new Demo03('A');
        Demo03 loadB = new Demo03('B');
        loadA.start();
        loadB.start();
        loadA.join();
        loadB.join();
    }
}

以上代码由3个类组成:StaticAStaticBDemo03.在Demo03中创建了两个线程,线程A试图初始化StaticA线程B尝试初始化StaticB,在StaticA的初始化过程中,会尝试初始化StaticB,同样在StaticB的初始化过程中,也初始化 StaticA,这就导致了死锁。

通过线程堆栈dump可以得到如下信息:

"Thread-1" #12 prio=5 os_prio=31 tid=0x00007f9dd4853000 nid=0x5703 in Object.wait() [0x0000700004797000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at jvm.chapter10.StaticB.<clinit>(Demo03.java:33)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at jvm.chapter10.Demo03.run(Demo03.java:52)

   Locked ownable synchronizers:
        - None

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9dd4801800 nid=0xa803 in Object.wait() [0x0000700004694000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at jvm.chapter10.StaticA.<clinit>(Demo03.java:17)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at jvm.chapter10.Demo03.run(Demo03.java:52)

   Locked ownable synchronizers:
        - None

在这种情况下,系统并没有给出足够的信息来判定死锁,但是死锁确实存在,我们需要格外小心由类的初始化引起的死锁问题。

免责声明:本站发布的内容(图片、视频和文字)以原创、来自互联网转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系QQ:712375056 进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。