Java 类加载分析

2021-02-08  本文已影响0人  雷涛赛文

一.类加载相关知识点

a. 类加载的本质

      将描述类的数据 从Class文件加载到内存 & 对数据进行校验、转换解析 和 初始化,最终形成可被虚拟机直接使用的Java使用类型,Class文件是一串二进制字节流。
      一旦程序运行,所有该类涉及的类(包括内部类和从其他包导入的类)都会在类加载的过程中加载到内存,因为在整个程序运行的过程中类加载只会发生一次,一旦某个类没有被加载,那么将不能再使用这个类。

b. 类加载过程

      分为五个步骤:加载 -> 验证 -> 准备 -> 解析 -> 初始化
      1、加载
      简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
      2、验证
      验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
      格式验证:验证是否符合class文件规范
      语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
      操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
      3、准备
      为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内),被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
      4、解析
      将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。可以认为是一些静态绑定的会被解析,动态绑定则只会在运行时进行解析;静态绑定包括一些final方法(不可以重写)、static方法(只会属于当前类),构造器(不会被重写)
      5、初始化
      将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
      所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。
      如果父类还没有被初始化,那么优先对父类初始化,但在<clinit>方法内部不会显示调用父类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的父类<clinit>方法已经被执行。
      JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

c. 类加载器

      启动类加载器
      扩展类加载器
      应用程序类加载器[自定义类加载器]


image.png
d.双亲委派模型 【流程代码实现在java.lang.ClassLoader的loadClass()中 】

      若一个类加载器收到了类加载请求,把 该类加载请求 委派给 父类加载器去完成,而不会自己去加载该类,每层的类加载器都是如此,因此所有的加载请求最终都应传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成该加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载。
      这种设计模式的好处在于:
      1、保证class只会被加载一次,也就是说类的数据结构只会在第一次创建的时候被加载进内存(方法区),以后要创建这个类的对象的时候,直接用方法区中的class在堆内存创建一个对象即可,这样的话创建对象就会比较快;
      2、保证系统类的安全性。因为在启动应用进程的时候就已经加载好了系统类(BootClassLoader),那后面运行期就不可能通过恶意伪造加载的方式去造成一些系统安全问题。

二.类初始化

      我们所说的加载,是指初始化,比如一个类在被使用(引用)时才会被加载(初始化),初始化的触发入口主要如下:当Java程序首次通过下面6种方式使用某个类或接口时,系统会初始化该类或接口。
      1. 创建类的实例。
      2. 访问类的静态变量,或者为静态变量赋值。
      3. 调用类的静态方法。
      4. 初始化某个类的子类。
      5. 直接使用java.exe命令来运行某个主类。
      6. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。

三.类加载顺序

      前面讲了好多理论知识,看的云里雾里的,带着以下问题看一下:
      a.静态代码块最先执行,是不是一个应用程序启动后,所有类中包含的静态代码块都会执行?
      b.静态内部类为什么不持有外部类的引用?非静态内部类为什么持有外部类的引用?
      c.外部类在加载时,是不是所有的内部类也被加载了?
      带着以上问题,我们来一起通过程序来实验一下:

package com.hly.classloader;
import android.util.Log;
public class OuterClass {
    public static long OUTER_DATE = System.currentTimeMillis();

    {
        Log.e("Seven", "外部类非静态块加载时间:" + System.currentTimeMillis());
    }

    static {
        Log.e("Seven","外部类静态块加载时间:" + System.currentTimeMillis());
    }

    public OuterClass() {
        Log.e("Seven","外部类构造函数时间:" + System.currentTimeMillis());
    }

    static class InnerStaticClass {
        public static long INNER_STATIC_DATE = System.currentTimeMillis();
        static {
            Log.e("Seven","静态内部类静态块加载时间:" + System.currentTimeMillis());
        }
        static long getOuterValue() {
            return OUTER_DATE;
        }
    }

    class InnerClass {
        public long INNER_DATE = 10;

        public InnerClass() {
            INNER_DATE = System.currentTimeMillis();
        }
    }
}

      以上可以看到,定义了一个类OuterClass,内部包含了静态变量,静态代码块,非静态代码块,构造方法,静态内部类,非静态内部类,通过以下步骤来进行验证:
      1.只创建OuterClass实例

private void handleCase(int num) {
    if (num == 1) {
        OuterClass oc = new OuterClass();
        Log.e("Seven", "Outer class value is: " + oc.OUTER_DATE);
    }
}

      首先程序在启动后,OuterClass类中的静态代码块没有执行,所以问题a已经验证:只有对类在进行初始化,即引用时,才会执行静态代码块,通过case 1就可以验证,直接看日志:

01-27 13:03:01.139 E/Seven   (15679): 外部类静态块加载时间:1611723781139
01-27 13:03:01.140 E/Seven   (15679): 外部类非静态块加载时间:1611723781140
01-27 13:03:01.140 E/Seven   (15679): 外部类构造函数时间:1611723781140
01-27 13:03:01.140 E/Seven   (15679): Outer class value is: 1611723781139

      通过以上可以看到,在创建OuterClass实例时,初始化的顺序为:静态代码块(静态变量)--->非静态代码块--->构造方法,没有加载静态及非静态内部类,问题c也验证了,在初始化外部类时,如果没有跟内部类有相关的逻辑,是不会初始化内部类的
      2.创建InnerClass实例

private void handleCase(int num) {
    if (num == 2) {
        OuterClass oc = new OuterClass();
        OuterClass.InnerClass ic = oc.new InnerClass();
        Log.e("Seven", "Inner class value is: " + ic.INNER_DATE);
    }
}

      非静态内部类不能单独创建,只能先创建外部类的实例,然后通过该实例来创建内部类,看一下日志:

01-27 13:09:41.078 E/Seven   (15898): 外部类静态块加载时间:1611724181078
01-27 13:09:41.078 E/Seven   (15898): 外部类非静态块加载时间:1611724181078
01-27 13:09:41.079 E/Seven   (15898): 外部类构造函数时间:1611724181079
01-27 13:09:41.079 E/Seven   (15898): Inner class value is: 1611724181079

      由于需要外部类的实例,所以外部类肯定初始化了,由于非静态内部类的创建需要依赖外部类的实例,所以持有外部类的引用。
      3.InnerStaticClass引用

private void handleCase(int num) {
    if (num == 3) {
        Log.e("Seven", "inner static class value is: " + OuterClass.InnerStaticClass.INNER_STATIC_DATE);
    }
}

      静态内部类的使用,直接通过外部类可以访问,看一下日志:

01-27 13:14:10.151 E/Seven   (16048): 静态内部类静态块加载时间:1611724450151
01-27 13:14:10.151 E/Seven   (16048): inner static class value is: 1611724450151

      通过以上可以看到,访问静态内部类的值时,只初始了静态内部类的静态代码块,外部类的代码块都没有执行,换句话说,静态内部类可以独立于外部类运行,也就是说静态内部类在创建后,外部类都没有实例化,所以不会持有外部类的引用。通过以上可以解答问题b
      但当静态内部类调用外部类的静态变量时,即外部类会被引用,那么在初始化静态内部类时会初始化外部类的静态及静态代码块:

private void handleCase(int num) {
    if (num == 3) {
        Log.e("Seven", "inner static class get outer value is: " + OuterClass.InnerStaticClass.getOuterValue());
    }
}

      日志为:

01-27 13:23:01.921 E/Seven   (16281): 静态内部类静态块加载时间:1611724981921
01-27 13:23:01.921 E/Seven   (16281): 外部类静态块加载时间:1611724981921
01-27 13:23:01.922 E/Seven   (16281): inner static class get outer value is: 1611724981921

      先加载静态内部类的静态代码块,后加载外部类的静态代码块及静态变量。

4.其他

      a.还有一种测试方式,就是本地写一个java类,在main()方法内进行实验,实例如下:

public class OuterClass {
    public static long OUTER_DATE = System.currentTimeMillis();

    {
       System.out.println("外部类非静态块加载时间:" + System.currentTimeMillis());
    }

    static {
        System.out.println("外部类静态块加载时间:" + System.currentTimeMillis());
    }

    public OuterClass() {
        System.out.println("外部类构造函数时间:" + System.currentTimeMillis());
    }

    static class InnerStaticClass {
        public static long INNER_STATIC_DATE = System.currentTimeMillis();
        static{
            System.out.println("静态内部类静态块加载时间:" + System.currentTimeMillis());
        }
    }

    class InnerClass {
        public long INNER_DATE = 0;
        public InnerClass() {
            INNER_DATE = System.currentTimeMillis();
        }
    }

    public static void main(String[] args) {
        System.out.println("==============main()=============");
        System.out.println("静态内部类静态变量加载时间:" + InnerStaticClass.INNER_STATIC_DATE);
    }
}

      执行javac生成class文件,然后使用java运行class文件,输入日志如下:

外部类静态块加载时间:1612782643295
==============main()=============
静态内部类静态块加载时间:1612782643296
静态内部类静态变量加载时间:1612782643296

      前面分析到静态内部类可以独立初始化,本例中也静态内部类也没有调用外部类的静态变量,日志中输出了"外部类静态块加载时间",是不是前后矛盾啊?答案是否定的。
      前面讲到直接使用java.exe命令来运行某个主类可以对类进行初始化,main()方法是外部类的一个方法,所以调用该方法后,肯定会先初始化外部类,继而调用到了外部类的静态代码块,如果放在其他类中执行,就不会走到外部类的静态代码块,这一点需要注意。
      b.类执行顺序的问题

public class ParentClass {
    static int num = 0;
    String name = "aaaaaaaa";
    static String name2 = "sssssssss";
    static ParentClass parentClass = new ParentClass();

    ParentClass(){
        System.out.println("--------构造函数-------");
    }

    {
        System.out.println("name1:" + name);
        System.out.println("--------非静态代码块----------");
    }

    static {
        num += 1;
        System.out.println("parentClass.parentClass.parentClass.name:" + parentClass.name);
        System.out.println("------------静态代码块*************" + num);
    }

    public static void main(String[] args) {
        System.out.println("---------------main()----------------------");
        ParentClass pa = new ParentClass();
    }
}

      执行javac,然后java运行结果如下:

name1:aaaaaaaa
--------非静态代码块----------
--------构造函数-------
parentClass.parentClass.parentClass.name:aaaaaaaa
------------静态代码块*************1
---------------main()----------------------
name1:aaaaaaaa
--------非静态代码块----------
--------构造函数-------

      结合上述实例,简单总结一下
      当在初始化类的时候,会先执行静态块和静态变量的声明。
      如果没有先声明静态变量,再在静态块里调用静态变量。会报错。[即调用的话,静态变量的声明需要放在静态代码块的上面]
      执行完静态块之后再执行非静态块。
      执行非静态块的时候,如果调用了没有声明的非静态变量[非静态变量的声明没有放在非静态块上面]会报错。
      注意:
      如果在类里声明了静态对象。像上面的例子。
      会先执行非静态块,其次构造方法,然后按照(静态优先,非静态其次的原则进行)
      静态块只会在非静态块执行完之后执行一次。

上一篇下一篇

猜你喜欢

热点阅读