Java类加载机制

1 概述

1.1 类加载机制

类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

1.2 类加载过程

Class类型通常以文件的形式存在(当然任何二进制流都可以是Class类型),只有被Java虚拟机装在的Class类型才能在程序中使用。

系统装在Class类型可以分为加载、连接和初始化3个步骤。其中,连接又可以分为验证、准备和解析3步。
在这里插入图片描述

1.2.1 加载类

加载类处于类加载的第一个阶段。在加载类时,Java虚拟机必须完成以下工作:

  • 通过类的全限定名,获取类的二进制数据流;
  • 解析类的二进制数据流为方法区内的数据结构;
  • 创建java.lang.Class类的实例,表示该类型。

1.2.2 验证类

当类加载到系统后,就开始连接操作,验证是连接操作的第一步。目的是保证加载的字节码是合法、合理并符合规范的。
在这里插入图片描述

1.2.3 准备

当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段虚拟机会为这个类分配相应的内存空间,并设置初始值。
在这里插入图片描述

1.2.4 解析类

解析阶段的工作是将类、接口、字段和方法的符号引用转为直接引用。

1.2.5 初始化

为类的静态变量赋予正确的初始值。

初始化的顺序:

在这里插入图片描述

1.3 类加载的条件

Class只要在必须要使用的时候才会被加载,虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前必须进行初始化。这里的“使用”指的是主动使用。分为以下几种情况:

  • 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
  • 当调用类的静态方法时,即当使用了字节码invokestatic指令。
  • 当使用类或接口的静态字段时(final常量除外),比如使用getstatic或者putstatic指令。
  • 当使用java.lang.reflect包中的方法反射类的方法时。
  • 当初始化子类时,要求先初始化父类。
  • 作为启动虚拟机,含有main()方法的那个类。

除了上述情况,其他情况都不会引起类的初始化。

2 类加载器

2.1 类加载器子系统

虚拟机设计团队将类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让应用程序自己去决定如何去获取所需要的的类。实现这个动作的代码模块称为类加载器
在这里插入图片描述

2.2 类加载器的分类

在标准的Java程序中,Java虚拟机会创建3类ClassLoader为整个应用程序服务。分别是:

  • BootStrap ClassLoader(启动类加载器):负责加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数路径指定的类库。
  • Extension ClassLoader(扩展类加载器):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定路径中的类库。
  • App ClassLoader(应用类加载器,也称为系统类加载器):负责加载用户类路径中(ClassPath)的类库。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

以上类加载器中,BootStrap ClassLoader由C++实现,是虚拟机自身的一部分。其余类加载器由Java实现,独立于虚拟机外,并且全都继承自抽象类java.lang.ClassLoader。

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求。

2.3 双亲委派模型

2.3.1 概念

在双亲委派模型中,除了引导类加载器之外,所有的类加载器都有一个父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是通过组合的关系来复用父加载器的代码。类加载器之间的组织结构如下图所示。
在这里插入图片描述
双亲委派模型的工作过程:如果一个类加载器接收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都会这么做,直到请求委派至启动类加载器。这时,父类加载器如果不能完成类加载请求,才会交由子类加载器去加载。

在这里插入图片描述
优点:双亲委派模式可以保证Java程序的稳定运作。

2.3.2 Tomcat案例

Tomcat等主流Java Web服务器实现了自定义的类加载器(不止一个),目的是为了解决一下问题:

  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。
  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。(过多的重复类库使方法区容易出现过度膨胀)
  • 服务器尽可能保证自身的安全不受部署的Web应用程序的影响。
  • 支持JSP应用的Web服务器,大多支持HotSwap功能。

由于可能存在上述问题,单独的ClassPath就无法满足需求了,所以各种Web服务器都提供了好几个ClassPath路径提供用户存放第三方类库。

在Tomcat目录中,有3组目录:

  • /common/* :类库可被Tomcar和所有Web应用程序共用。
  • /server/* :仅Tomcat可用,对Web应用不可见。
  • /shared/* :所有Web应用共用,对Tomcat不可见。

加上Web应用程序的目录:

  • /WebApp/WEB-INF/* :仅对此Web应用可用,对Tomcat和其他Web应用不可见。
    在这里插入图片描述

2.4 自定义类加载器

2.4.1 文件类加载器

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
 }

测试:

public class Message {
    public void echo(String message){
        System.out.println(message);
    }
}
public static void main(String[] args) throws Exception {
    FileSystemClassLoader loader = new FileSystemClassLoader("F:");
    Class<?> clazz = loader.findClass("Message");
    Object o = clazz.getDeclaredConstructor().newInstance();
    Method method = clazz.getMethod("echo",String.class);
    method.invoke(o,"hello");
}

2.4.2 网络类加载器

public class NetworkClassLoader extends ClassLoader {

    private String rootUrl;

    public NetworkClassLoader(String rootUrl) {
        this.rootUrl = rootUrl;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            URL url = new URL(path);
            InputStream ins = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootUrl + "/"
                + className.replace('.', '/') + ".class";
    }
}
public static void main(String[] args) throws Exception {
    NetworkClassLoader loader = new NetworkClassLoader("file:///F:");
    Class<?> clazz = loader.findClass("Message");
    Object o = clazz.getDeclaredConstructor().newInstance();
    Method method = clazz.getMethod("echo",String.class);
    method.invoke(o,"hello");
}

2.5 打破双亲委派

2.5.1 SPI与线程上下文类加载器

2.5.1.1 SPI

SPI(Service Provider Interface,服务提供者接口):提供给服务提供厂商与扩展框架功能的开发者使用的接口。

有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

在这里插入图片描述
常见的SPI包括:JDBC、JCE、JNDI、JAXP和JBI等。

2.5.1.2 线程上下文类加载器

线程上下文类加载器是JDK1.2开始引入的,getContextClassLoader()和setContextClassLoader(ClassLoader cl)分别是获取和设置当前线程的上下文类加载器,如果当前线程没有上下文类加载器,那么它将和父线程保持同样的类加载器。

站在开发者的角度,其他线程都是由Main线程,也就是main()函数所在的线程派生的,它是其他线程的父线程或者祖父线程。

System.out.println(Thread.currentThread().getContextClassLoader());

out:
sun.misc.Launcher$AppClassLoader@18b4aac2
2.5.1.3 JDBC案例

在这里插入图片描述
Java中使用JDBC这个SPI完全透明了应用程序和第三方厂商数据库库驱动的具体实现,不管数据库如何切换,应用程序只需要替换JDBC的驱动jar包以及数据库的名称即可,而不需要进行任何更新。

不需要SPI,也可以直接加载数据库驱动,例如:

Class.forName("com.mysql.jdbc.Driver").newInstance();

下面是mysql注册驱动及获取connection的过程,没有Class.forName,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 

那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
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;
    }
    // 通过SPI加载驱动类
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });
    // 继续加载系统属性中的驱动类
    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);
            // 使用AppClassloader加载
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

从上面可以看出JDBC中的DriverManager的加载Driver的步骤顺序依次是:

  1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
  2. 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。

下面详细分析SPI加载的那段代码:

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

try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
// Do nothing
}

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们注释掉的那一句代码。好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。

因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader 。这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中。

再看下看ServiceLoader.load(Class)的代码,的确如此:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

SPI机制直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

Driver静态代码块

com.mysql.jdbc.Driver加载后运行的静态代码块:

static {
	try {
		// Driver已经加载到TCCL中了,此时可以直接实例化
		java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
	} catch (SQLException E) {
		throw new RuntimeException("Can't register driver!");
	}
}

registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。

到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:

private static Connection getConnection(
     String url, java.util.Properties info, Class<?> caller) throws SQLException {
     /* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }

     if(url == null) {
         throw new SQLException("The url cannot be null", "08001");
     }

     SQLException reason = null;
     // 遍历注册到registeredDrivers里的Driver类
     for(DriverInfo aDriver : registeredDrivers) {
         // 检查Driver类有效性
         if(isDriverAllowed(aDriver.driver, callerCL)) {
             try {
                 println("    trying " + aDriver.driver.getClass().getName());
                 // 调用com.mysql.jdbc.Driver.connect方法获取连接
                 Connection con = aDriver.driver.connect(url, info);
                 if (con != null) {
                     // Success!
                     return (con);
                 }
             } catch (SQLException ex) {
                 if (reason == null) {
                     reason = ex;
                 }
             }

         } else {
             println("    skipping: " + aDriver.getClass().getName());
         }

     }
     throw new SQLException("No suitable driver found for "+ url, "08001");
 }

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
	    // 传入的classLoader为调用getConnetction的当前类加载器,从中寻找driver的class对象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
	// 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
	// driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}

由于TCCL本质就是当前应用类加载器,所以之前的初始化就是加载在当前的类加载器中,这一步就是校验存放的driver是否属于调用者的Classloader。例如在下文中的tomcat里,多个webapp都有自己的Classloader,如果它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器的Driver实例,想要区分只能靠TCCL了。

以上JDBC案例摘自《真正理解线程上下文类加载器(多案例分析)》

2.5.2 OSGi(to do)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值