Java类加载到类使用全过程

2020-12-25 10:04:24 浏览数 (1)

前言

上篇我们说到为了减少Activity类加载的过程,所以可以预创建Activity。

有的朋友就问我,类加载,类实例化到底是怎样一个过程,为什么预加载一次就能减少下次加载的时间呢?今天就一起来回顾一下,这也是面试常考的点哦~

类的生命周期

借用网上的一张图

类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

类加载阶段

类的加载主要有三步:

  • 将class文件字节码内容加载到内存中。
  • 并将这些静态数据转换成方法区中的运行时数据结构。
  • 在堆中生成一个代表这个类的java.lang.Class对象。

我们编写的java文件会在编译后变成.class文件,类加载器就是负责加载class字节码文件,class文件在文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构。并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎Execution Engine决定。

简单来说类加载机制就是从文件系统将一系列的 class 文件读入 JVM 内存中为后续程序运行提供资源的动作。

类加载器种类

类加载器种类主要有四种:

  • BootstrapClassLoader:启动类加载器,使用C 实现
  • ExtClassLoader:扩展类加载器,使用Java实现
  • AppClassLoader:应用程序类加载器,加载当前应用的classpath的所有类
  • UserDefinedClassLoader:用户自定义类加载器

属于依次继承关系,也就是上一级是下一级的父加载器。

类加载过程(双亲委派机制)

类加载的过程可以用一句话概括:

先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区。

对于类加载器加载过程,就用到了双亲委派机制,具体如下:

当一个类加载器收到了类加载的请求,它不会直接去加载这类,而是先把这个请求委派给父加载器去完成,依次会传递到最上级也就是启动类加载器,然后父加载器会检查是否已经加载过该类,如果没加载过,就会去加载,加载失败才会交给子加载器去加载,一直到最底层,如果都没办法能正确加载,则会跑出ClassNotFoundException异常。

举例:

  • Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
  • Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
  • 如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>lib中未找到所需类),就会让Extension ClassLoader尝试加载。
  • 如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
  • 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
  • 如果均加载失败,就会抛出ClassNotFoundException异常。

这么设计的原因主要有两点:

  • 这种层级关系可以避免类的重复加载。
  • 是为了防止危险代码的植入,比如String类,如果在AppClassLoader就直接被加载,就相当于会被篡改了,所以都要经过老大,也就是BootstrapClassLoader进行检查,已经加载过的类就不需要再去加载了。

代码看多了,都觉得文字是一种比较复杂的表达方式了,还是代码比较清晰明了,不知道你们有没有这样的感觉,直接上代码:

代码语言:javascript复制
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // 检查类是否已经加载过,如果已经加载过,则直接返回
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                // 委派父类加载器去加载类
                try {
                    if (parent != null) {
                     //一直找到最顶级的parent
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                // 委派父类加载器如果加载失败则调用findClass方法进行加载动作
                if (c == null) {
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

类验证,准备,解析

  • 验证。检查class是否符合要求,非必须阶段,对程序的运行期没有影响。
  • 准备。给类中的静态变量分配内存空间,这时候的内存分配仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起在堆中进行分配。
  • 解析。虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用理解为一个标示,而直接引用直接指向内存中的地址。

类初始化

这个类初始化和变量初始化可不一样,很多人会把这个弄混,认为初始化肯定是在实例化之后,其实不然。

类初始化指的是完成程序执行前的准备工作,会执行执行类构造器<clinit>()方法,在这个阶段,静态的(变量,方法,代码块)会被执行。同时在会开辟一块存储空间用来存放静态的数据。

初始化只在类加载的时候执行一次。

它的触发时机主要有以下几种情况:

  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时。
  • 类被反射调用的时候。
  • 初始化一个类的时候,如果其父类没有被初始化,会先初始化其父类。

类实例化

在类初始化完成之后,就可以进行类的实例化了。

类实例化指的是创建一个对象的过程,这个过程中会在堆中开辟内存,将一些非静态的方法,变量存放在里面。在程序执行的过程中,可以创建多个对象,既多次实例化。每次实例化都会开辟一块新的内存。

即new Object()。

最后来一道经典的面试题

  • 两个类,一个子类Son,一个父类Father,然后运行main函数,打印结果应该是什么?
代码语言:javascript复制
public class Father {
    private int i = test();
    private static int j = method();
    static {
        System.out.println("(1)");
    }
    Father() {
        System.out.println("(2)");
    }

    {
        System.out.println("(3)");
    }

    public int test() {
        System.out.println("(4)");
        return 1;
    }
    public static int method() {
        System.out.println("(5)");
        return 1;
    }
}

public class Son extends Father {
    private int i = test();
    private static int j = method();
    static {
        System.out.println("(6)");
    }
    Son() {
        System.out.println("(7)");
    }
    {
        System.out.println("(8)");
    }
    public int test() {
        System.out.println("(9)");
        return 1;
    }

    public static int method() {
        System.out.println("(10)");
        return 1;
    }
}

代码语言:javascript复制
   public static void main(String[] args) {
        Son s1  = new Son();
        System.out.println();
        Son s2 = new Son();
    }

答案明天见啦~

最后欢迎大家来码上积木微信讨论群,想加群的在公众号首页点击菜单 “联系我—加讨论群”,有我的微信号,记得加我备注“讨论群”哦。

参考

https://segmentfault.com/a/1190000023876273 https://www.cnblogs.com/throwable/p/12272269.html https://www.cnblogs.com/pu20065226/p/12206463.html

拜拜

感谢大家的阅读,有一起学习的小伙伴可以关注下公众号—码上积木❤️ 每日三问知识点/面试题,积少成多。

0 人点赞