JVM类加载机制、破坏双亲委派

JVM类加载机制我们从下面三个方面介绍一下,即 类加载过程、初始化类的顺序和类加载器

JVM类加载过程

要想使用一个Java类为自己工作,必须经过以下几个过程,类加载 -> 连接(验证 -> 准备 -> 解析)-> 类初始化,下面分别介绍:

1) 类加载load

从字节码二进制文件——.class文件将类加载到内存,从而达到类的从硬盘上到内存上的一个迁移,所有的程序必须加载到内存才能工作。将内存中的class放到运行时数据区的方法区内,之后在堆区建立一个java.lang.Class对象,用来封装方法区的数据结构。这个时候就体现出了万事万物皆对象了,干什么事情都得有个对象。就是到了最底层究竟是鸡生蛋,还是蛋生鸡呢?类加载的最终产物就是堆中的一个java.lang.Class对象。

2) 连接

验证:出于安全性的考虑,验证内存中的字节码是否符合JVM的规范,类的结构规范、语义检查、字节码操作是否合法、这个是为了防止用户自己建立一个非法的XX.class文件就进行工作了,或者是JVM版本冲突的问题,比如在JDK6下面编译通过的class(其中包含注解特性的类),是不能在JDK1.4的JVM下运行的。

准备:将类的静态变量进行分配内存空间、初始化默认值。(对象还没生成呢,所以这个时候没有实例变量什么事情)

解析:把类的符号引用转为直接引用(保留)

3) 类的初始化

将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值。

JVM 初始化类的顺序

1、父类【静态成员】和【静态代码块】,按在代码中出现的顺序依次执行 

2、子类【静态成员】和【静态代码块】,按在代码中出现的顺序依次执行

3、父类的【普通成员变量被普通成员方法赋值】和【普通代码块】,按在代码中出现的顺序依次执行

4、执行父类的构造方法 

5、子类的【普通成员变量被普通成员方法赋值】和【普通代码块】,按在代码中出现的顺序依次执行

6、执行子类的构造方法

JVM不触发初始化类

1、通过子类引用父类的静态字段,不会导致子类初始化

2、通过数组定义来引用类,不会触发此类的初始化

3、常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类

类加载器

类加载器加载过程:根加载器 -> 扩展类加载器 -> 应用类加载器 -> 用户自定义类加载器(继承 java.lang. ClassLoader)

根加载器(BoostrapClassLoader):它是用本地代码实现的类装载器,负责将 JDK 中 jre/lib 下的 类库 或者 XBootClassPath指定的类库加载到内存中,开发者无法直接获取到该加载器的引用(本地代码实现),所以无法通过引用操作

拓展类加载器(ExtClassLoader):,由sun公司sun.misc.Launcher$ExtClassLoader实现的,负责将JDK中 jre/lib/ext 或者 -Djava.ext.dir 指定的类库加载到内存,开发者可以直接使用拓展类加载器

应用类加载器(AppClassLoader): 由sun公司sun.misc.Launcher$AppClassLoader实现的,负责将类路径 java -classpath 或者 -Djava.class.path 指定的类库加载到内存中,开发者可以直接使用系统加载器

自定义类加载器:加载自己决定要加载某路径下的类

类加载器采用的是双亲委派原则,下面介绍一下双亲委派

双亲委派

定义:类加载时不会自己先去加载,而是丢给上层父类加载,上层父类一直向上丢,直到丢给根加载器加载,如果根加载加载成功直接返回,否则会丢给子类加载器加载。其实总结一句话就是:活来了,儿子不干活,全部交给父亲干,父亲干不了的自己再来干

双亲委派优势

1、避免类的重复加载,即父类加载过的类子类就不需要再加载了

2、防止用户任意篡改java 中的类,比如用户自定义了 java.lang.String 类,常规条件下肯定是加载不了的,因为加载时根加载器已经加载过该类了,就会直接返回

类加载源码如下:

// java.lang.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) {
                        // 双亲委派,递归调用父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                        // 没有父类的加载器就是根加载器(BootstrapClassLoader)
                        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();
                    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;
        }
    }

加载类的具体过程:首先从根加载器开始加载、根加载器加载不了的,由扩展类加载器加载,再加载不了的有应用加载器加载,应用加载器如果还加载不了就由自定义的加载器(一定继承自java.lang. ClassLoader)加载、如果自定义的加载器还加载不了。而且下面已经没有再特殊的类加载器了,就会抛出ClassNotFoundException

破坏双亲委派

破坏双亲委派的例子很多,网上的解释也很多,这里我就不做过多的解释了,如果想了解的话,请查看聊聊JDBC是如何破坏双亲委派模型的 和 浅谈双亲委派和破坏双亲委派 这两篇文章。下面我们着重介绍一下自己怎么样破坏双亲委派

1、如果我们自定义的加载器不想破坏双亲委派,只需要重写 java.lang.ClassLoader 中的 findClass 方法,示例代码如下:

package com.springboot.demo.class_loader;

import java.sql.Connection;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;

/**
 * 驱动类
 *
 * @author supu
 * @date 2019-06-27 14:07
 **/
public class Driver implements java.sql.Driver {
    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        return null;
    }

    @Override
    public boolean acceptsURL(String url) throws SQLException {
        return false;
    }

    @Override
    public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
        return new DriverPropertyInfo[0];
    }

    @Override
    public int getMajorVersion() {
        return 0;
    }

    @Override
    public int getMinorVersion() {
        return 0;
    }

    @Override
    public boolean jdbcCompliant() {
        return false;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }
}
package com.springboot.demo.class_loader;

import java.io.IOException;
import java.io.InputStream;

/**
 * 自定义加载器
 *
 * @author supu
 * @date 2019-06-25 14:46
 **/
public class CustomClassLoader extends ClassLoader {

    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] by = null;
        try {
            by = getByteByClassName(name);
            return defineClass(name, by, 0, by.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    private byte[] getByteByClassName(String name) throws IOException {
        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
        InputStream is = getClass().getResourceAsStream(fileName);
        byte[] by = new byte[is.available()];
        is.read(by);
        is.close();
        return by;
    }
}

测试结果如下:

JVM类加载机制、破坏双亲委派_第1张图片

2、如果使用我们自定义的加载器破坏双亲委派,只需要重写java.lang.ClassLoader 的loadClass(java.lang.String) 即可

自定义加载器代码如下:

package com.springboot.demo.class_loader;

import java.io.InputStream;

/**
 * 自定义加载器
 *
 * @author supu
 * @date 2019-06-25 14:46
 **/
public class CustomClassLoader extends ClassLoader {

    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    public Class loadClass(String name)
            throws ClassNotFoundException {
        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
        InputStream is;
        try {
            is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(fileName);
            }
            byte[] by = new byte[is.available()];
            is.read(by);
            is.close();
            return defineClass(name, by, 0, by.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

测试结果如下:

JVM类加载机制、破坏双亲委派_第2张图片

这里测试结果表明简单的重写 ClassLoader 的loadClass 方法是不行的,这里之所以报错是由于自己破坏了双亲委派原则后,父类的加载(java.sql.Driver)也会交由我们自定义的类加载器加载,即加载自己的 com.springboot.demo.class_loader.Driver 加载时会去加载 java.sql.Driver,  而 java.sql.Driver 类在 rt.jar 中,我们自己是无法加载到的。

如果我们真的需要破坏双亲委派加载自己扩展的类呢,有什么好的方案可以实现的呢,答案是肯定有的,只不过需要修改一下loadClass 方法的实现就可以了。下面通过示例来给大家展示一下如何使用自定义的加载器即破坏双亲委派又可以加载到类,示例代码如下:

package com.springboot.demo.class_loader;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 自定义web加载器
 *
 * @author supu
 * @date 2019-06-25 15:57
 **/
public class CustomWebappClassLoader extends URLClassLoader {
    /**
     * Instance of the SecurityManager installed.
     */
    protected SecurityManager securityManager;


    /**
     * The parent class loader.
     */
    protected ClassLoader parent = null;

    public CustomWebappClassLoader() {
        this(new URL[0], null);
    }


    public CustomWebappClassLoader(ClassLoader parent) {
        this(new URL[0], parent);
    }

    private void init(){
        ClassLoader p = getParent();
        if (p == null) {
            p = getSystemClassLoader();
        }
        this.parent = p;

        securityManager = System.getSecurityManager();
    }

    public CustomWebappClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);

        init();
    }

    public CustomWebappClassLoader(URL[] urls) {
        this(urls, null);
    }

    @Override
    public Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class result = doLoadClass(name);
            if (result == null) {
                throw new ClassNotFoundException(name);
            }
            return resolveIfNecessary(result, resolve);
        }
    }

    private Class doLoadClass(String name) throws ClassNotFoundException {
        checkPackageAccess(name);
        Class result = findClassIgnoringNotFound(name);
        return (result != null) ? result : loadFromParent(name);
    }

    private Class resolveIfNecessary(Class resultClass, boolean resolve) {
        if (resolve) {
            resolveClass(resultClass);
        }
        return (resultClass);
    }

    private Class loadFromParent(String name) {
        if (this.parent == null) {
            return null;
        }
        try {
            return Class.forName(name, false, this.parent);
        }
        catch (ClassNotFoundException ex) {
            return null;
        }
    }

    private Class findClassIgnoringNotFound(String name) {
        try {
            return findClass(name);
        }
        catch (ClassNotFoundException ex) {
            return null;
        }
    }

    private void checkPackageAccess(String name) throws ClassNotFoundException {
        if (this.securityManager != null && name.lastIndexOf('.') >= 0) {
            try {
                this.securityManager
                        .checkPackageAccess(name.substring(0, name.lastIndexOf('.')));
            }
            catch (SecurityException ex) {
                throw new ClassNotFoundException("Security Violation, attempt to use "
                        + "Restricted Class: " + name, ex);
            }
        }
    }
}
测试运行结果如下:

JVM类加载机制、破坏双亲委派_第3张图片

结论:

1、不破坏双亲委派,重写 findClass 方法

2、破坏双亲委派,重写 loadClass 方法

你可能感兴趣的:(JVM)