JVM的类加载机制

本文详细介绍了Java虚拟机(JVM)的类加载机制,包括类加载的五个阶段,类初始化的五种触发条件,以及双亲委派模型的工作原理。还探讨了类加载器的层次结构,如引导类加载器、扩展类加载器和系统类加载器,以及自定义类加载器和线程上下文类加载器的角色。文章通过示例说明了类加载的触发时机,并讨论了Java SPI加载机制在JDBC驱动加载中的应用。
摘要由CSDN通过智能技术生成

维基百科关于java虚拟机的描述:

虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器堆栈寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

jvm的问世,使得代码编译的结果从本地机器码转变成字节码。字节码文件可以在任意安装了jvm的平台上运行,jvm在其中充当一个翻译官,为物理机解释字节码。java程序是跨平台的,但jvm并不是跨平台的。

    class文件从被加载到虚拟机到被卸载,会经历以下这些阶段。

类的生命周期

其中解析所处的位置并不是确定的,在某些情景下解析会在初始化之后开始,目的是支持java 的动态绑定(运行时绑定)特性。

虚拟机的规范严格规定了有且只有以下5种情形才会触发类的初始化(注意是“有且只有”)。

(1)触发new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有初始化,则先触发其初始化。对应的java代码场景是:使用new关键字实例化一个对象、读取或设置一个类的静态字段(被final修饰、已在编译器将结果放入常量池的静态字段除外)、调用类的静态方法的时候。

(2)使用java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化,则先触发初始化。

(3)初始化一个类时,如果发现其父类还没有初始化,则需先初始化其父类。

(4)当虚拟机启动时,会触发主类( 包含main方法的类)的初始化。

(5)当使用jdk7的动态语言支持时,如果一个MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需先触发其初始化。

这五种触发类初始化的行为称为主动引用。除此之外的所有引用类的地方都不会触发类的初始化,称为被动引用。

Example1:

public class SuperClass {

    public static String value = "value defined in super class";

    static {
        System.out.println("super class init");
    }

}

class SubClass extends SuperClass{

    static {
        System.out.println("sub class init");
    }

}

class Main {

    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }

}

运行结果:

super class init
value defined in super class

显然,子类的初始化没有被触发。对于静态字段的访问,只有直接定义该字段的类才会被初始化。

Example2:

class Main {

    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[10];
    }

}

运行这段程序没有任何输出。可见,新建数组对象,并不会触发数组元素类的初始化。

Example3:

进一步,把Example1中的value字段修饰为常量

public class SuperClass {

    public static final String value = "value defined in super class";

    static {
        System.out.println("super class init");
    }

}

class SubClass extends SuperClass{

    static {
        System.out.println("sub class init");
    }

}

class Main {

    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }

}

运行结果:

value defined in super class

可见,对静态常量的访问不会触发类的初始化。

接口也有初始化过程。其与类的初始化的不同之处体现在第三种情况:当一个接口在初始化时,并不要求其父接口完成初始化,只有在真正使用到父接口的时候(如引用父接口中定义的常量)才会触发父接口的初始化。

接下来重点聊聊类加载阶段中,“通过类全限定名来获取描述此类的二进制字节流”这个动作的实施者:类加载器。这个二进制字节流可能是classpath下的、可能是通过字节码工具动态生成的、还有可能是通过网络下载的。

一个类在虚拟机中的唯一性并不单单由类本身决定,还取决于加载类的类加载器。同一个.class文件,由不同的类加载器实例加载到虚拟机,得到的Class对象是不等的。这意味着类加载器给类添加了一个额外的命名空间。

几乎所有的类加载器都继承自java.lang.ClassLoader(除了引导类加载器)。Java提供的类加载器有三个:

引导类加载器(BootStrap class loader),用来加载核心库。它是由C++语言实现的,属于虚拟机的一部分,其它的类加载器都是独立于虚拟机外部的。BootStrap类加载器负责加载${JAVA_HOME}/lib目录或者被-Xbootclasspath参数指定的路径中的,并且是能被虚拟机识别的,如rt.jar。

扩展类加载器(Extension class loader),由ExtClassLoader实现,用来加载java的扩展库:${JAVA_HOME}/lib/ext目录中的,或者是被java.ext.dirs系统变量指定的类库。

系统类加载器(System class loader),也叫应用程序类加载器(Application class loader),由AppClassLoader实现,用来加载classpath下的类。可以通过ClassLoader.getSystemClassLoader()来获取。

类加载器的继承关系如下图所示:

    类加载器

除了引导类加载器外,其他的类加载器都有一个parent类加载器。这个“parent”并不是继承关系中的父类,而是通过组合的方式实现的,即类加载器都持有一个parent属性来指向它的parent类加载器,可以通过调用getParent()方法获取父加载器实例。类加载器A的parent类加载器是加载A的类加载器,因为类加载器本身也是Java类,它也需要被类加载器加载。用户自定义的类加载器的parent类加载器通常是系统类加载器。类加载器的组合关系如下:

双亲委派模型

类的加载过程遵循双亲委派模型:如果一个类加载器收到类加载器请求,它首先将这个请求委托给parent类加载器,每一层类加载器都是如此,所有的类加载请求最终都会到达启动类加载器。只有当父加载器无法加载时,子加载器才尝试自己加载。双亲委派模式的好处是:保证了classpath中的类在各种类加载器环境中都是同一个类,除非是你自定义了类加载器并重写了loadClass方法去加载。来看看ClassLoader.loadClass方法的源码,可以更直观地理解双亲委派模型的执行过程:

    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) {
                        // parent类加载器不为空则交由parent加载
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果parent为null,则交由引导类加载器来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // parent类加载器没有加载到,则自己动手
                    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;
        }
    }

真正完成类的加载工作是通过调用defineClass方法完成的,而启动类的加载过程则是通过调用loadClass方法完成的。发起类加载过程的类加载器称为初始加载器,最终完成对类的定义的类加载器称为定义加载器。判断两个类是否相同时,参考的是定义加载器。一个类的定义加载器是这个类引用的其它类的初始加载器。findClass()方法抛ClassNotFoundException,defineClass()方法抛NoClassDefFoundError。

显式地加载类:

(1)Class.forName(String name) 或 Class.forName(String name, boolean initialize, ClassLoader loader)

(2)new UserClassLoader().loadClass(String name)

(3)Thread.getCurrent().getContextClassLoader().loadClass(String name)

自定义类加载器:

自定义类加载器通常只需要重写findClass方法。双亲委派模式以及避免类重复加载的逻辑都体现在loadClass方法中,因此尽量避免重写该方法。示例:文件系统类加载器
package com.wxy.popcorn.test.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FileSystemClassLoader extends ClassLoader { 

    private String rootDir; 

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

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

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

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

双亲委派模式并不能解决所有的类加载问题。Java提供的API并不总是被用户调用,有时它也需要回调用户的代码。一个典型的例子是JNDI服务,其目的是对资源进行集中管理和查找。JNDI的代码由启动类加载器加载,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但是启动类加载器当然无法加载到这些代码。线程上下文类加载器就是为了解决这个问题而被设计出来的。

线程上下文类加载器可以通过setContextClassLoader()方法设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在全局范围内都没有设置过,则默认是系统类加载器。JNDI服务便是使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器委托子类加载器去完成类加载,破坏了双亲委派模式。java中所有SPI加载动作都是采用这种方式,如JNDI、JDBC、JCE、JAXB等。参考JDBC驱动的注册和加载方式可以深入地理解上下文类加载器。

获取MySQL数据库连接的步骤如下:

// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");

在1.6以后,我们就不需要再写第一句来注册mysql驱动了,Java的SPI加载机制已经为我们自动完成了。其具体如何实现的呢?

先看看DriverManager类,在其静态块里调用了loadInitialDrivers()。

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

 看看loadInitialDrivers()里做了些啥

private static void loadInitialDrivers() {
        String drivers;
        try {
            // 从系统属性获取jdbc驱动类名
            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()

        // 通过SPI加载驱动
        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);
            }
        }
    }

DriverManager在初始化过程中,会通过SPI方式加载数据库驱动,同时也会加载系统参数指定的驱动。在调用ServiceLoader.load方法时,会去获取线程上下文类加载器。

在调用driverIterator.hasNext()方法时,会去寻找META-INF/services/java.sql.Driver文件中的驱动名

然后,在调用driverIterator.next()方法时,会使用线程上下文类加载器去加载驱动类。 

在获取数据库连接时,会再次使用线程上下文类加载器来进行校验驱动类是否能够被加载。

    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized (DriverManager.class) {
            // synchronize loading of the correct classloader.
            // 优先使用caller的类加载器去加载驱动类 ,如果不存在则获取线程上下文类加载器
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

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

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            // 验证驱动类是否能够被加载,这次加载会触发驱动类的初始化
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

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

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

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

 至此我们发现DriverManager最终只是调用了Class.forName来加载驱动类,那么驱动类是在哪里实例化的呢?看看mysql的Driver就一目了然了

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

在静态块里往DriverManager里注册了驱动类实例。

更多类加载器的应用还可以参考osgi、tomcat的类加载机制。欢迎补充!

问题:

1. 非显式的类加载动作是何时触发的?

even if your class A uses class B, class B will not be loaded until that line of code in A is actually run.

即使类A使用了类B,只有A的代码被真正执行,才会触发B的加载。如实例化:new A(),静态调用A.doSomething()

2. 类加载过程的简单流程?

比如,类A被类加载器CLA加载,则CLA是类A的定义类加载器。类A使用了类B,当A中代码被执行时,触发类B的加载,这个加载过程由类A的加载器CLA发起,CLA先尝试用其parent CLB来加载B(如果CLA没有重写loadClass()方法中的双亲委派逻辑),如果CLB成功加载了B,那么CLB是B的定义类加载器,如果CLB没有加载到B,则CLA去加载,如果成功,则CLA是B的定义类加载器,如果失败,则抛出ClassNotFoundException或NoClassDefFoundError。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值