37. 请你详细说说类加载流程,类加载机制及自定义类加载器 上

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 37. 请你详细说说类加载流程,类加载机制及自定义类加载器 上

37. 请你详细说说类加载流程,类加载机制及自定义类加载器 上


一、引言

当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。

二、类的加载、链接、初始化

1、加载

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成,类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。

1.1、加载的class来源

从本地文件系统内加载class文件

从JAR包加载class文件

通过网络加载class文件

把一个java源文件动态编译,并执行加载。

2、类的链接

通过类的加载,内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。

2.1、验证

验证阶段用于检查被加载的类是否有正确的内部结构,并和其他类协调一致。即是否满足java虚拟机的约束。

2.2、准备

类准备阶段负责为类的类变量分配内存,并设置默认初始值。

2.3、解析

我们知道,引用其实对应于内存地址。思考这样一个问题,在编写代码时,使用引用,方法时,类知道这些引用方法的内存地址吗?显然是不知道的,因为类还未被加载到虚拟机中,你无法获得这些地址。

举例来说,对于一个方法的调用,编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用,来指代要调用的方法。

解析阶段的目的,就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。

3、类的初始化

类的初始化阶段,虚拟机主要对类变量进行初始化。虚拟机调用< clinit>方法,进行类变量的初始化。

java类中对类变量进行初始化的两种方式:

在定义时初始化

在静态初始化块内初始化

3.1、< clinit>方法相关

虚拟机会收集类及父类中的类变量及类方法组合为< clinit>方法,根据定义的顺序进行初始化。虚拟机会保证子类的< clinit>执行之前,父类的< clinit>方法先执行完毕。

因此,虚拟机中第一个被执行完毕的< clinit>方法肯定是java.lang.Object方法。

public class Test {
    static int A = 10;
    static {
        A = 20;
    }
}
class Test1 extends Test {
    private static int B = A;
    public static void main(String[] args) {
        System.out.println(Test1.B);
    }
}
//输出结果
//20

从输出中看出,父类的静态初始化块在子类静态变量初始化之前初始化完毕,所以输出结果是20,不是10。

如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成< clinit>方法。

接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。

public interface InterfaceInitTest {
    long A = CurrentTime.getTime();
}
interface InterfaceInitTest1 extends InterfaceInitTest {
    int B = 100;
}
class InterfaceInitTestImpl implements InterfaceInitTest1 {
    public static void main(String[] args) {
        System.out.println(InterfaceInitTestImpl.B);
        System.out.println("---------------------------");
        System.out.println("当前时间:"+InterfaceInitTestImpl.A);
    }
}
class CurrentTime {
    static long getTime() {
        System.out.println("加载了InterfaceInitTest接口");
        return System.currentTimeMillis();
    }
}
//输出结果
//100
//---------------------------
//加载了InterfaceInitTest接口
//当前时间:1560158880660

从输出验证了:对于接口,只有真正使用父接口的类变量才会真正的加载父接口。这跟普通类加载不一样。

虚拟机会保证一个类的< clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的< clinit>方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>方法完毕。

public class MultiThreadInitTest {
    static int A = 10;
    static {
           System.out.println(Thread.currentThread()+"init MultiThreadInitTest");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread() + "start");
            System.out.println(MultiThreadInitTest.A);
            System.out.println(Thread.currentThread() + "run over");
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}
//输出结果
//Thread[main,5,main]init MultiThreadInitTest
//Thread[Thread-0,5,main]start
//10
//Thread[Thread-0,5,main]run over
//Thread[Thread-1,5,main]start
//10
//Thread[Thread-1,5,main]run over

从输出中看出验证了:只有第一个线程对MultiThreadInitTest进行了一次初始化,第二个线程一直阻塞等待等第一个线程初始化完毕。

3.2、类初始化时机

当虚拟机启动时,初始化用户指定的主类;

当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;

当遇到调用静态方法或者使用静态变量,初始化静态变量或方法所在的类;

子类初始化过程会触发父类初始化;

如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口初始化;

使用反射API对某个类进行反射调用时,初始化这个类;

Class.forName()会触发类的初始化

3.3、final定义的初始化

注意:对于一个使用final定义的常量,如果在编译时就已经确定了值,在引用时不会触发初始化,因为在编译的时候就已经确定下来,就是“宏变量”。如果在编译时无法确定,在初次使用才会导致初始化。

public class StaticInnerSingleton {
    /**
     * 使用静态内部类实现单例:
     * 1:线程安全
     * 2:懒加载
     * 3:非反序列化安全,即反序列化得到的对象与序列化时的单例对象不是同一个,违反单例原则
     */
    private static class LazyHolder {
        private static final StaticInnerSingleton INNER_SINGLETON = new StaticInnerSingleton();
    }
    private StaticInnerSingleton() {
    }
    public static StaticInnerSingleton getInstance() {
        return LazyHolder.INNER_SINGLETON;
    }
}

看这个例子,单例模式静态内部类实现方式。我们可以看到单例实例使用final定义,但在编译时无法确定下来,所以在第一次使用StaticInnerSingleton.getInstance()方法时,才会触发静态内部类的加载,也就是延迟加载。

这里想指出,如果final定义的变量在编译时无法确定,则在使用时还是会进行类的初始化。

3.4、ClassLoader只会对类进行加载,不会进行初始化

public class Tester {
    static {
        System.out.println("Tester类的静态初始化块");
    }
}
class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //下面语句仅仅是加载Tester类
        classLoader.loadClass("loader.Tester");
        System.out.println("系统加载Tester类");
        //下面语句才会初始化Tester类
        Class.forName("loader.Tester");
    }
}
//输出结果
//系统加载Tester类
//Tester类的静态初始化块

从输出证明:ClassLoader只会对类进行加载,不会进行初始化;使用Class.forName()会强制导致类的初始化。

目录
相关文章
|
2月前
|
Java
类加载器以及类的加载过程
这篇文章讨论了Java中的类加载器机制以及类的加载过程。
类加载器以及类的加载过程
|
11天前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
12天前
|
Arthas 前端开发 Java
类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类
类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类
类加载器 超详解:什么是类加载器,类加载器作用及应用场景,类加载时机,类加载的完整过程,类加载器分类
|
5月前
|
存储 缓存 前端开发
类加载与类加载器概述
类加载与类加载器概述
51 6
|
5月前
|
存储 安全 Java
JVM类加载(类加载过程、双亲委派模型)
JVM类加载(类加载过程、双亲委派模型)
|
设计模式 缓存 前端开发
从类加载到双亲委派:深入解析类加载机制与 ClassLoader
从类加载到双亲委派:深入解析类加载机制与 ClassLoader
85 1
|
存储 安全 Java
类加载器与类的加载过程
类加载器与类的加载过程
|
缓存 前端开发 Java
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 中
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 中
119 0
37. 请你详细说说类加载流程,类加载机制及自定义类加载器 中
|
安全 前端开发 Java
双亲委派模型与自定义类加载器
双亲委派模型与自定义类加载器
双亲委派模型与自定义类加载器