原文:juejin.im/post/5cffa528e51d4556da53d091

一、引言

当程序使用某个类时,如果该类还未被加载到内存中,则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类中对类变量进行初始化的两种方式:

  1. 在定义时初始化

  2. 在静态初始化块内初始化

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、类初始化时机
  1. 当虚拟机启动时,初始化用户指定的主类;

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

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

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

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

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

  7. 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()会强制导致类的初始化。

三、类加载器

类加载器负责将.class文件(不管是jar,还是本地磁盘,还是网络获取等等)加载到内存中,并为之生成对应的java.lang.Class对象。一个类被加载到JVM中,就不会第二次加载了。

那怎么判断是同一个类呢?

每个类在JVM中使用全限定类名(包名+类名)与类加载器联合为唯一的ID,所以如果同一个类使用不同的类加载器,可以被加载到虚拟机,但彼此不兼容。

1、JVM类加载器分类

1.1、Bootstrap ClassLoader

Bootstrap ClassLoader为根类加载器,负责加载java的核心类库。根加载器不是ClassLoader的子类,是有C++实现的。

public class BootstrapTest {    public static void main(String[] args) {        //获取根类加载器所加载的全部URL数组        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();        Arrays.stream(urLs).forEach(System.out::println);    }}//输出结果//file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar//file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar//file:/C:/SorftwareInstall/java/jdk/jre/classes

根类加载器负责加载%JAVA_HOME%/jre/lib下的jar包(以及由虚拟机参数 -Xbootclasspath 指定的类)。


我们将rt.jar解压,可以看到我们经常使用的类库就在这个jar包中。

1.2 、Extension ClassLoader

Extension ClassLoader为扩展类加载器,负责加载%JAVA_HOME%/jre/ext或者java.ext.dirs系统熟悉指定的目录的jar包。大家可以将自己写的工具包放到这个目录下,可以方便自己使用。

1.3、 System ClassLoader

System ClassLoader为系统(应用)类加载器,负责加载加载来自java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器默认都以系统类加载器作为父加载器。

四、类加载机制

1.1、JVM主要的类加载机制。

  1. 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也由该类加载器负责载入,除非显示使用另一个类加载器来载入。

  2. 父类委托(双亲委派):先让父加载器试图加载该Class,只有在父加载器无法加载时该类加载器才会尝试从自己的类路径中加载该类。

  3. 缓存机制:缓存机制会将已经加载的class缓存起来,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存中不存在该Class时,系统才会读取该类的二进制数据,并将其转换为Class对象,存入缓存中。这就是为什么更改了class后,需要重启JVM才生效的原因。

注意:类加载器之间的父子关系并不是类继承上的父子关系,而是实例之间的父子关系。


public class ClassloaderPropTest {    public static void main(String[] args) throws IOException {        //获取系统类加载器        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();        System.out.println("系统类加载器:" + systemClassLoader);        /*        获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定,如果操作系统没有指定        CLASSPATH环境变量,则默认以当前路径作为系统类加载器的加载路径         */        Enumeration eml = systemClassLoader.getResources("");while (eml.hasMoreElements()) {            System.out.println(eml.nextElement());        }//获取系统类加载器的父类加载器,得到扩展类加载器        ClassLoader extensionLoader = systemClassLoader.getParent();        System.out.println("系统类的父加载器是扩展类加载器:" + extensionLoader);        System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));        System.out.println("扩展类加载器的parant:" + extensionLoader.getParent());    }}//输出结果//系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2//file:/C:/ProjectTest/FengKuang/out/production/FengKuang///系统类的父加载器是扩展类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d//扩展类加载器的加载路径:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext//扩展类加载器的parant:null

从输出中验证了:系统类加载器的父加载器是扩展类加载器。但输出中扩展类加载器的父加载器是null,这是因为父加载器不是java实现的,是C++实现的,所以获取不到。但扩展类加载器的父加载器是根加载器。

1.2、类加载流程图


图中红色部分,可以是我们自定义实现的类加载器来进行加载。

五、创建并使用自定义类加载器

1、自定义类加载分析

除了根类加载器,所有类加载器都是ClassLoader的子类。所以我们可以通过继承ClassLoader来实现自己的类加载器。

ClassLoader类有两个关键的方法:

  1. protected Class loadClass(String name, boolean resolve):name为类名,resove如果为true,在加载时解析该类。

  2. protected Class findClass(String name) :根据指定类名来查找类。

所以,如果要实现自定义类,可以重写这两个方法来实现。但推荐重写findClass方法,而不是重写loadClass方法,因为loadClass方法内部会调用findClass方法。

我们来看一下loadClass的源码

protected Class> loadClass(String name, boolean resolve)        throws ClassNotFoundException    {        synchronized (getClassLoadingLock(name)) {            //第一步,先从缓存里查看是否已经加载            Class> c = findLoadedClass(name);            if (c == null) {                long t0 = System.nanoTime();                try {                //第二步,判断父加载器是否为null                    if (parent != null) {                        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                }

                if (c == null) {                   //第三步,如果前面都没有找到,就会调用findClass方法                    long t1 = System.nanoTime();                    c = findClass(name);

                    // this is the defining class loader; record the stats                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                    sun.misc.PerfCounter.getFindClasses().increment();                }            }            if (resolve) {                resolveClass(c);            }            return c;        }    }

loadClass加载方法流程:

  1. 判断此类是否已经加载;

  2. 如果父加载器不为null,则使用父加载器进行加载;反之,使用根加载器进行加载;

  3. 如果前面都没加载成功,则使用findClass方法进行加载。

所以,为了不影响类的加载过程,我们重写findClass方法即可简单方便的实现自定义类加载。

2、实现自定义类加载器

基于以上分析,我们简单重写findClass方法进行自定义类加载。

public class Hello {   public void test(String str){       System.out.println(str);   }}

public class MyClassloader extends ClassLoader {

    /**     * 读取文件内容     *     * @param fileName 文件名     * @return     */    private byte[] getBytes(String fileName) throws IOException {        File file = new File(fileName);        long len = file.length();        byte[] raw = new byte[(int) len];        try (FileInputStream fin = new FileInputStream(file)) {            //一次性读取Class文件的全部二进制数据            int read = fin.read(raw);            if (read != len) {                throw new IOException("无法读取全部文件");            }            return raw;        }    }

    @Override    protected Class> findClass(String name) throws ClassNotFoundException {        Class clazz = null;        //将包路径的(.)替换为斜线(/)        String fileStub = name.replace(".", "/");        String classFileName = fileStub + ".class";        File classFile = new File(classFileName);

        //如果Class文件存在,系统负责将该文件转换为Class对象        if (classFile.exists()) {            try {                //将Class文件的二进制数据读入数组                byte[] raw = getBytes(classFileName);                //调用ClassLoader的defineClass方法将二进制数据转换为Class对象                clazz = defineClass(name, raw, 0, raw.length);            } catch (IOException e) {                e.printStackTrace();            }        }        //如果clazz为null,表明加载失败,抛出异常        if (null == clazz) {            throw new ClassNotFoundException(name);        }        return clazz;    }

    public static void main(String[] args) throws Exception {        String classPath = "loader.Hello";        MyClassloader myClassloader = new MyClassloader();        Class> aClass = myClassloader.loadClass(classPath);        Method main = aClass.getMethod("test", String.class);        System.out.println(main);        main.invoke(aClass.newInstance(), "Hello World");    }}//输出结果//Hello World

ClassLoader还有一个重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是将class的二进制数组转换为Calss对象。

此例子很简单,我写了一个Hello测试类,并且编译过后放在了当前路径下(大家可以在findClass中加入判断,如果没有此文件,可以尝试查找.java文件,并进行编译得到.class文件;或者判断.java文件的最后更新时间大于.class文件最后更新时间,再进行重新编译等逻辑)。

六、总结

本篇从类加载的三大阶段:加载、链接、初始化开始细说每个阶段的过程;详细讲解了JVM常用的类加载器的区别与联系,以及类加载机制流程,最后通过自定义的类加载器例子结束本篇。

【面试题专栏】

2019年面试官最喜欢问的28道ZooKeeper面试题

2019年全网最热门的123个Java并发面试题总结

全网最热门的119个Spring问题,哪些你还不会?

2020面试还搞不懂MyBatis?看看这27道面试题!(含答案和思维导图)

2020年去一线大厂面试先过SSM框架这一关!

Spring Cloud+Spring Boot高频面试题解析

2019年常见的Linux面试题及答案解析,哪些你还不会?

2019年常见Elasticsearch面试题答案解析

18道kafka高频面试题哪些你还不会?(含答案和思维导图)

2019年12道RabbitMQ高频面试题你都会了吗?(含答案解析)

2019年Dubbo你掌握的如何?快看看这30道高频面试题!

2019年228道Java中高级面试题(8),哪些你还不会?

 你在看吗?

ccs加载out文件_类加载流程、类加载机制及自定义类加载器详解相关推荐

  1. pyqt5从子目录加载qrc文件_【JVM系统学习之路】一篇看懂类加载

    JVM系统学习之路系列演示代码地址:https://github.com/mtcarpenter/JavaTutorial 嗨喽,小伙伴大家好,我是小春哥,今天是打卡 [JVM系统学习之路] 的第二篇 ...

  2. cad无法加载arx文件_多年经验总结CAD技巧8

    72.[关于选择的问题] 当绘图时没有虚线框显示,比如画一个矩形,取一点后,拖动鼠标时没有矩形虚框跟着变化,这时需修改DRAGMODE的系统变量,推荐修改为AUTO. 系统变量为ON时,再选定要拖动的 ...

  3. android ndk怎样加载o文件_在Android中使用TFLite c++部署

    之前的文章中,我们跟大家介绍过如何使用NNAPI来加速TFLite-Android的inference(可参考使用NNAPI加速android-tflite的Mobilenet分类器).不过之前介绍的 ...

  4. pyqt5从子目录加载qrc文件_实战PyQt5: 045-添加资源文件

    添加资源文件 在使用PyQt进行图形界面开发的时候不免要用到一些外部资源,比如图片,qss配置文件等.在前面代码中,遇到这类问题,我们使用绝对路径的方式来解决,这种方式,本身有其不方便之处(比如,调整 ...

  5. import引入json文件_关于TypeScript中import JSON的正确姿势详解

    前言 Typescript是微软内部出品的,用actionscript的语法在写js的一门新语言,最近 TypeScript 中毒,想想我一个弱类型出身的人,怎么就喜欢上了类型约束--当然这不是重点, ...

  6. java tomcat 404配置_在Tomcat中配置404自定义错误页面详解

    一直使用tomcat,今天想到一个问题,自定义404错误页面, 为了获得很好的用户感受,是不应当向用户暴露404这样的页面的, 问题的出发点是我在Struts2中定义错误页面, 在Struts2中是这 ...

  7. JVM------类加载器详解

    JVM------类加载器详解 1.图解类加载器工作流程 2.类加载器种类 3.类加载器的加载顺序 4.一些需要了解的机制 1.图解类加载器工作流程 2.类加载器种类 启动类加载器(Bootstrap ...

  8. java 加载class文件路径_动手实现MVC: 1. Java 扫描并加载包路径下class文件

    背景 用过spring框架之后,有个指定扫描包路径,然后自动实例化一些bean,这个过程还是比较有意思的,抽象一下,即下面三个点 如何扫描包路径下所有的class文件 如何扫描jar包中对应包路径下所 ...

  9. java文件读入原理_描述一下JVM加载class文件的原理机制

    1.JVM 简介 JVM 是我们Javaer 的最基本功底了,刚开始学Java 的时候,一般都是从"Hello World "开始的,然后会写个复杂点class ,然后再找一些开源 ...

最新文章

  1. 输入3个数a,b,c,要求按由小到大的顺序输出
  2. mysq中文编码问题
  3. 卸载Win10+ubuntu14双系统中的ubuntu系统
  4. Windows核心编程 第八章 用户方式中线程的同步(下)
  5. 微信小微商户获取申请状态
  6. 函数初识(文字总结)
  7. scanf 接收 空格 输入_当接受用户输入的含有空格的字符串时,应使用()函数。...
  8. 安装mamached数据库
  9. spring javafx_带有Spring的JavaFX 2
  10. GLSL/C++ 实现滤镜效果
  11. oracle数据库登录审计,oracle数据库审计
  12. [PYTHON]一个简单的单元測试框架
  13. 三角函数和复指数函数的转化_【导数压轴】当三角函数遇到导数02
  14. android ListView的怪异现象
  15. html5字体加粗斜体,font设置字体加粗
  16. Games 101 作业2 超详细说明
  17. 2021-07-16芯片-全球半导体产业核心地区的补贴及激励措施
  18. 职教云计算机网络基础题库,智慧职教云课堂APP计算机组装与维护章节测试答案...
  19. 如何实现JS主动触发事件
  20. error:1962 no operating system found

热门文章

  1. Django05-2:路由分发/命名空间/伪静态/虚拟环境/django版本区别
  2. powershell /遍历/psobject/字符串转换Json/json数组操作
  3. Redis -- Hash(哈希) [3]
  4. C和指针之函数递归实现把amount表示的值转换为单词形式written_amount(unsigned int amount,char *buffer)
  5. Android之切换账号登录依然能登录成功问题解决办法
  6. C++编译出现binding ‘const string {aka const std::__cxx11::basic_string<char>}’ to reference of type ‘std
  7. shell之a+b求和l脚本的三种写法
  8. Android之AndroidManifest.xml文件解析和权限集合
  9. php in_array 判断数组中是否存在此元素
  10. 围棋经典棋谱_秀秀老师:茶艺师也要学好围棋