JVM笔记(1)——类加载子系统

本文详细介绍了JVM中的类加载过程,包括懒加载机制、类加载的三个阶段(加载、链接、初始化)以及类加载器的工作原理。类加载器分为启动类加载器、扩展类加载器、应用程序类加载器和用户自定义类加载器,并遵循双亲委派机制。同时,文章探讨了如何打破双亲委派机制,例如在类的热部署和JNDI服务接口类加载中的应用。
摘要由CSDN通过智能技术生成

 一个类在什么时候会被加载?

JVM中类加载采用的是懒加载机制,就是当这个类被用到的时候才会去加载。例如当jvm中执行引擎执行到类A方法中一条创建类B对象的字节码指令new #3,首先到类A的运行时常量池中找到索引#3对应存储的引用,若此时引用还是符号引用,则尝试将其解析为对类B的直接引用,先用类B的全限定名在加载类A的加载器的命名空间中查找类B有没有加载,若没有找到则从类A的加载器开始以双亲委派机制尝试加载类B,向上委派的过程中检查当前层有没有加载过类B,若没有才最终在向下回派的过程中由能加载类B的加载器进行加载,触发真正的类加载过程(本地native方法defineClass1),加载初始化完毕后返回类B的InstanceKlass对象结构体的直接引用,替换常量池原符号引用。

类加载的时机 或者说用到类的时机

一、内存结构概览

其中,类加载子系统负责从文件系统网络中加载class文件,加载的类信息存放在一块称为方法区的内存空间,称为类的DNA元数据模板。

旁白:方法区是一个虚拟概念,具体实现看jdk版本,jdk7及以前称为永久代(虚拟机内存中),之后则是元空间(本地内存中)。

 

二、类加载过程

类加载过程分为以下三个部分:

1. 加载——loading

类加载器(ClassLoader)通过类的全限定名从磁盘或网络中获取此类的二进制字节流,将类元数据(如常量池、字段和方法等)生成一个InstanceKlass对象(C++)存放在方法区,并在堆中生成一个代表这个类的java.lang.Class对象,可以通过这个Class对象访问方法区中的类元数据,元数据中又存有class对象的引用。同一个类的类对象在jvm中最多只有一个

参考:class对象存储在Java堆中

2. 链接——Linking

验证字节码信息的正确性,保证加载类的正确性,不会危害虚拟机安全。

并为类的静态变量在堆中分配内存并设置默认初始值。

将类中常量池内的部分符号引用解析为直接引用。

旁白:在生成字节码文件过程中,会在其中生成一个常量池,它主要包括在该类中出现过的各类包,类,接口,字段,方法等元素的全限定名,所谓符号引用,只是一个符号而已,只是告知jvm,此类需要哪些调用方法,引用或者继承哪些类等等信息。在解析过程中,就将这些符号转换为指向具体资源所在地址的直接引用。Java虚拟机规范没有规定符号引用的解析时机,在Hotspot中类加载阶段会将非虚方法(包括静态方法、私有方法、构造方法、父类方法)的符号引用解析为直接引用,其他的符号引用则在字节码指令执行第一次用到的时候进行解析。

参考:
字面量,符号引用,字段

符号引用和直接引用,解析和分派

为什么在Java类加载过程中,是先生成class对象,再验证class字节码文件的正确性?

在Java类加载的过程中,确实是先生成class对象,然后再验证class字节码文件的正确性。这个顺序的设计是有一定的原因和考虑的。

首先,生成class对象是类加载的第一步,也是最基本的一步,它包含了类的名称、访问标志、类的父类、接口、字段、方法等信息,这些信息都是在class字节码文件中保存的。生成class对象是为后续的验证、准备、解析、初始化等步骤提供了必要的基础数据,只有在生成了class对象之后,才能对类进行进一步的处理。

其次,对class字节码文件进行验证是确保代码的安全性和正确性的重要步骤。由于Java是一门安全性较高的语言,代码的运行必须经过验证过程,以确保代码不会对系统造成损害或漏洞。但是,在验证class字节码文件之前,必须先生成class对象,否则就无法进行验证。因为在验证过程中需要用到class对象的信息,比如类的继承关系、字段和方法的访问控制等。如果反过来先验证class字节码文件,就无法获得class对象的相关信息,从而无法完成验证过程。

因此,为了保证类加载的顺序和正确性,Java设计了先生成class对象,再验证class字节码文件的步骤。

3. 初始化——Initialization

 执行类构造器方法<clinit>()的过程。初始化类的静态变量执行静态代码块

 旁白:在程序的执行过程中,一个类只会被加载一次,虚拟机须保证对类的<clinit>()方法加锁,以避免多线程下重复执行了<clinit>()导致重复加载。此外,类的使用分为主动使用和被动使用,只有主动使用才会执行初始化,后面的章节会再详细讲这点

三、类加载器详解

1. 类加载器分类 

前面提到类的字节码文件是由类加载器加载到内存的,而类加载器分为以下四种

1. 启动类加载器(BootstrapClassLoader):负责加载java核心库($JAVA_HOME/jre/lib/rt.jar等)中的类,包括扩展类加载器和应用程序类加载器。 或Xbootclassoath选项指定的jar包

2. 扩展类加载器(ExtClassLoader):负责加载java扩展包($JAVA_HOME/jre/lib/ext/*.jar等)中的类。或 -Djava.ext.dirs指定目录下的jar包 

3. 应用程序类加载器(AppClassLoader): 负责加载环境变量classpath下的类,就是程序自己的类。或-Djava.class.path指定的jar包

4. 用户自定义类加载器:负责加载用户指定的类。自定义加载方式,以实现不同的需求。可通过继承ClassLoader或URLClassLoader来实现

2. 几类加载器之间的关系

我们经常看到类加载器关系图只是反应在加载类时它们的层级关系,它们之间并不是继承关系。

实际上,启动类加载器是用C/C++编写的,除了启动类加载器外,其他加载器都是直接或间接继承自抽象类ClassLoader。例如ExtClassLoader和AppClassLoader都是JVM启动入口类Launcher中的内部类,都继承自URLClassLoader,间接继承自ClassLoader,,如下。

Launcher类、ExtClassLoader类和AppClassLoader类是在jvm启动时由启动类加载器加载。ExtClassLoader和AppClassLoader在jvm中都是单例的,在创建Launcher对象时初始化,初始化AppClassLoader实例时会将ExtClassLoader实例指定为其用于委托的父类加载器,即将其成员变量parent值设置为ExtClassLoader实例,而ExtClassLoader实例的parent值为null,但实际其委托的父类加载器仍为启动类加载器。

如下,ClassLoader的getParent()方法用于返回用于委托的父类加载器parent

而我们实现的自定义类加载器,其parent值会默认设置为AppClassLoader,也可以通过在构造器中调用父类构造器ClassLoader(ClassLoader parent)来手动设置parent,如果设置为null,则表示将启动类加载器作为父类加载器。

当然自定义类加载器也可以重写ClassLoader类的loadClass方法,加载类时不委托给父类加载器加载。参考:自定义类加载器的默认父类加载器为什么是AppClassLoader?

了解了这几类加载器之间的关系,我们就很容易明白加载类时的双亲委派机制 

3. 类加载模式——双亲委派机制

jvm在加载一个类时遵循双亲委派机制,加载器在loadClass方法中将加载任务如下图所示逐层向上委托给自己的父类加载器parent,直到委托到启动类加载器,如果启动加载器没有找到此类,则将加载任务沿原路递归回来依次向下委派,直到某一层可以加载此类。如果委派到最后没有加载器能够加载,则抛出ClassNotFoundException异常。

程序启动时的main方法类是由AppClassLoader加载的,而类中直接引用到的类是由加载当前类的加载器先执行loadClass的(全盘负责的特性),类中指定加载器加载的类则是由指定加载器先loadClass的,由此我们就可以推出程序中每个类的加载流程

也可以直接看ClassLoader类中的loadClass()方法源码

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //加锁,防止多线程下类重复加载
        synchronized (getClassLoadingLock(name)) {
            //首先检查类是否已经加载 判断条件:类全名name相同 && 加载类的加载器实例对象相同
            //这也是判断两个对象是否属于同一个类的条件
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果parent不为空或者父类加载器为启动类加载器 则委托给父类加载器进行加载
                    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方法尝试加载,如果没加载到,则抛出ClassNotFoundException异常
                    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;
        }
    }

双亲委派机制的作用:加载类时先向上委托,这样就保证了JDK的核心类优先加载,防止核心API被入侵自定义同名类篡改

4. 如何打破双亲委派机制

但是在一些应用场景中,我们反而需要打破这种双亲委派机制

打破方法:

1. 自定义类加载器,并且重写loadClass方法,加载类时不委托给父类加载器

2. 使用ThreadContextClassLoader类加载器

场景1:类的热部署

热部署就是在不重启应用的情况下,当类的字节码文件修改后,能够在jvm中生成新的类对象,实现动态更新。但是一般情况下类的加载都是由系统自带的类加载器完成,且对于同一个全限定名的java类,只能被加载一次(如上方loadClass中的findLoadedClass方法所示,若已经加载,则不重复加载第二次),而且无法被卸载。

那么如何实现类的热部署呢?这就要打破双亲委派机制。自定义一个类加载器,并重写loadClass方法,在loadclass中将要热部署的类自行加载,不委托给父类加载器。并且程序启动时开启一个监测线程,定时监测类文件是否发生变动,若变动,则new一个新的自定义类加载器实例,用它去重新加载类文件,此时虽然类名一样,但是加载器实例不一样,这样就可以生成新的类对象,然后再用这个新的类对象去创建实例。Tomcat中的热部署就是用类似的原理实现的

写了一个示例:热部署简单实现代码示例

场景2:JNDI服务接口类加载

JNDI服务(JDBC/JCE/JAXB/JBI)是jdk的核心类,通过SPI(Service Provider Interface)的服务发现机制,实现了对一类服务接口的不同厂商实现的可插拔式动态装配,例如数据库驱动接口java.sql.Driver,MySql的实现类为com.mysql.jdbc.Driver 属于第三方类库,是由AppClassLoader加载,但管理各个Driver接口实现类的DriverManager也是jdk核心库类,是由启动类加载器进行加载的,那么它要加载com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委派机制的原理相悖,那它是怎么解决这个问题的?

实际上在DriverManager中也是破坏了双亲委派机制,在加载DriverManager类时执行其类中静态代码块:通过调用ServiceLoader.load(Driver.class)方法扫描classpath下的driver驱动配置文件,

然后直接获取当前线程上下文类加载器去加载的Driver实现类。

而这个线程上下文类加载器是通过java.lang.Thread类的setContextClassLoader()方法设置的,如果当前线程创建时没有主动设置,则会从父线程继承一个,而最初程序启动时的主线程在Launcher类的Launcher()方法中将线程上下文类加载器设置为了应用程序类加载器AppClassLoader,由此而来。

 参考文章:https://zhuanlan.zhihu.com/p/185612299

下一篇: JVM笔记(2)—— 运行时数据区概述及线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值