0
点赞
收藏
分享

微信扫一扫

深入JVM内核7 类加载详解

Gascognya 2021-09-24 阅读 30

1.类加载器

1.1 启动类加载器(Bootstrap ClassLoader)

启动类加载器是使用C++语言实现的(HotSpot),负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。
启动类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib 下的jar包(如 rt.jar)和配置,然后将这些系统类加载到方法区内。
由于类加载器是使用平台相关的底层C/C++语言实现的, 所以该加载器不能被Java代码访问到。但是,我们可以查询某个类是否被引导类加载器加载过。

public class ClassLoaderStudy {
    public static void main(String[] args) {
        String str = "Hello Class Loader";
        System.out.println("str class loader==" + str.getClass().getClassLoader());
    }
}

ClassLoader是由启动类加载器加载的

str class loader==null
说明是启动类加载
1.2 扩展类加载器 (Extension ClassLoader)

此加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 {JAVA_HOME}\lib\ext 目录下的类库, 开发者可以直接获取此加载器。
拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的。

1.3 应用程序加载器 (Application ClassLoader)

此加载器负责加载用户类路径上指定的类库,若没有指定自定义加载器,则此加载器一般是程序中默认的加载器。
应用类加载器将拓展类加载器当成自己的父类加载器。

public class ClassLoaderStudy {
    public static void main(String[] args) throws ClassNotFoundException {

        ClassLoaderStudy t = new ClassLoaderStudy();
        System.out.println("t class loader==" + t.getClass().getClassLoader());
        System.out.println("t parent class loader==" + t.getClass().getClassLoader().getParent());
        System.out.println("t parent.parent class loader==" + t.getClass().getClassLoader().getParent().getParent());
    }
}
t class loader==sun.misc.Launcher$AppClassLoader@18b4aac2
t parent class loader==sun.misc.Launcher$ExtClassLoader@5b2133b1
t parent.parent class loader==null
1.4 用户自定义类加载器(Customized ClassLoader)

用户可以自己定义类加载器来加载类。所有的类加载器都要继承 java.lang.ClassLoader 类并重写 findClass(String name) 方法。用户自定义类加载器默认父加载器是 应用程序加载器

public class TestJDKClassLoader {

    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
        System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
    }
}
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader
1.5 类加载说明
  • Java程序不能直接引用启动类加载器,直接设置classLoader为null,默认就使用启动类加载器
  • 类加载器并不需要等到某个类 “首次主动使用”的时候才加载它,JVM规范允许类加载器在预料到某个类将要被使用的时候就预先加载它
  • 如果在加载的时候.class文件缺失,会在该类首次主动使用时报告LinkageError错误,如果一直没有被使用,就不会报错

2.双亲委派

2.1 双亲委派模型

双亲委派模型工作过程:一个类加载器收到类加载的请求,它首先会把这个请求委派给父类加载器去完成,层层上升,只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。

要注意的是父加载器和子加载器的关系不是继承关系而是组合关系。子加载器中有一个私有属性 parent 指向父加载器。

具体到上述三个加载器时:当应用程序加载器尝试加载类的时候,首先尝试让其父加载器–拓展类加载器加载;如果拓展类加载器加载成功,则直接返回加载结果 Class<T> instance , 加载失败,则会询问是否引导类加载器已经加载了该类;只有没有加载的时候,应用类加载器才会尝试自己加载。

从源码看双亲委派模型:

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

2.2 双亲委派目的
首先明确一点:jvm如何认定两个对象同属于一个类型,必须同时满足下面两个条件:
  • 都是用同名的类完成实例化的。
  • 两个实例各自对应的同名的类的加载器必须是同一个。比如两个相同名字的类,一个是用系统加载器加载的,一个扩展类加载器加载的,两个类生成的对象将被jvm认定为不同类型的对象。
它的好处可以用一句话总结,即防止内存中出现多份同样的字节码。
JVM加载jar包是否会将包里的所有类全部加载进内存?

JVM对class文件是按需加载(运行期间动态加载),非一次性加载,见示例(启动需要加上参数:-verbose:class)

2.3 示例

public class MyClassLoader extends ClassLoader {

    private String myName = "";

    public MyClassLoader(String myName) {
        this.myName = myName;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = this.loadClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }

    private byte[] loadClassData(String clsName) {
        byte[] data = null;
        InputStream in = null;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        clsName = clsName.replace(".", "/");

        try  {
            in = new FileInputStream(new File("classes/"+clsName+".class"));

            int a = 0;
            while ((a = in.read()) != -1){
                out.write(a);
            }
            data = out.toByteArray();
        } catch (Exception err) {
            err.printStackTrace();
        }


        return data;
    }
}
public class ClassLoaderStudy {
    public static void main(String[] args) throws ClassNotFoundException {

        MyClassLoader myClassLoader = new MyClassLoader("myClassloader1");
        Class cls1 = myClassLoader.loadClass("com.kpioneer.demo.jvm.classloader.MyClass");
        System.out.println("cls1 class loader ==" + cls1.getClassLoader());
        System.out.println("cls1 parent class loader ==" + cls1.getClassLoader().getParent());
    }
}
public class MyClass {
    public void t(){

    }
}

cls1 class loader ==sun.misc.Launcher$AppClassLoader@18b4aac2
cls1 parent class loader ==sun.misc.Launcher$ExtClassLoader@5b2133b1

将上文的MyClass.java编译后的MyClass.class 复制到新建classes文件下,同时删除src下MyClass.java文件



运行ClassLoaderStudy

public class ClassLoaderStudy {
    public static void main(String[] args) throws ClassNotFoundException {

        MyClassLoader myClassLoader = new MyClassLoader("myClassloader1");
        Class cls1 = myClassLoader.loadClass("com.kpioneer.demo.jvm.classloader.MyClass");
        System.out.println("cls1 class loader ==" + cls1.getClassLoader());
        System.out.println("cls1 parent class loader ==" + cls1.getClassLoader().getParent());
    }
}
cls1 class loader ==com.kpioneer.demo.jvm.classloader.MyClassLoader@33c7353a
cls1 parent class loader ==sun.misc.Launcher$AppClassLoader@18b4aac2
2.4 双亲委派模型说明

1、双亲委派模型对于保证Java程序的稳定运作很重要
2、实现双亲委派的代码在java.lang.ClassLoaderloadClass()方法中,如果自定义类加载器的话,推荐覆盖实现findClass()方法
3、如果有一个类加载器能加载某个类,称为定义类加载器,所有能成功返回该类的Class的类加载器都被称为初始类加载器。
4、如果没有指定父加载器,默认就是启动加载器
5、每个类加载器都有自己的命名空间,命名空间由该加载器及其所有父加载器所加载的类构成,不同的命名空间,可以出现类的全路径名相同的情况
6、由同一类加载器加载的属于相同包的类组成运行时包。决定两个类是不是属于一个运行时包,不仅看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问包可见(即默认包访问权限以及由protect修饰的类及类成员)的属性。这样的限制能避免用户自定义的类冒充核心类库去访问核心类的包可见成员。
假设用户自定义了一个java.lang.Test类,并由自定义加载器加载,Test类并不能访问核心类库java.lang.*中的包可见成员。

3.破坏双亲委派

3.1、为什么需要破坏双亲委派?

因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

3.2、破坏双亲委派的实现

我们结合Driver来看一下在spi(Service Provider Inteface)中如何实现破坏双亲委派。

    public static void main(String[] args) throws ClassNotFoundException {
  
        Class driverManager = Class.forName("java.sql.DriverManager");
        System.out.println("driverManager  class loader=="+driverManager.getClassLoader() );

        Class mysql = Class.forName("com.mysql.cj.jdbc.Driver");
        System.out.println("jdbcDriver  class loader=="+mysql.getClassLoader() );
    }
driverManager  class loader==null
jdbcDriver  class loader==sun.misc.Launcher$AppClassLoader@18b4aac2

先从DriverManager开始看,平时我们通过DriverManager来获取数据库的Connection:

        com.mysql.jdbc.Driver driver=new com.mysql.jdbc.Driver();
        DriverManager.registerDriver(driver);
        String url="jdbc:mysql://localhost:3306/mydb3";

        String username="root";
        String password="123";

        Connection con=DriverManager.getConnection(url,username,password);
        System.out.println(con);

在调用DriverManager的时候,会先初始化类,调用其中的静态块:

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

private static void loadInitialDrivers() {
    ...
        // 加载Driver的实现类
        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) {
                }
                return null;
            }
        });
    ...
}

为了节约空间,笔者省略了一部分的代码,重点来看一下

ServiceLoader.load(Driver.class):

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取当前线程中的上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

可以看到,load方法调用获取了当前线程中的上下文类加载器,那么上下文类加载器放的是什么加载器呢?

public Launcher() {
    ...
    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);
    ...
}

在sun.misc.Launcher中,我们找到了答案,在Launcher初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,而这个AppClassLoader,就是之前上文提到的系统类加载器Application ClassLoader,所以上下文类加载器默认情况下就是系统加载器。

继续来看下ServiceLoader.load(service, cl):

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // ClassLoader.getSystemClassLoader()返回的也是系统类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

上面这段就不解释了,比较简单,然后就是看LazyIterator迭代器:

private class LazyIterator implements Iterator<S>{
    // ServiceLoader的iterator()方法最后调用的是这个迭代器里的next
    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    
    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        // 根据名字来加载类
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
    
    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    
    
    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                // 在classpath下查找META-INF/services/java.sql.Driver名字的文件夹
                // private static final String PREFIX = "META-INF/services/";
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

}

总结
这个时候我们再看下整个mysql的驱动加载过程:

  • 第一,获取线程上下文类加载器,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)
  • 第二,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
  • 第三,通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端

很明显,mysql驱动采用的这种spi服务确确实实是破坏了双亲委派模型的,毕竟做到了父级类加载器加载了子级路径中的类。

举报

相关推荐

0 条评论