美文网首页
四、JVM系列(类加载机制)

四、JVM系列(类加载机制)

作者: 大虾啊啊啊 | 来源:发表于2021-05-07 11:11 被阅读0次

    前言

    当编译器把Java源代码编译成字节码之后,虚拟机便可以把字节码读取进JVM内存,从而解析、运行等整个过程。最终执行得到我们想要的结果。这个过程我们叫做Java虚拟机的类加载机制。JVM执行class字节码的过程分为七个阶段:
    加载、验证、准备、解析、初始化、使用、卸载。

    一、加载

    这是类加载过程的第一个阶段。在这个阶段,JVM主要是将字节码从各个位置(磁盘,网络)转换成二进制字节流加载到内存中。接着会为这个类在JVM的方法区中创建对应的Class对象,这个Class对象就是这个类各种数据访问的入口。

    二、验证

    当JVM加载完Class字节码之并在方法区创建对应Class对象之后,JVM便会启动该字节码流的校验。只有符合JVM字节码规范的文件才能被JVM正确执行。这个校验大概分为几个类型:

    • JVM规范校验。JVM对字节码流文件格式的校验,比如:文件是否以0x cafe babe开头
    • 代码逻辑校验。JVM对代码组成的数据流和控制流校验。比如:一个方法的入参类型是Int,调用方法传入的是String。

    三、准备(重点)

    当字节码文件校验结束之后。JVM便会开始为类变量分配内存并初始化。
    这里需要注意两点:内存分配的对象以及初始化的类型。

    • 内存分配的对象
      Java中的变量有两种类型类变量和类成员变量。类变量指的是static修饰的变量、其他所有类型都是类成员变量。在准备阶段,JVM只会为类变量分配内存,而不会为类成员变量分配内存。
    public static int factor = 3;
    public String website = "www.cnblogs.com/chanshuyi";
    

    例如以上上代码,在准备阶段是为factor 分配内存,而不是为website 分配内存

    • 初始化类型
      在准备阶段,JVM会为类变量分配内存,并初始化类型。这里的初始化是为变量初始化该数据类型的零值。而不是用户代码里设置的值。例如
    public static int sector = 3;
    

    例如以上代码在准备阶段sector 的值是0,而不是3
    但是如果变量是由final修饰,则直接为其初始化代码初始化的值

    public static final int number = 3;
    

    以上例子。在准备阶段直接为number 初始化的值是3.

    四、解析(了解)

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

    五、初始化(重点)

    到了初始化阶段,用户定义的Java程序代码才真正的开始执行。在这个阶段JVM会根据语句的顺序对类对象进行初始化。一般来说当JVM遇到以下5种情况的时候会触发初始化。

    • 遇到new,getstatic,putstatic,invokestatic这四条字节码指令的时候,如果类没有进行过初始化,则先触发其初始化。生成这四条字节码指令的Java代码场景是:
      (1)new 一个对象,
      (2)读取或者设置一个static静态字段(除final修饰的变量除外,已经放在了常量池)、
      (3)调用静态方法
    • 使用java.lang.reflect 包的方法对类进行反射调用的时候
    • 当初始化一个类的时候,如果发现其父类没有初始化,则先初始化父类
    • 当虚拟机启动执行了main方法所在的类,如果该类没初始化,则触发其初始化

    六、使用

    当JVM初始化阶段完毕之后,JVM便从开始入口方法执行用户程序代码。

    七、卸载

    当用户程序代码执行完毕之后,JVM将销毁开始创建的class对象,最后负责运行的JVM也退出内存。
    通过以上的结论,我们来看看例子

    例子1

    public class Book {
        public static void main(String[] args)
        {
            System.out.println("Hello ShuYi.");
        }
    
        Book()
        {
            System.out.println("书的构造方法");
            System.out.println("price=" + price +",amount=" + amount);
        }
    
        {
            System.out.println("书的普通代码块");
        }
    
        int price = 110;
    
        static
        {
            System.out.println("书的静态代码块");
        }
    
        static int amount = 112;
    }
    

    ##################################

    此处给大家思考打印结果

    ################################

    书的静态代码块
    Hello ShuYi.
    

    在初始化阶段首先初始化main方法入口所在的类,初始化了Book类对象,执行类初始初始化方法

    static
        {
            System.out.println("书的静态代码块");
        }
    
        static int amount = 112;
    

    接着执行main方法中的代码

    System.out.println("Hello ShuYi.");
    

    因此得出

    书的静态代码块
    Hello ShuYi.
    

    但是上面我说了既然初始化了类对象,为什么Book的构造方法没有调用呢?因为我们确实没有进行Book类实例的初始化,如果要触发构造方法,则要调用new Book()

    例子2

    class Grandpa
    {
        static
        {
            System.out.println("爷爷在静态代码块");
        }
    }    
    class Father extends Grandpa
    {
        static
        {
            System.out.println("爸爸在静态代码块");
        }
    
        public static int factor = 25;
    
        public Father()
        {
            System.out.println("我是爸爸~");
        }
    }
    class Son extends Father
    {
        static 
        {
            System.out.println("儿子在静态代码块");
        }
    
        public Son()
        {
            System.out.println("我是儿子~");
        }
    }
    public class InitializationDemo
    {
        public static void main(String[] args)
        {
            System.out.println("爸爸的岁数:" + Son.factor);  //入口
        }
    }
    

    ##################################

    此处给大家思考打印结果

    ################################

    爷爷在静态代码块
    爸爸在静态代码块
    爸爸的岁数:25
    

    分析:
    首先初始化main所在的类InitializationDemo,执行main方法之后,调用Son.factor,此时Son继承了Father,Father继承了Grandpa。通过以上初始化分析的第四点,如果一个类要初始化的时候,发现其父类没初始化,要先初始化其父类。所以分别执行了

     System.out.println("爷爷在静态代码块");
    System.out.println("爸爸在静态代码块");
      public static int factor = 25;
    

    此时并没有初始化Son ,因为我们是通过Son调用了Father的静态变量,所以最终打印的是

     System.out.println("爷爷在静态代码块");
    System.out.println("爸爸在静态代码块");
     System.out.println("爸爸的岁数:" + Son.factor);  //入口
    

    例子3

    public class Book {
        public static void main(String[] args)
        {
            staticFunction();
        }
    
        static Book book = new Book();
    
        static
        {
            System.out.println("书的静态代码块");
        }
    
        {
            System.out.println("书的普通代码块");
        }
    
        Book()
        {
            System.out.println("书的构造方法");
            System.out.println("price=" + price +",amount=" + amount);
        }
    
        public static void staticFunction(){
            System.out.println("书的静态方法");
        }
    
        int price = 110;
        static int amount = 112;
    }
    

    ##################################

    此处给大家思考打印结果

    ################################

    书的普通代码块
    书的构造方法
    price=110,amount=0
    书的静态代码块
    书的静态方法
    

    解析:
    1、初始化main所在类Book
    2、接着按顺序初始化类变量,方法。执行了 static Book book = new Book();
    3、此时调用了new Book(),对Book实例进行初始化,
    4、Book实例进行初始化则触发了普通代码块,类成员变量

       {
            System.out.println("书的普通代码块");
        }
        int price = 110;
    

    接着执行构造方法

     System.out.println("书的构造方法");
            System.out.println("price=" + price +",amount=" + amount);
    

    5、接着往下执行类的静态代码块

       static
        {
            System.out.println("书的静态代码块");
        }
    

    6、然后执行main方法里的代码

     System.out.println("书的静态方法");
    

    所以最终

            System.out.println("书的普通代码块");
     System.out.println("书的构造方法");
            System.out.println("price=" + price +",amount=" + amount);
            System.out.println("书的静态代码块");
     System.out.println("书的静态方法");
    

    小结:在初始化阶段我们要记住,初始化类对象的时候,首先是执行类初始化的方法,静态代码块、静态变量。并且是按顺序执行,当调用了new 关键字才会初始化类对象。执行非静态的代码块,非静态变量。

    八、类加载器

    上面我们说到了类加载机制?当然类的加载是由谁来完成的呢?就是我们说的类加载器。对于任意一个类而言,都需要由它的类加载器和这个类本身来确定它在JVM中的唯一性。也就是说每一个类的加载都需要有对应的类加载器,如果两个类的加载器不同,即使它们是来自同一个字节码文件,这两个类就必不相等。(两个类的Class对象不equals)。

    Java类加载器可以分为三种:
    (1)启动类加载器(Bootstrap Class-Loader)
    加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar
    (2)扩展类加载器(Extension Class-Loader)
    加载 jre/lib/ext 包下面的 jar 文件
    (3)应用类加载器(Application Class-Loader)
    根据程序的类路径(classpath)来加载 Java 类。
    来看一个例子:

    package cn.enjoyedu.concurrent.cas;
    
    public class ClassLoaderTest {
    
        public static void main(String[] args) {
           ClassLoader loader =  ClassLoaderTest.class.getClassLoader();
            System.out.println(loader);
            System.out.println(loader.getParent());
            System.out.println(loader.getParent().getParent());
        }
    }
    
    
    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@1b6d3586
    null
    

    第一行打印结果是AppClassLoader,说明我们的ClassLoaderTest 类对应的类加载器是一个应用类加载器。
    通过getParent拿到上层的类加载器ExtClassLoader扩展类加载器。在通过ExtClassLoader扩展类加载器获取上层理论上应该拿到Bootstrap 启动类加载器,再该版本JDK中并没有拿到。但是不影响。

    双亲委派模型

    image.png

    以上我们说了JVM提供的三种类加载器,开发者也可以自定义加载器。他们的层次关系如上图所示,这种关系我们称之为双亲委派模型。如果一个类加载器收到类加载的请求,他会先托给上层类加载器去完成,上层又会托给上层,一直到达顶层的类加载器。如果上层的类加载器无法完成,自己才会去执行加载。
    双亲委派模型的好处是类的加载具备了带有优先的层次关系。这保证了JAVA程序的稳定运作。双亲委派模型保证了一个类被指定的类加载器加载。这样使得同一个字节码的类是相等的。

    相关文章

      网友评论

          本文标题:四、JVM系列(类加载机制)

          本文链接:https://www.haomeiwen.com/subject/johsdltx.html