JVM-类加载机制

一、基础概念

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把该类加载到JVM。

其主要流程如下:
在这里插入图片描述

1.什么是类加载

那么什么是类加载?

Java的类加载,就是把字节码格式“.class”文件加载到JVM的方法区,并在JVM的堆区建立一 个java.lang.Class对象的实例,用来封装Java类相关的数据和方法。

那Class对象又是什么呢?

你可以 把它理解成业务类的模板,JVM根据这个模板来创建具体业务类对象实例。

2.运行jar文件的方式

执行下面命令就可以运行jar包

java -jar xxx.jar

但是上述方式如果关闭了窗口或者ctrc+c后进程就终止了。
所以一般建议以下方式进行启动

nohup java -jar myapp.jar &

以上命令会以nohup方式启动“myapp.jar”应用程序,并且不会因为控制台关闭或者SSH连接断开而停止。

现在,“myapp.jar”已被成功地转移到后台进程中。

3.懒加载

JVM并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加 载。

二、类加载机制

1.类加载整体流程

在这里插入图片描述

基本流程:class文件加载至内存,然后执行链接(验证、准备、解析),初始化;最终形成JVM可以直接使用的JAVA类型的过程。

加载:在方法区形成该类的运行时数据结构;

在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等。
在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

链接:class文件是否存在问题;一些符号引号替换成直接引用。具体如下:

校验:检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节码验证,符号引用验证) 

准备:给类的静态变量分配并初始化存储空间;会赋予一个默认值但不是真正的代码定义的值。 

解析:将常量池中的符号引用转成直接引用; 

初始化:到了初始化阶段,才真正去执行Java里面的代码。主要为静态变量赋值和执行静态块,此时赋予的值才是真正代码中定义的值。初始化一个类时,会先初始化它的父类。

虚拟机会保证一个类的初始化在多线程环境中被正确加锁和同步。

注意:
1.类加载的过程中,是不会涉及到堆内存的。
2.主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

2. 类加载器

顾名思义就是来完成类加载这个过程的组件。

站在Java虚拟机的角度讲,只存在两种不同的类加载器:

一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分:

另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

1.类加载器层次结构

类加载器的层次结构如下

在这里插入图片描述

1.引导类加载器bootstrap classloader
加载JAVA核心库($JAVA_HOME/jre/lib/rt.jar),原生代码实现(C++),并不继承自java.lang.ClassLoader。

2.扩展类加载器extensions classloader
负责加载扩展目录($JAVA_HOME/jre/ext/*.jar)下的类包
由sun.misc.Launcher.ExtClassLoader实现

3.应用程序类加载器 application classloader(也称系统类加载器)
一般来说,JAVA应用的类由它加载,即加载路径是classpath下的路径。

主要就是加载你自己写的那些类,比如如下类:


由sun.misc.Launcher.AppClassLoader实现。应用程序默认用它来加载类。

4.自定义类加载器

开发人员继承java.lang.ClassLoader实现自己的类加载器。(自定义类加载器不会打破双亲委托机制)

这里请你注意,类加载器的父子关系不是通过继承来实现的,比如AppClassLoader并不是ExtClassLoader的 子类,而是说AppClassLoader的parent成员变量指向ExtClassLoader对象。

同样的道理,如果你要自定义类 加载器,不去继承AppClassLoader,而是继承ClassLoader抽象类,再重写findClass和loadClass方法即可。

2.ClassLoader类加载器

ClassLoader的基本职责就是:

第一,根据指定的类名称,找到或者生成对应的字节码,并根据字节码生成class对象

第二,加载JAVA应用所需的资源,如配置文件等。

JVM类加载是由类加载器来完成的,JDK提供一个抽象类ClassLoader,这个抽象类中定义了三个关键方法分别是:findClass,defineClass和loadClass,理解清楚它们的作用和关系非常重要。

那么如何理解loadclass和findclass?如果你并不想打破双亲委托机制,但是又想定义自己的类加载器来加载特定目录下的类,你需要重写 findClass和loadClass方法中的哪一个?

loadclass定义类加载的流程
findclass真正的去执行加载

所以上面的问题:不想打破双亲委托机制,但是又想定义自己的类加载器来加载特定目录下的类,只需要重写findClass方法即可。

java.lang.ClassLoader实现源码如下:

public abstract class ClassLoader {

    //每个类加载器都有个⽗加载器
    private final ClassLoader parent;


    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            //如果没有加载过
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //先委托给⽗加载器去加载,注意这是个递归调⽤
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则,调用Bootstrap Classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                // 如果⽗加载器没加载成功,调⽤⾃⼰的findClass去加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

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


    protected Class<?> findClass(String name){
    //1. 根据传⼊的类名name,到在特定⽬录下去寻找类⽂件,把.class⽂件读⼊内存
        ...
    //2. 调⽤defineClass将字节数组转成Class对象
    return defineClass(buf, off, len);
    }


    // 将字节码数组解析成⼀个Class对象,⽤native⽅法实现
    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }



}

从上面的代码我们可以得到几个关键信息:

  • JVM的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个parent字段,指向父加载器
  • findClass方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件 读到内存得到字节码数组,然后调用defineClass方法得到Class对象。
  • defineClass是个工具方法,它的职责是调用native方法把Java类的字节码解析成一个Class对象,所谓的 native方法就是由C语言实现的方法,Java通过JNI机制调用。
  • loadClass是个public方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不 是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用, 也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类时,会先委托父加载器去加 载,然后父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,才会交还 给子加载器加载,这就是双亲委托机制。

源码实现流程解读

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。

  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;
    如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。

  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

3.写一个自定义类加载器:

参考:https://gitee.com/daiwei-dave/core-java.git

4.类加载器初始化过程

1.会创建JVM启动器实例sun.misc.Launcher。
2.sun.misc.Launcher初始化使用了单例模式设计(饿汉式),保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。

3.在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

4.JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。

其源码如下:

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

2.3 Java创建对象的过程

Java是一门面向对象的编程语言,在Java程序运行过程中每时每刻都有对象被创建出来。
在语言层面上,创建对象通常仅仅是一个new关键字而已,而在虚拟机中,对象的创建又是怎样一个过程呢?new一个对象可以分为两个过程:类加载和创建对象。

在这里插入图片描述

1.类加载

参考类加载机制,使用双亲委派模型

2.分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量

这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况

划分内存的方法:

  • “指针碰撞”(Bump the Pointer)(默认用指针碰撞)

如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。(可以补一个脑图理解)

  • “空闲列表”(Free List)

如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表, 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录

解决并发问题的方法:

  • CAS(compare and swap)

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。即在分配内存时,如果发现已有其他线程被占用,则进行重试

  • TLAB本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。这样每个线程之间就相互隔离了。
通过­XX:+UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启­XX:+UseTLAB),-­XX:TLABSize 指定TLAB大小。

3.初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。

这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。

4.设置对象头

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

这些信息存放在对象的对象头Object Header之中。

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈
希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

在这里插入图片描述
5.执行init方法

执行初始化方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。

初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法

2.4 SPI

SPI机制简介

SPI的全名为Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。

主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。

简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。

为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

参考:

1.深入理解SPI机制 https://www.jianshu.com/p/3a3edbcd8f24

2.阿里面试:什么地方违反了双亲委派模型
https://www.sohu.com/a/334000357_505800

2.5 JNDI

JNDI是 Java 命名与目录接口(Java Naming and Directory Interface),在J2EE规范中是重要的规范之一,不少专家认为,没有透彻理解JNDI的意义和作用,就没有真正掌握J2EE特别是EJB的知识。

那么,JNDI到底起什么作用?

要了解JNDI的作用,我们可以从“如果不用JNDI我们怎样做?用了JNDI后我们又将怎样做?”

没有JNDI的做法:

程序员开发时,知道要开发访问MySQL数据库的应用,于是将一个对 MySQL JDBC 驱动程序类的引用进行了编码,并通过使用适当的 JDBC URL 连接到数据库。
如下:

Connection conn=null; 
try { 
	Class.forName("com.mysql.jdbc.Driver", true, Thread.currentThread().getContextClassLoader()); 
	conn=DriverManager.getConnection("jdbc:mysql://MyDBServer?user=xxx&password=xxx"); 
	...... 
	conn.close(); 
} catch(Exception e) { 
	e.printStackTrace(); 
} finally { 
	if(conn!=null) { 
		try { 
			conn.close(); 
		} catch(SQLException e) {} 
	}
}

这是传统的做法,也是以前非Java程序员(如Delphi、VB等)常见的做法。这种做法一般在小规模的开发过程中不会产生问题,只要程序员熟悉Java语言、了解JDBC技术和MySQL,可以很快开发出相应的应用程序。

没有JNDI的做法存在的问题:

1、数据库服务器名称MyDBServer 、用户名和口令都可能需要改变,由此引发JDBC URL需要修改;

2、数据库可能改用别的产品,如改用DB2或者Oracle,引发JDBC驱动程序包和类名需要修改;如要修改:com.mysql.jdbc.Driver,可能会用Oracle的驱动。
3、随着实际使用终端的增加,原配置的连接池参数可能需要调整;

解决办法:

程序员应该不需要关心“具体的数据库后台是什么?JDBC驱动程序是什么?JDBC URL格式是什么?访问数据库的用户名和口令是什么?”等等这些问题。

程序员编写的程序应该没有对 JDBC 驱动程序的引用,没有服务器名称,没有用户名称或口令 —— 甚至没有数据库池或连接管理。

而是把这些问题交给J2EE容器来配置和管理,程序员只需要对这些配置和管理进行引用即可。即由硬编码转为软编码。

由此,就有了JNDI。

用了JNDI之后的做法:

使用spi机制,动态切换数据库驱动。

思考

1.在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么

代码测试:

package java.lang;

/**
 * @author daiwei
 * @date 2019/2/11 15:42
 */
public class String {
    public static void main(String[] args) {
//        String string=new String();
//        string.say();
        say();
    }

    public static void say(){
        System.out.println("hahah");
    }
}

结果如下:
在这里插入图片描述

原因分析:

更据Java的双亲委托机制,Java首先会自下而上查找java.lang.String是否已经被加载,结果发现bootstrap classloader类已经加载了,故不会再在重新加载java.lang.String类了,直接返回之前已经加载的java.lang.String类,而之前的类没有main方法。

故抛出找不到对应方法的错误。如果换成package.String等其他包名,则可以被加载

2.实战加载resource文件下的配置

使用spring提供的工具org.springframework.core.io.DefaultResourceLoader加载配置文件

3.JDBC中如何打破双亲委派模型

我们平时看到的mysql的加载是这个样子的:
在这里插入图片描述
DriverManager的初始化方法loadInitialDrivers,大家可以从中看到先是获取jdbc.drivers属性,得到类的路径。然后通过系统类加载器加载。

其源码如下:java.sql.DriverManager#loadInitialDrivers

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

三、双亲委托机制

JDK默认的类加载机制是双亲委派机制。

1.实现原理

1.加载流程

在这里插入图片描述

简单的说就是某个特定的类加载器接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,比如说从应用加载器委托给扩展类加载器,从扩展类加载器委托给引导类加载器。这种委托,直至委托到层次最高的类加载器,即引导类加载器,如果委托的父类加载器可以完成加载任务,那么成功返回;只有父类加载器无法完成时,才去依次交给下层加载直到自己加载。

加载具体流程如下:

在这里插入图片描述

例如:当jvm要加载Test.class的时候,

(1)首先会到自定义加载器中查找,看是否已经加载过,如果已经加载过,则返回字节码。

(2)如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class。

(3)如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过。

(4)如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过。

(5)如果BoopStrap ClassLoader依然没有加载过,则到自己指定类加载路径下(“sun.boot.class.path”)查看是否有Test.class字节码,有则加载返回,没有通知下一层加载器ExtClassLoader到自己指定的类加载路径下(java.ext.dirs)查看。

(6)依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException。

2. 为什么采用双亲委派模型?

1.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改.

2.类的全局唯一性:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性.

  • 使得Java类随着类加载器不同而具备带优先级的层次关系,如java.lang.Object(位于rt.jar内),无论那个类加载器要加载该类,最终都委派给顶层引导类加载器,因此Object类在程序的各种类加载环境中都是同一个类。即能够有效确保一个类的全局唯一性。
  • 相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,(比如引用了不同的jar包,里面都自己加载了一套Object类)Java类型体系中最基础的行为也就无从保证,应用程序也将会变得一片混乱。如果有兴趣的话,可以尝试去写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
  • 保证了Java核心库的类型安全。核心库中所有系统都只会有一个类。

2.破坏双亲委派模型

使用JNDI服务、代码模块热部署OSGi等方式可以打破双亲委派模型。

1、继承ClassLoader并重写loadClass方法和findClass方法。

JDK1.2之前还没有引入双亲委派模式,为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。

JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

2、JNDI服务的代码有启动类加载器去加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用有独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,单启动类加载器不可能“认识”这些代码。

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这个行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。

Java中所有涉及SPI的加载动作基本上都是采用这种方式,例如:JNDI、JDBC、JCE、JAXB、和JBI等。

3、业界“事实上”Java模块化标准的OSGi,它实现模块化热部署的关键就是它自定义的类加载器机制的实现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。具体详见《深入理解虚拟机》第7章。

3.Tomcat打破双亲委托机制

参考我的另一篇博文

Tomcat架构设计_autodeploy:-CSDN博客

  • 26
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM中的双亲委派机制是一种类加载机制,它规定了在Java中一个类被加载时如何进行类加载器的选择。根据这个机制,当一个类需要被加载时,首先会由类加载器ClassLoader检查是否已经加载过该类,如果是,则直接返回已经加载过的类;如果不是,则将该请求委派给父类加载器去加载。这样的过程会一直向上委派,直到达到顶层的引导类加载器(Bootstrap ClassLoader)。引用 引用中提到,并不是所有的类加载器都采用双亲委派机制。Java虚拟机规范并没有强制要求使用双亲委派机制,只是建议使用。实际上,一些类加载器可能会采用不同的加载顺序,例如Tomcat服务器类加载器就是采用代理模式,首先尝试自己去加载某个类,如果找不到再代理给父类加载器。 引用中提到,引导类加载器(Bootstrap ClassLoader)是最早开始工作的类加载器,负责加载JVM的核心类库,例如java.lang.*包中的类。这些类在JVM启动时就已经被加载到内存中。 综上所述,JVM的双亲委派机制是一种类加载机制,它通过类加载器的委派方式来加载类,首先检查是否已经加载过该类,如果没有则委派给父类加载器去加载,直到达到顶层的引导类加载器。不过,并不是所有的类加载器都采用该机制,一些类加载器可能会采用不同的加载顺序。引用<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [JVM-双亲委派机制](https://blog.csdn.net/m0_51608444/article/details/125835862)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [jvm-双亲委派机制](https://blog.csdn.net/y08144013/article/details/130724858)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值