Java虚拟机-类加载器ClassLoader

1 类加载器

Java 的类加载,就是把字节码格式“.class”文件加载到 JVM的方法区,并在 JVM 的堆区建立一个java.lang.Class对象的实例,用来封装Java类相关的数据和方法。 Class对象可以把它理解成业务类的模板,JVM根据这个模板来创建具体业务类对象实例。

JVM类加载使用懒加载的机制,也就是说并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。JVM 类加载是由类加载器来完成的,JDK类加载器的抽象基类为ClassLoader。

在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

案例

@Test
public  void  test() throws Exception {
    /** 获取当前Class对象的类加载器加载指定类,返回Class对象**/
    Class<?> aClass = ClassLoaderTest.class.getClassLoader().loadClass("jvm.Bootstrap");
    /** 通过Class对象获取构造函数 **/
    Constructor<?> constructor = aClass.getConstructor();
    /** 通过反射构造实例化类对象**/
    Bootstrap Bootstrap = (Bootstrap)constructor.newInstance();
    System.out.println(Bootstrap);
}

2 ClassLoader类结构

image
  • ClassLoader:ClassLoader类加载器的基类,其内部存在一个类型为ClassLoader属性parent,用来实现类加载器之间的父子关系。
    public abstract class ClassLoader {
        ...
        //父类加载器
        private final ClassLoader parent;
  • URLClassLoader:URLClassLoader是ClassLoader子类,其内部存一个类型为URLClassPath属性为ucp,ucp负责管理被当前类加载器加载Class文件资源路径。
    public class URLClassLoader extends SecureClassLoader implements Closeable {
        private final URLClassPath ucp;

3 ClassLoader中核心方法

protected final Class<?> defineClass(String name, byte[] b, int  off, int len) throws ClassFormatError{ … }  

defineClass是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。

protected Class<?> findClass(String name) throws ClassNotFoundException { … }

findClass 方法的主要职责就是找到“.class”文件,.class”文件可能来自文件系统或者网络,找到后把“.class”文件读到内存中得到字节码数组,然后调用 defineClass 方法得到 Class 对象。

public synchronized Class<?> loadClass(String name) throws ClassNotFoundException{ … }  

loadClass 是个 public 方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。

4 Java 虚拟机中类加载器

在JVM中定义了4类加载器分别为:启动(Bootstrap)类加载器扩展(Extension)类加载器系统(System)类加载器,以及用户自定义加载器

image
4.1 启动(Bootstrap)类加载器

引导类加载器是负责加载并管理<JAVA_HOME>/lib 的核心类库 -Xbootclasspath选项指定的jar包class文件对应的Class对象。

启动(Bootstrap)类加载器是扩展(Extension)类加载器的父加载器,是最高等级的加载器,由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以 不允许直接通过引用进行操作

//从系统属性中获取启动(Bootstrap)类加载器加载并管理核心类库
System.getProperty("sun.boot.class.path")
C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\classes
4.2 扩展(Extension)类加载器

扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责加载并管理 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中class文件对应的Class对象。

//从系统属性中获取扩展(Extension)类加载器加载并管理核心类库
System.getProperty("java.ext.dirs")
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext;
C:\windows\Sun\Java\lib\ext
4.3 系统(System)类加载器

系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责加载并管理用户类路径(java -classpath或-Djava.class.path变量所指定的URL资源)中class文件对应的Class对象。开发者可以直接使用系统类加载器。

//从系统属性中获取系统(System)类加载器加载并管理核心类库
System.getProperty("java.class.path")
...省略
D:\project_alibaba\jvm-in-action\target\classes;
...省略
4.4 案例
package jvm;
import java.lang.reflect.Constructor;

/**获取JVM类加载器 **/
public class ClassLoaderTest {
    public static void main(String[] args) {
        try {
            System.out.println(ClassLoader.getSystemClassLoader());
            System.out.println(ClassLoader.getSystemClassLoader().getParent());
            System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@45ee12a7
null

这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。

5 类加载(双亲委派机制)

首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,直到最终交给不存在父类加载器的启动(Bootstrap)类加载器加载。

image
5.1 实现流程

双亲委派模型对于保证Java程序的稳定运作很重要,它的具体实现在java.lang.ClassLoader类loadClass()方法中。

public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  

protected synchronized Class<?> loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  

    // 首先判断该类是否已经被加载  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        //如果没有被加载,就委托给父类加载
        try {  
            if (parent != null) {  
                //如果存在父类加载器,就委派给父类加载器加载,这里是一个递归调用的过程。  
                c = parent.loadClass(name, false);  
            } else {    // 递归终止条件
                // 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
                // parent == null就意味着由启动类加载器尝试加载该类,  
                // 即通过调用 native方法 findBootstrapClass0(String name)加载  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
            // 注意,若自身也加载不了,也会产生ClassNotFoundException异常并向上抛出
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}
//没错!此方法没有具体实现,只是抛了一个异常,而且访问权限是protected。这充分证明了:这个方法就是给开发者重写用的,即自定义类加载器时需实现此方法!
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
  • AppClassLoader,ExtClassLoader 对findClass实现都继承自URLClassLoader,

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        //将类的包路径转换为url资源路径
                        String path = name.replace('.', '/').concat(".class");
                        //通过URLClassLoader URLClassPath ucp中获取url路径对应的二进制数据
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                //读取二进制获取调用defineClass获取Class对象象
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
5.2 案例

ClassLoaderTest,EventID,Sdp属于被不同类加载器管理的类对象,使用相同系统类加载器加载时,由于采用双亲委派机制,获取Class对象是由不同的类加载器加载。

@Test
    public void classLoaderLoadClass2() throws IOException {
        try {
            //调用加载当前类的类加载器(这里即为系统类加载器)加载ClassLoaderTest
            Class typeLoaded = ClassLoaderTest.class.getClassLoader().loadClass("com.wuhao.jvm.classLoader.ClassLoaderTest");
            //查看被加载的ClassLoaderTest对象是被那个类加载器加载的
            System.out.println(typeLoaded.getClassLoader());

            //调用加载当前类的类加载器(这里即为系统类加载器)加载EventID
            Class typeLoaded1 = ClassLoaderTest.class.getClassLoader().loadClass("com.sun.java.accessibility.util.EventID");
            //查看被加载的ClassLoaderTest对象是被那个类加载器加载的
            System.out.println(typeLoaded1.getClassLoader());

            //调用加载当前类的类加载器(这里即为系统类加载器)加载Sdp
            Class typeLoaded2 = ClassLoaderTest.class.getClassLoader().loadClass("com.oracle.net.Sdp");
            //查看被加载的ClassLoaderTest对象是被那个类加载器加载的
            System.out.println(typeLoaded2.getClassLoader());
        } catch (Exception e) {
        }
    }
5.3 模式优点

双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载).

例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但是永远无法被加载运行。

5.4 模式缺点

由于双亲委派模型存在,一个类被谁加载是由类加载器管理资源所决定,即使某个类存在于多个类加载器管理资源中。也只会被更上程的类加载器加载。这样就会导致一个在被上层类加载中Class中获取的上程类加载器,是无法加载下层类加载器中管理Class对象。

5.5 线程上下文类加载器

为了解决这个问题JVM引用了线程上下文类加载器

这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使用系统程序类加载器。

5.6 如何破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式。大多数的类加载器都遵循这个模型,双亲委派模型的具体逻辑实现在ClassLoader的loadClass方法,如果我们自定义类加载器,采用双亲委派模型:只需要重写ClassLoader的findClass()方法即可,破坏双亲委派模型:重写ClassLoader的整个loadClass()方法(因为双亲委派模型的逻辑主要实现就在此方法中,若我们重写即可破坏掉。)

6 Java虚拟机对类加器初始化

当运行一个Java程序流程:

1.根据JVM内存配置要求,为JVM申请特定大小的内存空间;

2.创建一个引导类加载器实例,初步加载系统类到内存方法区区域中;

3.创建JVM 启动器实例 Launcher,并取得构造扩展(Extension)类加载器,系统(System)类加载器;

4.使用系统(System)类加载器加载我们定义的 org.luanlouis.jvm.load.Main类;

5.加载完成时候JVM会执行Main类的main方法入口,执行Main类的main方法;

6.结束,java程序运行结束,JVM销毁。

6.1 初始化Launcher
public class Launcher {

 public Launcher() {
        //创建扩展(Extension)类加载器
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        //创建系统(System)类加载器
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(this.loader);
        
    public ClassLoader getClassLoader() {
        return loader;
    }

    static class ExtClassLoader extends URLClassLoader {}


    static class AppClassLoader extends URLClassLoader {}
6.2 ExtClassLoader实例化
  • 1 读取System.getProperty("java.ext.dirs")参数对应扩展类加载器需要加载jar文件路径转换为 File[] 文件数组。

  • 2 调用public ExtClassLoader(File[] var1) 实例化 ExtClassLoader 扩展类加载器。

  • 3 实例化ExtClassLoader前实例化父类URLClassLoader,实例化URLClassLoader需要将File[]转换为URL[]作为第一个参数传入,并传入null来表示父类加载器
    为启动(Bootstrap)类加载器。

static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            //1 读取扩展类加载器需要加载的文件列表
            final File[] var0 = getExtDirs();

            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        int var1 = var0.length;

                        for(int var2 = 0; var2 < var1; ++var2) {
                            MetaIndex.registerDirectory(var0[var2]);
                        }
                        //2 实例化ExtClassLoader
                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) {
                throw (IOException)var2.getException();
            }
        }

        void addExtURL(URL var1) {
            super.addURL(var1);
        }

        //3 构造扩展类加载器
        public ExtClassLoader(File[] var1) throws IOException {
            //3 实例化ExtClassLoader前实例化父类URLClassLoader
            //第一个参数将File[]转换为URL[] 
            //第二个参数用来填写父类加载器,这里父类加载器为启动加载器,用null表示
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }


        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }


        private static URL[] getExtURLs(File[] var0) throws IOException {
            Vector var1 = new Vector();

            for(int var2 = 0; var2 < var0.length; ++var2) {
                String[] var3 = var0[var2].list();
                if (var3 != null) {
                    for(int var4 = 0; var4 < var3.length; ++var4) {
                        if (!var3[var4].equals("meta-index")) {
                            File var5 = new File(var0[var2], var3[var4]);
                            var1.add(Launcher.getFileURL(var5));
                        }
                    }
                }
            }

            URL[] var6 = new URL[var1.size()];
            var1.copyInto(var6);
            return var6;
        }
        ...省略代码
6.3 AppClassLoader实例化

1 读取System.getProperty("java.class.path")参数对应扩展类加载器需要加载jar文件路径转换为 File[] 文件数组

2 将File[] var1转换为URL[] 构建AppClassLoader

3 构建父类URLClassLoader 传入父加载器var2为扩展类加载器

 static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);


        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            1 读取System.getProperty("java.class.path")参数对应扩展类加载器需要加载jar文件路径转换为 File[] 文件数组
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                   //将File[] var1转换为URL[] 构建AppClassLoader
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    //构建AppClassLoader 
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

        AppClassLoader(URL[] var1, ClassLoader var2) {
            //构建父类URLClassLoader 传入父加载器var2为扩展类加载器
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
6.4 URLClassLoader 初始化
  • 1 将URL资源交给URLClassPath类型的字段管理
    public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
         1 将URL资源交给URLClassPath类型的字段管理
        ucp = new URLClassPath(urls, factory);
        acc = AccessController.getContext();
    }
6.5 ClassLoader 初始化
  • 1 设置父类加载器给属性parent
private final ClassLoader parent;

        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    
    private ClassLoader(Void unused, ClassLoader parent) {
        //1 设置父类加载器给属性parent
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容